diff --git a/Cargo.toml b/Cargo.toml index 107e6d3..3edbc96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 652 +# version: 653 [workspace] resolver = "3" members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"] [workspace.package] -version = "0.3.17-pre.1" +version = "0.3.17-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-api/src/capability.rs b/crates/ksp-store-api/src/capability.rs index 9b7b686..9da9166 100644 --- a/crates/ksp-store-api/src/capability.rs +++ b/crates/ksp-store-api/src/capability.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/capability.rs -// version: 3 +// version: 4 //! Private home for backend-agnostic Store capability contracts. //! @@ -9,6 +9,7 @@ //! outside `ksp-store-api`. pub(crate) mod raw_account; +pub(crate) mod raw_conflict; pub(crate) mod raw_retention; pub(crate) mod raw_transaction; @@ -17,3 +18,12 @@ pub(crate) mod raw_transaction; /// The alias uses only standard-library primitives so backend implementations /// need no async helper dependency merely to implement `ksp-store-api`. pub type StoreApiFuture<'a, T> = std::pin::Pin + std::marker::Send + 'a>>; + +/// Backend-neutral classifier for errors crossing a Store capability boundary. +/// +/// Implementations must classify from structured KSP error state. Consumers such +/// as Workers must not infer retryability by matching backend-specific strings. +pub trait StoreErrorClassifier: std::marker::Send + std::marker::Sync { + /// Classifies one already-redacted Store error as retryable/transient or terminal. + fn classify_store_error(&self, error: &crate::Error) -> crate::StoreErrorClass; +} diff --git a/crates/ksp-store-api/src/capability/raw_conflict.rs b/crates/ksp-store-api/src/capability/raw_conflict.rs new file mode 100644 index 0000000..fc5100a --- /dev/null +++ b/crates/ksp-store-api/src/capability/raw_conflict.rs @@ -0,0 +1,50 @@ +// file: crates/ksp-store-api/src/capability/raw_conflict.rs +// version: 1 + +/// Read capability for bounded operator inspection of persisted RAW transaction variants. +pub trait RawTransactionVariantInspectionRead: std::marker::Send + std::marker::Sync { + /// Inspects one random-access variant window with exact logical counts. + fn inspect_raw_transaction_variants<'a>( + &'a self, + query: &'a crate::RawTransactionVariantInspectionQuery, + ) -> crate::StoreApiFuture<'a, crate::Result>>; + + /// Reads explicit detail for one durable variant reference. + fn get_raw_transaction_variant<'a>( + &'a self, + reference: &'a crate::RawTransactionVariantReference, + ) -> crate::StoreApiFuture<'a, crate::Result>>; +} + +/// Read capability for bounded operator inspection of RAW transaction conflict cases. +pub trait RawTransactionConflictInspectionRead: std::marker::Send + std::marker::Sync { + /// Inspects one random-access conflict-case window with exact logical counts. + fn inspect_raw_transaction_conflicts<'a>( + &'a self, + query: &'a crate::RawTransactionConflictInspectionQuery, + ) -> crate::StoreApiFuture<'a, crate::Result>>; + + /// Reads one payload-free conflict case including its monotone participant ledger. + fn get_raw_transaction_conflict<'a>( + &'a self, + reference: &'a crate::RawTransactionConflictReference, + ) -> crate::StoreApiFuture<'a, crate::Result>>; +} + +/// Read capability for one RAW transaction conflict case append-only history. +pub trait RawTransactionConflictHistoryRead: std::marker::Send + std::marker::Sync { + /// Inspects one random-access conflict-history window ordered by durable revision. + fn inspect_raw_transaction_conflict_history<'a>( + &'a self, + query: &'a crate::RawTransactionConflictHistoryQuery, + ) -> crate::StoreApiFuture<'a, crate::Result>>; +} + +/// Write capability for explicit compare-and-set RAW transaction conflict actions. +pub trait RawTransactionConflictActionWrite: std::marker::Send + std::marker::Sync { + /// Applies one revision-guarded conflict action atomically. + fn apply_raw_transaction_conflict_action<'a>( + &'a self, + request: crate::RawTransactionConflictActionRequest, + ) -> crate::StoreApiFuture<'a, crate::Result>; +} diff --git a/crates/ksp-store-api/src/error.rs b/crates/ksp-store-api/src/error.rs index 615c1a5..4253deb 100644 --- a/crates/ksp-store-api/src/error.rs +++ b/crates/ksp-store-api/src/error.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/error.rs -// version: 2 +// version: 3 /// 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"); @@ -13,3 +13,13 @@ pub const ERROR_CODE_RAW_PROVENANCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_ 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"); + +/// Backend-neutral retryability class for errors crossing a Store capability boundary. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum StoreErrorClass { + /// The operation may be retried by an owning runtime under an explicit bounded policy. + Transient, + /// Retrying without an external state/configuration change is not safe or not justified. + Terminal, +} diff --git a/crates/ksp-store-api/src/lib.rs b/crates/ksp-store-api/src/lib.rs index 5b6e415..e5d5803 100644 --- a/crates/ksp-store-api/src/lib.rs +++ b/crates/ksp-store-api/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/lib.rs -// version: 9 +// version: 10 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -18,6 +18,8 @@ mod error; mod model; /// Boxed async operation returned by object-safe Store capability contracts. pub use self::capability::StoreApiFuture; +/// Backend-neutral classifier for Store errors crossing capability boundaries. +pub use self::capability::StoreErrorClassifier; /// Read capability for random-access RAW account-observation inspection. pub use self::capability::raw_account::RawAccountObservationInspectionRead; /// Read capability for persisted RAW account-state observations. @@ -30,6 +32,14 @@ pub use self::capability::raw_account::RawAccountStateInspectionRead; pub use self::capability::raw_account::RawAccountStateRead; /// Write capability for complete canonical RAW account-state acquisitions. pub use self::capability::raw_account::RawAccountStateWrite; +/// Write capability for explicit revision-guarded RAW transaction conflict actions. +pub use self::capability::raw_conflict::RawTransactionConflictActionWrite; +/// Read capability for one RAW transaction conflict case append-only history. +pub use self::capability::raw_conflict::RawTransactionConflictHistoryRead; +/// Read capability for bounded operator inspection of RAW transaction conflict cases. +pub use self::capability::raw_conflict::RawTransactionConflictInspectionRead; +/// Read capability for bounded operator inspection of persisted RAW transaction variants. +pub use self::capability::raw_conflict::RawTransactionVariantInspectionRead; /// Read capability for canonical RAW transaction retention metadata. pub use self::capability::raw_retention::RawTransactionRetentionRead; /// Write capability for policy-authorized RAW transaction retention transitions. @@ -58,12 +68,48 @@ pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID; 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; +/// Backend-neutral retryability class for errors crossing Store capability boundaries. +pub use self::error::StoreErrorClass; /// Persistable acquisition observation linked to one complete canonical RAW account state. pub use self::model::raw_account::RawAccountObservation; /// Canonical complete N1 RAW account state independent from acquisition transport. pub use self::model::raw_account::RawAccountState; /// Durable backend-independent identity of one canonical RAW account state. pub use self::model::raw_account::RawAccountStateReference; +/// Explicit mutable operation over one RAW transaction conflict case. +pub use self::model::raw_conflict::RawTransactionConflictAction; +/// Durable backend-neutral outcome of one conflict-case action request. +pub use self::model::raw_conflict::RawTransactionConflictActionOutcome; +/// Compare-and-set request for one mutable RAW transaction conflict action. +pub use self::model::raw_conflict::RawTransactionConflictActionRequest; +/// Detailed payload-free conflict projection including the participant ledger. +pub use self::model::raw_conflict::RawTransactionConflictDetail; +/// Stable append-only event kind for RAW transaction conflict history. +pub use self::model::raw_conflict::RawTransactionConflictEventKind; +/// Safe semantic origin of one conflict-history transition. +pub use self::model::raw_conflict::RawTransactionConflictEventOrigin; +/// Payload-free append-only history event for one RAW transaction conflict case. +pub use self::model::raw_conflict::RawTransactionConflictEventSummary; +/// Backend-independent random-access query for one conflict case history. +pub use self::model::raw_conflict::RawTransactionConflictHistoryQuery; +/// Backend-independent random-access inspection query for conflict cases. +pub use self::model::raw_conflict::RawTransactionConflictInspectionQuery; +/// Payload-free durable participant of one RAW transaction conflict case. +pub use self::model::raw_conflict::RawTransactionConflictParticipantSummary; +/// Durable backend-independent identity of one RAW transaction conflict case. +pub use self::model::raw_conflict::RawTransactionConflictReference; +/// Explicit canonical resolution strategy used when closing one conflict case. +pub use self::model::raw_conflict::RawTransactionConflictResolution; +/// Durable action classification recorded for a conflict resolution. +pub use self::model::raw_conflict::RawTransactionConflictResolutionAction; +/// Payload-free current projection of one RAW transaction conflict case. +pub use self::model::raw_conflict::RawTransactionConflictSummary; +/// Explicit detail for one RAW transaction variant. +pub use self::model::raw_conflict::RawTransactionVariantDetail; +/// Backend-independent random-access inspection query for RAW transaction variants. +pub use self::model::raw_conflict::RawTransactionVariantInspectionQuery; +/// Payload-free summary of one persisted RAW transaction variant. +pub use self::model::raw_conflict::RawTransactionVariantSummary; /// Backend-independent random-access inspection query for RAW account observations. pub use self::model::raw_inspection::RawAccountObservationInspectionQuery; /// Safe observation summary for one canonical RAW account-state acquisition. diff --git a/crates/ksp-store-api/src/model.rs b/crates/ksp-store-api/src/model.rs index c1c4035..ddb9c9a 100644 --- a/crates/ksp-store-api/src/model.rs +++ b/crates/ksp-store-api/src/model.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/model.rs -// version: 5 +// version: 6 //! Private home for persistent Store models. //! @@ -9,6 +9,7 @@ //! one or more models. pub(crate) mod raw_account; +pub(crate) mod raw_conflict; pub(crate) mod raw_inspection; pub(crate) mod raw_outcome; pub(crate) mod raw_pagination; diff --git a/crates/ksp-store-api/src/model/raw_conflict.rs b/crates/ksp-store-api/src/model/raw_conflict.rs new file mode 100644 index 0000000..d3f52dc --- /dev/null +++ b/crates/ksp-store-api/src/model/raw_conflict.rs @@ -0,0 +1,951 @@ +// file: crates/ksp-store-api/src/model/raw_conflict.rs +// version: 1 + +/// Durable backend-independent identity of one RAW transaction conflict case. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct RawTransactionConflictReference { + transaction: crate::RawTransactionReference, +} + +impl RawTransactionConflictReference { + /// Creates one conflict-case identity from the owning transaction identity. + #[must_use] + pub fn new(transaction: crate::RawTransactionReference) -> Self { + return Self { transaction }; + } + + /// Returns the logical transaction identity owning this conflict case. + #[must_use] + pub fn transaction(&self) -> &crate::RawTransactionReference { + return &self.transaction; + } +} + +/// Backend-independent random-access inspection query for RAW transaction variants. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionVariantInspectionQuery { + direction: crate::RawSortDirection, + network: crate::RawNetworkId, + page: crate::RawInspectionPageRequest, + transaction: std::option::Option, +} + +impl RawTransactionVariantInspectionQuery { + /// Creates one variant-inspection query after enforcing network coherence. + pub fn try_new( + network: crate::RawNetworkId, + transaction: std::option::Option, + direction: crate::RawSortDirection, + page: crate::RawInspectionPageRequest, + ) -> crate::Result { + if let std::option::Option::Some(reference) = transaction.as_ref() + && reference.network() != &network + { + return std::result::Result::Err(raw_query_error("transaction")); + } + return std::result::Result::Ok(Self { direction, network, page, transaction }); + } + + /// Returns the requested deterministic traversal direction. + #[must_use] + pub const fn direction(&self) -> crate::RawSortDirection { + return self.direction; + } + + /// Returns the mandatory logical network scope. + #[must_use] + pub fn network(&self) -> &crate::RawNetworkId { + return &self.network; + } + + /// Returns the random-access inspection page request. + #[must_use] + pub const fn page(&self) -> crate::RawInspectionPageRequest { + return self.page; + } + + /// Returns the optional exact transaction identity used to filter variants. + #[must_use] + pub fn transaction(&self) -> std::option::Option<&crate::RawTransactionReference> { + return self.transaction.as_ref(); + } +} + +/// Payload-free summary of one persisted RAW transaction variant. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionVariantSummary { + block_time: std::option::Option, + created_at: crate::RawTimestamp, + format_id: crate::RawFormatId, + format_version: u32, + is_current_canonical: bool, + observation_count: u64, + origin: crate::RawTransactionVariantOrigin, + payload_size_bytes: std::option::Option, + reference: crate::RawTransactionVariantReference, + retention_state: crate::RawRetentionState, + slot: u64, +} + +impl RawTransactionVariantSummary { + /// Creates one payload-free variant summary after validating bounded metadata. + #[allow(clippy::too_many_arguments)] + pub fn try_new( + reference: crate::RawTransactionVariantReference, + origin: crate::RawTransactionVariantOrigin, + slot: u64, + block_time: std::option::Option, + format_id: crate::RawFormatId, + format_version: u32, + retention_state: crate::RawRetentionState, + payload_size_bytes: std::option::Option, + is_current_canonical: bool, + observation_count: u64, + created_at: crate::RawTimestamp, + ) -> crate::Result { + if format_version == 0 { + return std::result::Result::Err(raw_model_error("format_version")); + } + if let std::option::Option::Some(size) = payload_size_bytes { + let size = match usize::try_from(size) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("payload_size_bytes")), + }; + if size == 0 || size > crate::MAX_RAW_PAYLOAD_BYTES { + return std::result::Result::Err(raw_model_error("payload_size_bytes")); + } + } + if retention_state == crate::RawRetentionState::Purged && payload_size_bytes.is_some() { + return std::result::Result::Err(raw_model_error("payload_size_bytes")); + } + return std::result::Result::Ok(Self { + block_time, + created_at, + format_id, + format_version, + is_current_canonical, + observation_count, + origin, + payload_size_bytes, + reference, + retention_state, + slot, + }); + } + + /// Returns the optional block timestamp carried by this variant. + #[must_use] + pub const fn block_time(&self) -> std::option::Option { + return self.block_time; + } + + /// Returns when this variant identity was first persisted. + #[must_use] + pub const fn created_at(&self) -> crate::RawTimestamp { + return self.created_at; + } + + /// Returns the KSP-owned RAW format identifier. + #[must_use] + pub fn format_id(&self) -> &crate::RawFormatId { + return &self.format_id; + } + + /// Returns the KSP-owned RAW format version. + #[must_use] + pub const fn format_version(&self) -> u32 { + return self.format_version; + } + + /// Returns whether this variant is selected as the current canonical representation. + #[must_use] + pub const fn is_current_canonical(&self) -> bool { + return self.is_current_canonical; + } + + /// Returns the number of durable acquisition observations linked to this variant. + #[must_use] + pub const fn observation_count(&self) -> u64 { + return self.observation_count; + } + + /// Returns the durable origin classification of this variant. + #[must_use] + pub const fn origin(&self) -> crate::RawTransactionVariantOrigin { + return self.origin; + } + + /// Returns the payload size when that metadata remains available. + #[must_use] + pub const fn payload_size_bytes(&self) -> std::option::Option { + return self.payload_size_bytes; + } + + /// Returns the durable variant reference. + #[must_use] + pub fn reference(&self) -> &crate::RawTransactionVariantReference { + return &self.reference; + } + + /// Returns the logical payload-retention state of this variant. + #[must_use] + pub const fn retention_state(&self) -> crate::RawRetentionState { + return self.retention_state; + } + + /// Returns the Solana slot carried by this variant. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } +} + +/// Explicit detail for one RAW transaction variant. +#[derive(Debug)] +pub struct RawTransactionVariantDetail { + summary: crate::RawTransactionVariantSummary, + transaction: std::option::Option, +} + +impl RawTransactionVariantDetail { + /// Creates one variant detail and validates any attached complete RAW transaction. + pub fn try_new(summary: crate::RawTransactionVariantSummary, transaction: std::option::Option) -> crate::Result { + if let std::option::Option::Some(value) = transaction.as_ref() { + if value.reference() != summary.reference().transaction() + || value.slot() != summary.slot() + || value.block_time() != summary.block_time() + || value.payload().format_id() != summary.format_id() + || value.payload().format_version() != summary.format_version() + { + return std::result::Result::Err(raw_model_error("transaction")); + } + let payload_size = match u64::try_from(value.payload().bytes().len()) { + std::result::Result::Ok(size) => size, + std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("transaction")), + }; + if let std::option::Option::Some(expected) = summary.payload_size_bytes() + && payload_size != expected + { + return std::result::Result::Err(raw_model_error("transaction")); + } + if summary.retention_state() == crate::RawRetentionState::Purged { + return std::result::Result::Err(raw_model_error("transaction")); + } + } + return std::result::Result::Ok(Self { summary, transaction }); + } + + /// Returns the payload-free variant summary. + #[must_use] + pub fn summary(&self) -> &crate::RawTransactionVariantSummary { + return &self.summary; + } + + /// Returns the complete RAW transaction only when this inspection response includes local payload material. + #[must_use] + pub fn transaction(&self) -> std::option::Option<&crate::RawTransaction> { + return self.transaction.as_ref(); + } +} + +/// Backend-independent random-access inspection query for RAW transaction conflict cases. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionConflictInspectionQuery { + direction: crate::RawSortDirection, + network: crate::RawNetworkId, + page: crate::RawInspectionPageRequest, + status: std::option::Option, + transaction: std::option::Option, +} + +impl RawTransactionConflictInspectionQuery { + /// Creates one conflict-inspection query after enforcing network coherence. + pub fn try_new( + network: crate::RawNetworkId, + status: std::option::Option, + transaction: std::option::Option, + direction: crate::RawSortDirection, + page: crate::RawInspectionPageRequest, + ) -> crate::Result { + if let std::option::Option::Some(reference) = transaction.as_ref() + && reference.network() != &network + { + return std::result::Result::Err(raw_query_error("transaction")); + } + return std::result::Result::Ok(Self { direction, network, page, status, transaction }); + } + + /// Returns the requested deterministic traversal direction. + #[must_use] + pub const fn direction(&self) -> crate::RawSortDirection { + return self.direction; + } + + /// Returns the mandatory logical network scope. + #[must_use] + pub fn network(&self) -> &crate::RawNetworkId { + return &self.network; + } + + /// Returns the random-access inspection page request. + #[must_use] + pub const fn page(&self) -> crate::RawInspectionPageRequest { + return self.page; + } + + /// Returns the optional lifecycle-status filter. + #[must_use] + pub const fn status(&self) -> std::option::Option { + return self.status; + } + + /// Returns the optional exact transaction identity used to filter conflict cases. + #[must_use] + pub fn transaction(&self) -> std::option::Option<&crate::RawTransactionReference> { + return self.transaction.as_ref(); + } +} + +/// Durable action recorded as the resolution decision of one conflict case. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionConflictResolutionAction { + /// Keep the current canonical selector unchanged while resolving the case. + KeepCurrentCanonical, + /// Select a preserved participant as the canonical representation. + PromoteVariant, + /// Restore and select a preserved local participant as the canonical representation. + RestoreVariant, +} + +impl RawTransactionConflictResolutionAction { + /// Returns the stable persistence code for this resolution action. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::KeepCurrentCanonical => "keep_current_canonical", + Self::PromoteVariant => "promote_variant", + Self::RestoreVariant => "restore_variant", + }; + } +} + +/// Payload-free current projection of one RAW transaction conflict case. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionConflictSummary { + canonical_revision: u64, + canonical_variant: crate::RawTransactionVariantReference, + created_at: crate::RawTimestamp, + latest_incoming_variant: crate::RawTransactionVariantReference, + latest_relation: crate::RawTransactionVariantRelation, + latest_reason: crate::RawTransactionVariantRelationReason, + latest_resolution_action: std::option::Option, + participant_count: u64, + reference: crate::RawTransactionConflictReference, + resolved_canonical_variant: std::option::Option, + revision: u64, + status: crate::RawTransactionConflictStatus, + updated_at: crate::RawTimestamp, +} + +impl RawTransactionConflictSummary { + /// Creates one current conflict projection after validating revisions, scopes and status-dependent fields. + #[allow(clippy::too_many_arguments)] + pub fn try_new( + reference: crate::RawTransactionConflictReference, + status: crate::RawTransactionConflictStatus, + revision: u64, + canonical_variant: crate::RawTransactionVariantReference, + canonical_revision: u64, + latest_incoming_variant: crate::RawTransactionVariantReference, + latest_relation: crate::RawTransactionVariantRelation, + latest_reason: crate::RawTransactionVariantRelationReason, + resolved_canonical_variant: std::option::Option, + latest_resolution_action: std::option::Option, + participant_count: u64, + created_at: crate::RawTimestamp, + updated_at: crate::RawTimestamp, + ) -> crate::Result { + if revision == 0 || canonical_revision == 0 || participant_count < 2 || updated_at < created_at { + return std::result::Result::Err(raw_model_error("conflict_projection")); + } + if canonical_variant.transaction() != reference.transaction() || latest_incoming_variant.transaction() != reference.transaction() { + return std::result::Result::Err(raw_model_error("variant_reference")); + } + let comparison = crate::RawTransactionVariantComparison::try_new(latest_relation, latest_reason); + if comparison.is_err() + || !matches!(latest_relation, crate::RawTransactionVariantRelation::Conflict | crate::RawTransactionVariantRelation::Incomparable) + { + return std::result::Result::Err(raw_model_error("latest_relation")); + } + match status { + crate::RawTransactionConflictStatus::Open => { + if resolved_canonical_variant.is_some() || latest_resolution_action.is_some() { + return std::result::Result::Err(raw_model_error("resolution")); + } + }, + crate::RawTransactionConflictStatus::Resolved => { + let resolved = match resolved_canonical_variant.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(raw_model_error("resolved_canonical_variant")), + }; + if resolved.transaction() != reference.transaction() || latest_resolution_action.is_none() { + return std::result::Result::Err(raw_model_error("resolution")); + } + }, + } + return std::result::Result::Ok(Self { + canonical_revision, + canonical_variant, + created_at, + latest_incoming_variant, + latest_relation, + latest_reason, + latest_resolution_action, + participant_count, + reference, + resolved_canonical_variant, + revision, + status, + updated_at, + }); + } + + /// Returns the current canonical-selector revision. + #[must_use] + pub const fn canonical_revision(&self) -> u64 { + return self.canonical_revision; + } + + /// Returns the current canonical variant. + #[must_use] + pub fn canonical_variant(&self) -> &crate::RawTransactionVariantReference { + return &self.canonical_variant; + } + + /// Returns when this conflict case was created. + #[must_use] + pub const fn created_at(&self) -> crate::RawTimestamp { + return self.created_at; + } + + /// Returns the most recently persisted incoming divergent participant. + #[must_use] + pub fn latest_incoming_variant(&self) -> &crate::RawTransactionVariantReference { + return &self.latest_incoming_variant; + } + + /// Returns the latest divergent relation. + #[must_use] + pub const fn latest_relation(&self) -> crate::RawTransactionVariantRelation { + return self.latest_relation; + } + + /// Returns the latest divergent relation reason. + #[must_use] + pub const fn latest_reason(&self) -> crate::RawTransactionVariantRelationReason { + return self.latest_reason; + } + + /// Returns the latest durable resolution action when the case is resolved. + #[must_use] + pub const fn latest_resolution_action(&self) -> std::option::Option { + return self.latest_resolution_action; + } + + /// Returns the durable participant count for this conflict case. + #[must_use] + pub const fn participant_count(&self) -> u64 { + return self.participant_count; + } + + /// Returns the durable conflict-case reference. + #[must_use] + pub fn reference(&self) -> &crate::RawTransactionConflictReference { + return &self.reference; + } + + /// Returns the explicit resolved canonical variant when the case is resolved. + #[must_use] + pub fn resolved_canonical_variant(&self) -> std::option::Option<&crate::RawTransactionVariantReference> { + return self.resolved_canonical_variant.as_ref(); + } + + /// Returns the strictly positive current conflict revision. + #[must_use] + pub const fn revision(&self) -> u64 { + return self.revision; + } + + /// Returns the current lifecycle status. + #[must_use] + pub const fn status(&self) -> crate::RawTransactionConflictStatus { + return self.status; + } + + /// Returns when this current projection last changed. + #[must_use] + pub const fn updated_at(&self) -> crate::RawTimestamp { + return self.updated_at; + } +} + +/// Payload-free durable participant of one RAW transaction conflict case. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionConflictParticipantSummary { + first_seen_at: crate::RawTimestamp, + first_seen_revision: u64, + variant: crate::RawTransactionVariantReference, +} + +impl RawTransactionConflictParticipantSummary { + /// Creates one participant summary with a strictly positive first-seen revision. + pub fn try_new(variant: crate::RawTransactionVariantReference, first_seen_revision: u64, first_seen_at: crate::RawTimestamp) -> crate::Result { + if first_seen_revision == 0 { + return std::result::Result::Err(raw_model_error("first_seen_revision")); + } + return std::result::Result::Ok(Self { first_seen_at, first_seen_revision, variant }); + } + + /// Returns when this participant first entered the conflict case. + #[must_use] + pub const fn first_seen_at(&self) -> crate::RawTimestamp { + return self.first_seen_at; + } + + /// Returns the conflict revision at which this participant was first observed. + #[must_use] + pub const fn first_seen_revision(&self) -> u64 { + return self.first_seen_revision; + } + + /// Returns the durable variant reference. + #[must_use] + pub fn variant(&self) -> &crate::RawTransactionVariantReference { + return &self.variant; + } +} + +/// Detailed payload-free conflict projection including the monotone participant ledger. +#[derive(Debug)] +pub struct RawTransactionConflictDetail { + participants: std::vec::Vec, + summary: crate::RawTransactionConflictSummary, +} + +impl RawTransactionConflictDetail { + /// Creates one detail after validating participant count, scope and required current references. + pub fn try_new( + summary: crate::RawTransactionConflictSummary, + participants: std::vec::Vec, + ) -> crate::Result { + let participant_count = match u64::try_from(participants.len()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("participants")), + }; + if participant_count != summary.participant_count() { + return std::result::Result::Err(raw_model_error("participants")); + } + let mut ids = std::collections::BTreeSet::new(); + for participant in &participants { + if participant.variant().transaction() != summary.reference().transaction() + || participant.first_seen_revision() > summary.revision() + || !ids.insert(participant.variant().variant_id()) + { + return std::result::Result::Err(raw_model_error("participants")); + } + } + for required in [ + std::option::Option::Some(summary.canonical_variant()), + std::option::Option::Some(summary.latest_incoming_variant()), + summary.resolved_canonical_variant(), + ] + .into_iter() + .flatten() + { + if !ids.contains(&required.variant_id()) { + return std::result::Result::Err(raw_model_error("participants")); + } + } + return std::result::Result::Ok(Self { participants, summary }); + } + + /// Returns the monotone participant ledger projection. + #[must_use] + pub fn participants(&self) -> &[crate::RawTransactionConflictParticipantSummary] { + return self.participants.as_slice(); + } + + /// Returns the current conflict projection. + #[must_use] + pub fn summary(&self) -> &crate::RawTransactionConflictSummary { + return &self.summary; + } +} + +/// Stable append-only event kind for RAW transaction conflict history. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionConflictEventKind { + /// A new conflict case was opened from a proved divergence. + Opened, + /// A previously unseen variant participant was added to the conflict case. + ParticipantAdded, + /// The case was resolved while retaining the current canonical selector. + ResolvedKeepCanonical, + /// The case was resolved by selecting another participant as canonical. + ResolvedPromoteVariant, + /// The case was resolved by restoring and selecting a preserved local participant. + ResolvedRestoreVariant, + /// A resolved case became open again. + Reopened, +} + +impl RawTransactionConflictEventKind { + /// Returns the stable persistence code for this history event kind. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Opened => "opened", + Self::ParticipantAdded => "participant_added", + Self::ResolvedKeepCanonical => "resolved_keep_canonical", + Self::ResolvedPromoteVariant => "resolved_promote_variant", + Self::ResolvedRestoreVariant => "resolved_restore_variant", + Self::Reopened => "reopened", + }; + } +} + +/// Safe semantic origin of one conflict-history transition. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionConflictEventOrigin { + /// The transition was caused by deterministic Store/runtime behavior. + Automatic, + /// The transition was requested through an operator-facing action contract. + Operator, +} + +impl RawTransactionConflictEventOrigin { + /// Returns the stable persistence code for this event origin. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Automatic => "automatic", + Self::Operator => "operator", + }; + } +} + +/// Payload-free append-only history event for one RAW transaction conflict case. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionConflictEventSummary { + candidate: std::option::Option, + canonical_after: std::option::Option, + canonical_before: std::option::Option, + comparison: std::option::Option, + kind: crate::RawTransactionConflictEventKind, + origin: crate::RawTransactionConflictEventOrigin, + reference: crate::RawTransactionConflictReference, + resolution_action: std::option::Option, + revision: u64, + timestamp: crate::RawTimestamp, +} + +impl RawTransactionConflictEventSummary { + /// Creates one payload-free history event after validating revision and variant scopes. + #[allow(clippy::too_many_arguments)] + pub fn try_new( + reference: crate::RawTransactionConflictReference, + revision: u64, + kind: crate::RawTransactionConflictEventKind, + origin: crate::RawTransactionConflictEventOrigin, + canonical_before: std::option::Option, + candidate: std::option::Option, + canonical_after: std::option::Option, + comparison: std::option::Option, + resolution_action: std::option::Option, + timestamp: crate::RawTimestamp, + ) -> crate::Result { + if revision == 0 { + return std::result::Result::Err(raw_model_error("revision")); + } + for variant in [canonical_before.as_ref(), candidate.as_ref(), canonical_after.as_ref()].into_iter().flatten() { + if variant.transaction() != reference.transaction() { + return std::result::Result::Err(raw_model_error("variant_reference")); + } + } + let expected_resolution = match kind { + crate::RawTransactionConflictEventKind::ResolvedKeepCanonical => { + std::option::Option::Some(crate::RawTransactionConflictResolutionAction::KeepCurrentCanonical) + }, + crate::RawTransactionConflictEventKind::ResolvedPromoteVariant => { + std::option::Option::Some(crate::RawTransactionConflictResolutionAction::PromoteVariant) + }, + crate::RawTransactionConflictEventKind::ResolvedRestoreVariant => { + std::option::Option::Some(crate::RawTransactionConflictResolutionAction::RestoreVariant) + }, + crate::RawTransactionConflictEventKind::Opened + | crate::RawTransactionConflictEventKind::ParticipantAdded + | crate::RawTransactionConflictEventKind::Reopened => std::option::Option::None, + }; + if resolution_action != expected_resolution { + return std::result::Result::Err(raw_model_error("resolution_action")); + } + if matches!(kind, crate::RawTransactionConflictEventKind::Opened | crate::RawTransactionConflictEventKind::ParticipantAdded) { + let comparison = match comparison { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Err(raw_model_error("comparison")), + }; + if !matches!(comparison.relation(), crate::RawTransactionVariantRelation::Conflict | crate::RawTransactionVariantRelation::Incomparable) { + return std::result::Result::Err(raw_model_error("comparison")); + } + } + return std::result::Result::Ok(Self { + candidate, + canonical_after, + canonical_before, + comparison, + kind, + origin, + reference, + resolution_action, + revision, + timestamp, + }); + } + + /// Returns the optional candidate/participant involved in this event. + #[must_use] + pub fn candidate(&self) -> std::option::Option<&crate::RawTransactionVariantReference> { + return self.candidate.as_ref(); + } + + /// Returns the optional canonical variant after this transition. + #[must_use] + pub fn canonical_after(&self) -> std::option::Option<&crate::RawTransactionVariantReference> { + return self.canonical_after.as_ref(); + } + + /// Returns the optional canonical variant before this transition. + #[must_use] + pub fn canonical_before(&self) -> std::option::Option<&crate::RawTransactionVariantReference> { + return self.canonical_before.as_ref(); + } + + /// Returns optional comparison evidence attached to this history transition. + #[must_use] + pub const fn comparison(&self) -> std::option::Option { + return self.comparison; + } + + /// Returns the append-only history event kind. + #[must_use] + pub const fn kind(&self) -> crate::RawTransactionConflictEventKind { + return self.kind; + } + + /// Returns the safe semantic origin of this transition. + #[must_use] + pub const fn origin(&self) -> crate::RawTransactionConflictEventOrigin { + return self.origin; + } + + /// Returns the owning conflict-case reference. + #[must_use] + pub fn reference(&self) -> &crate::RawTransactionConflictReference { + return &self.reference; + } + + /// Returns the optional durable resolution action. + #[must_use] + pub const fn resolution_action(&self) -> std::option::Option { + return self.resolution_action; + } + + /// Returns the strictly positive conflict revision produced by this event. + #[must_use] + pub const fn revision(&self) -> u64 { + return self.revision; + } + + /// Returns the bounded KSP timestamp of this transition. + #[must_use] + pub const fn timestamp(&self) -> crate::RawTimestamp { + return self.timestamp; + } +} + +/// Backend-independent random-access query for one conflict case append-only history. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionConflictHistoryQuery { + direction: crate::RawSortDirection, + page: crate::RawInspectionPageRequest, + reference: crate::RawTransactionConflictReference, +} + +impl RawTransactionConflictHistoryQuery { + /// Creates one history-inspection query scoped to exactly one conflict case. + #[must_use] + pub fn new(reference: crate::RawTransactionConflictReference, direction: crate::RawSortDirection, page: crate::RawInspectionPageRequest) -> Self { + return Self { direction, page, reference }; + } + + /// Returns the requested deterministic traversal direction. + #[must_use] + pub const fn direction(&self) -> crate::RawSortDirection { + return self.direction; + } + + /// Returns the random-access inspection page request. + #[must_use] + pub const fn page(&self) -> crate::RawInspectionPageRequest { + return self.page; + } + + /// Returns the exact conflict-case scope. + #[must_use] + pub fn reference(&self) -> &crate::RawTransactionConflictReference { + return &self.reference; + } +} + +/// Resolution strategy used when explicitly closing one RAW transaction conflict case. +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RawTransactionConflictResolution { + /// Resolve while retaining the current canonical variant. + KeepCurrentCanonical(crate::RawTransactionVariantReference), + /// Resolve by selecting another durable participant as canonical. + PromoteVariant(crate::RawTransactionVariantReference), + /// Resolve by restoring and selecting a locally preserved participant. + RestoreVariant(crate::RawTransactionVariantReference), +} + +impl RawTransactionConflictResolution { + /// Returns the stable resolution-action classification. + #[must_use] + pub const fn action(&self) -> crate::RawTransactionConflictResolutionAction { + return match self { + Self::KeepCurrentCanonical(_) => crate::RawTransactionConflictResolutionAction::KeepCurrentCanonical, + Self::PromoteVariant(_) => crate::RawTransactionConflictResolutionAction::PromoteVariant, + Self::RestoreVariant(_) => crate::RawTransactionConflictResolutionAction::RestoreVariant, + }; + } + + /// Returns the explicit canonical target selected by this resolution. + #[must_use] + pub fn target_variant(&self) -> &crate::RawTransactionVariantReference { + return match self { + Self::KeepCurrentCanonical(value) | Self::PromoteVariant(value) | Self::RestoreVariant(value) => value, + }; + } +} + +/// Explicit mutable operation over one RAW transaction conflict case. +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RawTransactionConflictAction { + /// Explicitly keep the current canonical selector without resolving the case. + KeepCurrentCanonical, + /// Select another durable participant as the current canonical variant. + PromoteVariant(crate::RawTransactionVariantReference), + /// Restore and select a locally preserved participant as the current canonical variant. + RestoreVariant(crate::RawTransactionVariantReference), + /// Close the case with one explicit canonical resolution. + Resolve(crate::RawTransactionConflictResolution), + /// Reopen one previously resolved conflict case. + Reopen, +} + +/// Compare-and-set request for one mutable RAW transaction conflict action. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionConflictActionRequest { + action: crate::RawTransactionConflictAction, + expected_canonical_revision: u64, + expected_conflict_revision: u64, + reference: crate::RawTransactionConflictReference, +} + +impl RawTransactionConflictActionRequest { + /// Creates one action request with strictly positive expected revisions and scope-safe target variants. + pub fn try_new( + reference: crate::RawTransactionConflictReference, + expected_conflict_revision: u64, + expected_canonical_revision: u64, + action: crate::RawTransactionConflictAction, + ) -> crate::Result { + if expected_conflict_revision == 0 || expected_canonical_revision == 0 { + return std::result::Result::Err(raw_model_error("expected_revision")); + } + let target = match &action { + crate::RawTransactionConflictAction::PromoteVariant(value) | crate::RawTransactionConflictAction::RestoreVariant(value) => { + std::option::Option::Some(value) + }, + crate::RawTransactionConflictAction::Resolve(value) => std::option::Option::Some(value.target_variant()), + crate::RawTransactionConflictAction::KeepCurrentCanonical | crate::RawTransactionConflictAction::Reopen => std::option::Option::None, + }; + if let std::option::Option::Some(target) = target + && target.transaction() != reference.transaction() + { + return std::result::Result::Err(raw_model_error("target_variant")); + } + return std::result::Result::Ok(Self { action, expected_canonical_revision, expected_conflict_revision, reference }); + } + + /// Returns the requested conflict mutation. + #[must_use] + pub fn action(&self) -> &crate::RawTransactionConflictAction { + return &self.action; + } + + /// Returns the expected current canonical-selector revision. + #[must_use] + pub const fn expected_canonical_revision(&self) -> u64 { + return self.expected_canonical_revision; + } + + /// Returns the expected current conflict revision. + #[must_use] + pub const fn expected_conflict_revision(&self) -> u64 { + return self.expected_conflict_revision; + } + + /// Returns the targeted conflict-case reference. + #[must_use] + pub fn reference(&self) -> &crate::RawTransactionConflictReference { + return &self.reference; + } +} + +/// Durable backend-neutral outcome of one conflict-case action request. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionConflictActionOutcome { + /// The requested durable mutation was applied and revisioned. + Applied, + /// The requested target already exactly matches durable state; no new revision was consumed. + AlreadyAtTarget, + /// At least one expected revision no longer matches current durable state. + StaleRevision, + /// The requested operation requires an open case but the current case is not open. + ConflictNotOpen, + /// The requested operation requires a resolved case but the current case is not resolved. + ConflictNotResolved, + /// The requested variant is not a durable participant of this conflict case. + VariantNotParticipant, + /// The requested operation requires local variant payload material that is unavailable. + VariantPayloadUnavailable, +} + +fn raw_model_error(field: &'static str) -> crate::Error { + return crate::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid backend-agnostic RAW Store conflict model").with_context("field", field); +} + +fn raw_query_error(field: &'static str) -> crate::Error { + return crate::Error::new(crate::ERROR_CODE_RAW_QUERY_INVALID, "invalid backend-agnostic RAW Store conflict query").with_context("field", field); +} + +#[cfg(test)] +#[path = "../../unit_tests/model/raw_conflict.rs"] +mod tests; diff --git a/crates/ksp-store-api/tests/dependency_boundary.rs b/crates/ksp-store-api/tests/dependency_boundary.rs index 3d57384..166dde7 100644 --- a/crates/ksp-store-api/tests/dependency_boundary.rs +++ b/crates/ksp-store-api/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/tests/dependency_boundary.rs -// version: 8 +// version: 9 //! Dependency canaries for the Store API RAW foundation. @@ -53,6 +53,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { let crate_root = include_str!("../src/lib.rs"); let model_home = include_str!("../src/model.rs"); let raw_account = include_str!("../src/model/raw_account.rs"); + let raw_conflict = include_str!("../src/model/raw_conflict.rs"); let raw_inspection = include_str!("../src/model/raw_inspection.rs"); let raw_outcome = include_str!("../src/model/raw_outcome.rs"); let raw_pagination = include_str!("../src/model/raw_pagination.rs"); @@ -61,12 +62,14 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { let raw_transaction = include_str!("../src/model/raw_transaction.rs"); let capability_home = include_str!("../src/capability.rs"); let raw_account_capability = include_str!("../src/capability/raw_account.rs"); + let raw_conflict_capability = include_str!("../src/capability/raw_conflict.rs"); let raw_retention_capability = include_str!("../src/capability/raw_retention.rs"); let raw_transaction_capability = include_str!("../src/capability/raw_transaction.rs"); assert!(crate_root.contains("mod capability;")); assert!(crate_root.contains("mod error;")); assert!(crate_root.contains("mod model;")); assert!(model_home.contains("raw_account")); + assert!(model_home.contains("raw_conflict")); assert!(model_home.contains("raw_inspection")); assert!(model_home.contains("raw_outcome")); assert!(model_home.contains("raw_pagination")); @@ -74,12 +77,14 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { assert!(model_home.contains("raw_retention")); assert!(model_home.contains("raw_transaction")); assert!(capability_home.contains("raw_account")); + assert!(capability_home.contains("raw_conflict")); assert!(capability_home.contains("raw_retention")); assert!(capability_home.contains("raw_transaction")); for source in [ crate_root, model_home, raw_account, + raw_conflict, raw_inspection, raw_outcome, raw_pagination, @@ -88,6 +93,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { raw_transaction, capability_home, raw_account_capability, + raw_conflict_capability, raw_retention_capability, raw_transaction_capability, ] { @@ -112,6 +118,11 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { for forbidden in ["TransactionStatusObservation", "RawLogNotification", "RawSlotEvent", "RawVoteEvent", "RawBlock", "YellowstoneEntry"] { assert!(!crate_root.contains(forbidden), "deferred pre.006 model leaked into Store API surface: {forbidden}"); } + assert!(raw_conflict_capability.contains("trait RawTransactionVariantInspectionRead")); + assert!(raw_conflict_capability.contains("trait RawTransactionConflictInspectionRead")); + assert!(raw_conflict_capability.contains("trait RawTransactionConflictHistoryRead")); + assert!(raw_conflict_capability.contains("trait RawTransactionConflictActionWrite")); + assert!(capability_home.contains("trait StoreErrorClassifier")); assert!(raw_transaction_capability.contains("trait RawTransactionRead")); assert!(raw_transaction_capability.contains("trait RawTransactionInspectionRead")); assert!(raw_transaction_capability.contains("trait RawTransactionObservationInspectionRead")); @@ -129,6 +140,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { for forbidden in ["trait StoreBackend", "trait Store", "PostgresStore", "MySqlStore", "Arc( + &'a self, + query: &'a ksp_store_api::RawTransactionVariantInspectionQuery, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let _ = query; + return std::boxed::Box::pin(async { + return ksp_store_api::RawInspectionPage::try_new(std::vec::Vec::new(), 0, 0); + }); + } + + fn get_raw_transaction_variant<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionVariantReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let _ = reference; + return std::boxed::Box::pin(async { + return std::result::Result::Ok(std::option::Option::None); + }); + } +} + +impl ksp_store_api::RawTransactionConflictInspectionRead for ExternalMemoryBackend { + fn inspect_raw_transaction_conflicts<'a>( + &'a self, + query: &'a ksp_store_api::RawTransactionConflictInspectionQuery, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let _ = query; + return std::boxed::Box::pin(async { + return ksp_store_api::RawInspectionPage::try_new(std::vec::Vec::new(), 0, 0); + }); + } + + fn get_raw_transaction_conflict<'a>( + &'a self, + reference: &'a ksp_store_api::RawTransactionConflictReference, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let _ = reference; + return std::boxed::Box::pin(async { + return std::result::Result::Ok(std::option::Option::None); + }); + } +} + +impl ksp_store_api::RawTransactionConflictHistoryRead for ExternalMemoryBackend { + fn inspect_raw_transaction_conflict_history<'a>( + &'a self, + query: &'a ksp_store_api::RawTransactionConflictHistoryQuery, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result>> { + let _ = query; + return std::boxed::Box::pin(async { + return ksp_store_api::RawInspectionPage::try_new(std::vec::Vec::new(), 0, 0); + }); + } +} + +impl ksp_store_api::RawTransactionConflictActionWrite for ExternalMemoryBackend { + fn apply_raw_transaction_conflict_action<'a>( + &'a self, + request: ksp_store_api::RawTransactionConflictActionRequest, + ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result> { + let _ = request; + return std::boxed::Box::pin(async { + return std::result::Result::Ok(ksp_store_api::RawTransactionConflictActionOutcome::Applied); + }); + } +} + +impl ksp_store_api::StoreErrorClassifier for ExternalMemoryBackend { + fn classify_store_error(&self, error: &ksp_store_api::Error) -> ksp_store_api::StoreErrorClass { + let _ = error; + return ksp_store_api::StoreErrorClass::Terminal; + } +} + #[test] fn v0_3_8_pre_003_external_backend_implements_canonical_and_inspection_capabilities_without_runtime_crate() { let backend = ExternalMemoryBackend; @@ -232,6 +307,11 @@ fn v0_3_8_pre_003_external_backend_implements_canonical_and_inspection_capabilit 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 variant_inspection: &dyn ksp_store_api::RawTransactionVariantInspectionRead = &backend; + let conflict_inspection: &dyn ksp_store_api::RawTransactionConflictInspectionRead = &backend; + let conflict_history: &dyn ksp_store_api::RawTransactionConflictHistoryRead = &backend; + let conflict_action: &dyn ksp_store_api::RawTransactionConflictActionWrite = &backend; + let error_classifier: &dyn ksp_store_api::StoreErrorClassifier = &backend; let _ = transaction_read; let _ = transaction_write; let _ = transaction_inspection; @@ -246,5 +326,10 @@ fn v0_3_8_pre_003_external_backend_implements_canonical_and_inspection_capabilit let _ = account_observation_write; let _ = retention_read; let _ = retention_write; + let _ = variant_inspection; + let _ = conflict_inspection; + let _ = conflict_history; + let _ = conflict_action; + let _ = error_classifier; return; } diff --git a/crates/ksp-store-api/tests/public_api.rs b/crates/ksp-store-api/tests/public_api.rs index b9de744..f4b1ed8 100644 --- a/crates/ksp-store-api/tests/public_api.rs +++ b/crates/ksp-store-api/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/tests/public_api.rs -// version: 11 +// version: 12 //! Integration canaries for the public `ksp-store-api` surface. @@ -222,3 +222,20 @@ fn public_v0_3_16_pre_005_variant_comparator_is_available_from_crate_root() { let _ = comparator; return; } + +#[test] +fn public_v0_3_17_pre_002_conflict_lifecycle_contracts_are_root_exported_and_dyn_compatible() { + let variant_inspection: std::option::Option<&dyn ksp_store_api::RawTransactionVariantInspectionRead> = std::option::Option::None; + let conflict_inspection: std::option::Option<&dyn ksp_store_api::RawTransactionConflictInspectionRead> = std::option::Option::None; + let conflict_history: std::option::Option<&dyn ksp_store_api::RawTransactionConflictHistoryRead> = std::option::Option::None; + let conflict_action: std::option::Option<&dyn ksp_store_api::RawTransactionConflictActionWrite> = std::option::Option::None; + let error_classifier: std::option::Option<&dyn ksp_store_api::StoreErrorClassifier> = std::option::Option::None; + assert!(variant_inspection.is_none()); + assert!(conflict_inspection.is_none()); + assert!(conflict_history.is_none()); + assert!(conflict_action.is_none()); + assert!(error_classifier.is_none()); + assert_ne!(ksp_store_api::StoreErrorClass::Transient, ksp_store_api::StoreErrorClass::Terminal); + assert_ne!(ksp_store_api::RawTransactionConflictActionOutcome::AlreadyAtTarget, ksp_store_api::RawTransactionConflictActionOutcome::StaleRevision); + return; +} diff --git a/crates/ksp-store-api/tests/release_completeness.rs b/crates/ksp-store-api/tests/release_completeness.rs index de8370c..550a70c 100644 --- a/crates/ksp-store-api/tests/release_completeness.rs +++ b/crates/ksp-store-api/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/tests/release_completeness.rs -// version: 6 +// version: 7 //! Release-level boundary and completeness canaries for the backend-neutral Store API RAW surface. #[test] @@ -20,19 +20,24 @@ fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() { "pub use ksp_core_lib::Pubkey;", "pub use ksp_core_lib::Result;", "pub use self::capability::StoreApiFuture;", + "pub use self::capability::StoreErrorClassifier;", "pub use self::capability::raw_account::RawAccountObservationInspectionRead;", "pub use self::capability::raw_account::RawAccountObservationRead;", "pub use self::capability::raw_account::RawAccountObservationWrite;", - "pub use self::capability::raw_account::RawAccountStateRead;", "pub use self::capability::raw_account::RawAccountStateInspectionRead;", + "pub use self::capability::raw_account::RawAccountStateRead;", "pub use self::capability::raw_account::RawAccountStateWrite;", + "pub use self::capability::raw_conflict::RawTransactionConflictActionWrite;", + "pub use self::capability::raw_conflict::RawTransactionConflictHistoryRead;", + "pub use self::capability::raw_conflict::RawTransactionConflictInspectionRead;", + "pub use self::capability::raw_conflict::RawTransactionVariantInspectionRead;", "pub use self::capability::raw_retention::RawTransactionRetentionRead;", "pub use self::capability::raw_retention::RawTransactionRetentionWrite;", + "pub use self::capability::raw_transaction::RawTransactionInspectionRead;", "pub use self::capability::raw_transaction::RawTransactionObservationInspectionRead;", "pub use self::capability::raw_transaction::RawTransactionObservationRead;", "pub use self::capability::raw_transaction::RawTransactionObservationWrite;", "pub use self::capability::raw_transaction::RawTransactionRead;", - "pub use self::capability::raw_transaction::RawTransactionInspectionRead;", "pub use self::capability::raw_transaction::RawTransactionWrite;", "pub use self::error::ERROR_CODE_RAW_CONFLICT;", "pub use self::error::ERROR_CODE_RAW_MODEL_INVALID;", @@ -40,9 +45,27 @@ fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() { "pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID;", "pub use self::error::ERROR_CODE_RAW_QUERY_INVALID;", "pub use self::error::ERROR_CODE_RAW_RETENTION_INVALID;", + "pub use self::error::StoreErrorClass;", "pub use self::model::raw_account::RawAccountObservation;", "pub use self::model::raw_account::RawAccountState;", "pub use self::model::raw_account::RawAccountStateReference;", + "pub use self::model::raw_conflict::RawTransactionConflictAction;", + "pub use self::model::raw_conflict::RawTransactionConflictActionOutcome;", + "pub use self::model::raw_conflict::RawTransactionConflictActionRequest;", + "pub use self::model::raw_conflict::RawTransactionConflictDetail;", + "pub use self::model::raw_conflict::RawTransactionConflictEventKind;", + "pub use self::model::raw_conflict::RawTransactionConflictEventOrigin;", + "pub use self::model::raw_conflict::RawTransactionConflictEventSummary;", + "pub use self::model::raw_conflict::RawTransactionConflictHistoryQuery;", + "pub use self::model::raw_conflict::RawTransactionConflictInspectionQuery;", + "pub use self::model::raw_conflict::RawTransactionConflictParticipantSummary;", + "pub use self::model::raw_conflict::RawTransactionConflictReference;", + "pub use self::model::raw_conflict::RawTransactionConflictResolution;", + "pub use self::model::raw_conflict::RawTransactionConflictResolutionAction;", + "pub use self::model::raw_conflict::RawTransactionConflictSummary;", + "pub use self::model::raw_conflict::RawTransactionVariantDetail;", + "pub use self::model::raw_conflict::RawTransactionVariantInspectionQuery;", + "pub use self::model::raw_conflict::RawTransactionVariantSummary;", "pub use self::model::raw_inspection::RawAccountObservationInspectionQuery;", "pub use self::model::raw_inspection::RawAccountObservationSummary;", "pub use self::model::raw_inspection::RawAccountStateInspectionQuery;", @@ -87,7 +110,6 @@ fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() { "pub use self::model::raw_retention::RawTransactionRetentionTransition;", "pub use self::model::raw_retention::RawTransactionTombstone;", "pub use self::model::raw_transaction::RawTransaction;", - "pub use self::model::raw_transaction::compare_raw_transaction_variants;", "pub use self::model::raw_transaction::RawTransactionConflictStatus;", "pub use self::model::raw_transaction::RawTransactionObservation;", "pub use self::model::raw_transaction::RawTransactionReference;", @@ -97,6 +119,7 @@ fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() { "pub use self::model::raw_transaction::RawTransactionVariantReference;", "pub use self::model::raw_transaction::RawTransactionVariantRelation;", "pub use self::model::raw_transaction::RawTransactionVariantRelationReason;", + "pub use self::model::raw_transaction::compare_raw_transaction_variants;", ]; expected.sort_unstable(); assert_eq!(actual, expected); @@ -122,7 +145,16 @@ fn v0_3_8_pre_003_exact_production_module_inventory_is_raw_only() { }; assert_eq!( model_names, - std::vec!["raw_account.rs", "raw_inspection.rs", "raw_outcome.rs", "raw_pagination.rs", "raw_primitives.rs", "raw_retention.rs", "raw_transaction.rs"] + std::vec![ + "raw_account.rs", + "raw_conflict.rs", + "raw_inspection.rs", + "raw_outcome.rs", + "raw_pagination.rs", + "raw_primitives.rs", + "raw_retention.rs", + "raw_transaction.rs" + ] ); let capability_names = rust_file_names(root.join("capability").as_path()); assert!(capability_names.is_ok()); @@ -130,13 +162,15 @@ fn v0_3_8_pre_003_exact_production_module_inventory_is_raw_only() { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; - assert_eq!(capability_names, std::vec!["raw_account.rs", "raw_retention.rs", "raw_transaction.rs"]); + assert_eq!(capability_names, std::vec!["raw_account.rs", "raw_conflict.rs", "raw_retention.rs", "raw_transaction.rs"]); return; } #[test] fn pre_007_public_evolutive_enums_remain_non_exhaustive() { let sources = [ + include_str!("../src/error.rs"), + include_str!("../src/model/raw_conflict.rs"), include_str!("../src/model/raw_outcome.rs"), include_str!("../src/model/raw_pagination.rs"), include_str!("../src/model/raw_primitives.rs"), @@ -145,6 +179,12 @@ fn pre_007_public_evolutive_enums_remain_non_exhaustive() { ]; for enum_name in [ "RawAcquisitionOrigin", + "RawTransactionConflictAction", + "RawTransactionConflictActionOutcome", + "RawTransactionConflictEventKind", + "RawTransactionConflictEventOrigin", + "RawTransactionConflictResolution", + "RawTransactionConflictResolutionAction", "RawEntityWriteOutcome", "RawObservationWriteOutcome", "RawRetentionState", @@ -156,6 +196,7 @@ fn pre_007_public_evolutive_enums_remain_non_exhaustive() { "RawTransactionVariantRelation", "RawTransactionVariantRelationReason", "RawTransactionVariantWriteOutcome", + "StoreErrorClass", ] { assert_non_exhaustive(&sources, enum_name); } @@ -169,6 +210,7 @@ fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() { include_str!("../src/lib.rs"), include_str!("../src/model.rs"), include_str!("../src/model/raw_account.rs"), + include_str!("../src/model/raw_conflict.rs"), include_str!("../src/model/raw_inspection.rs"), include_str!("../src/model/raw_outcome.rs"), include_str!("../src/model/raw_pagination.rs"), @@ -177,6 +219,7 @@ fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() { include_str!("../src/model/raw_transaction.rs"), include_str!("../src/capability.rs"), include_str!("../src/capability/raw_account.rs"), + include_str!("../src/capability/raw_conflict.rs"), include_str!("../src/capability/raw_retention.rs"), include_str!("../src/capability/raw_transaction.rs"), ]; @@ -207,7 +250,9 @@ fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() { #[test] fn v0_3_8_pre_003_capability_inventory_stays_fine_grained_without_runtime_facade() { let sources = [ + include_str!("../src/capability.rs"), include_str!("../src/capability/raw_account.rs"), + include_str!("../src/capability/raw_conflict.rs"), include_str!("../src/capability/raw_retention.rs"), include_str!("../src/capability/raw_transaction.rs"), ]; @@ -228,6 +273,11 @@ fn v0_3_8_pre_003_capability_inventory_stays_fine_grained_without_runtime_facade "pub trait RawAccountStateRead: std::marker::Send + std::marker::Sync {", "pub trait RawAccountStateInspectionRead: std::marker::Send + std::marker::Sync {", "pub trait RawAccountStateWrite: std::marker::Send + std::marker::Sync {", + "pub trait RawTransactionConflictActionWrite: std::marker::Send + std::marker::Sync {", + "pub trait RawTransactionConflictHistoryRead: std::marker::Send + std::marker::Sync {", + "pub trait RawTransactionConflictInspectionRead: std::marker::Send + std::marker::Sync {", + "pub trait RawTransactionVariantInspectionRead: std::marker::Send + std::marker::Sync {", + "pub trait StoreErrorClassifier: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionObservationInspectionRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionObservationRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionObservationWrite: std::marker::Send + std::marker::Sync {", diff --git a/crates/ksp-store-api/tests/security_hardening.rs b/crates/ksp-store-api/tests/security_hardening.rs index 1a631a6..182d1d4 100644 --- a/crates/ksp-store-api/tests/security_hardening.rs +++ b/crates/ksp-store-api/tests/security_hardening.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/tests/security_hardening.rs -// version: 3 +// version: 4 //! Adversarial and retention-race canaries for the Store API RAW foundation. @@ -166,3 +166,62 @@ fn v0_3_8_pre_003_inspection_summaries_and_counts_reject_payload_shaped_or_incon assert!(ksp_store_api::RawInspectionPage::::try_new(std::vec::Vec::new(), 1, 2).is_err()); return; } + +#[test] +fn v0_3_17_pre_002_variant_detail_debug_keeps_complete_payload_material_redacted() { + let network = match ksp_store_api::RawNetworkId::new("mainnet".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let transaction_reference = ksp_store_api::RawTransactionReference::new(network, ksp_store_api::RawTransactionSignature::new([0x71_u8; 64])); + let variant_id = match ksp_store_api::RawTransactionVariantId::try_new(1) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let variant_reference = ksp_store_api::RawTransactionVariantReference::new(transaction_reference.clone(), variant_id); + let format = match ksp_store_api::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let created_at = match ksp_store_api::RawTimestamp::from_unix_millis(1_000) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let payload_bytes = HOSTILE_MARKER.as_bytes().to_vec().into_boxed_slice(); + let payload_size = match u64::try_from(payload_bytes.len()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let summary = ksp_store_api::RawTransactionVariantSummary::try_new( + variant_reference, + ksp_store_api::RawTransactionVariantOrigin::Native, + 77, + std::option::Option::None, + format.clone(), + 1, + ksp_store_api::RawRetentionState::Full, + std::option::Option::Some(payload_size), + true, + 1, + created_at, + ); + let summary = match summary { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let payload = ksp_store_api::RawPayload::try_new(format, 1, payload_bytes, ksp_store_api::RawContentHash::new([0x72_u8; 32])); + let payload = match payload { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let transaction = ksp_store_api::RawTransaction::new(transaction_reference, 77, std::option::Option::None, payload); + let detail = ksp_store_api::RawTransactionVariantDetail::try_new(summary, std::option::Option::Some(transaction)); + let detail = match detail { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let debug = std::format!("{detail:?}"); + assert!(!debug.contains(HOSTILE_MARKER)); + assert!(!debug.contains("payload: [")); + return; +} diff --git a/crates/ksp-store-api/unit_tests/model/raw_conflict.rs b/crates/ksp-store-api/unit_tests/model/raw_conflict.rs new file mode 100644 index 0000000..ea689ae --- /dev/null +++ b/crates/ksp-store-api/unit_tests/model/raw_conflict.rs @@ -0,0 +1,293 @@ +// file: crates/ksp-store-api/unit_tests/model/raw_conflict.rs +// version: 1 + +fn network(value: &str) -> std::option::Option { + return crate::RawNetworkId::new(value.to_owned()).ok(); +} + +fn transaction_reference(network: crate::RawNetworkId, signature_byte: u8) -> crate::RawTransactionReference { + return crate::RawTransactionReference::new(network, crate::RawTransactionSignature::new([signature_byte; 64])); +} + +fn variant_reference(transaction: crate::RawTransactionReference, variant_id: u64) -> std::option::Option { + let variant_id = match crate::RawTransactionVariantId::try_new(variant_id) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + return std::option::Option::Some(crate::RawTransactionVariantReference::new(transaction, variant_id)); +} + +fn timestamp(value: u64) -> std::option::Option { + return crate::RawTimestamp::from_unix_millis(value).ok(); +} + +fn page() -> std::option::Option { + let limit = match crate::RawPageLimit::new(25) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + return std::option::Option::Some(crate::RawInspectionPageRequest::new(0, limit)); +} + +#[test] +fn v0_3_17_pre_002_conflict_and_variant_queries_enforce_network_scope() { + let mainnet = match network("mainnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let devnet = match network("devnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let transaction = transaction_reference(devnet, 1); + let page = match page() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!( + crate::RawTransactionVariantInspectionQuery::try_new( + mainnet.clone(), + std::option::Option::Some(transaction.clone()), + crate::RawSortDirection::Ascending, + page, + ) + .is_err() + ); + assert!( + crate::RawTransactionConflictInspectionQuery::try_new( + mainnet, + std::option::Option::Some(crate::RawTransactionConflictStatus::Open), + std::option::Option::Some(transaction), + crate::RawSortDirection::Ascending, + page, + ) + .is_err() + ); + return; +} + +#[test] +fn v0_3_17_pre_002_action_request_requires_positive_double_revision_and_same_transaction_target() { + let mainnet = match network("mainnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let transaction = transaction_reference(mainnet.clone(), 2); + let conflict = crate::RawTransactionConflictReference::new(transaction.clone()); + let target = match variant_reference(transaction, 2) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!( + crate::RawTransactionConflictActionRequest::try_new(conflict.clone(), 0, 1, crate::RawTransactionConflictAction::PromoteVariant(target.clone()),) + .is_err() + ); + let other = transaction_reference(mainnet, 3); + let other_target = match variant_reference(other, 3) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!( + crate::RawTransactionConflictActionRequest::try_new(conflict.clone(), 1, 1, crate::RawTransactionConflictAction::PromoteVariant(other_target),) + .is_err() + ); + let request = crate::RawTransactionConflictActionRequest::try_new( + conflict, + 5, + 7, + crate::RawTransactionConflictAction::Resolve(crate::RawTransactionConflictResolution::PromoteVariant(target)), + ); + assert!(request.is_ok()); + let request = match request { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(request.expected_conflict_revision(), 5); + assert_eq!(request.expected_canonical_revision(), 7); + return; +} + +#[test] +fn v0_3_17_pre_002_conflict_projection_enforces_status_resolution_and_conflict_relation() { + let mainnet = match network("mainnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let transaction = transaction_reference(mainnet, 4); + let conflict = crate::RawTransactionConflictReference::new(transaction.clone()); + let canonical = match variant_reference(transaction.clone(), 1) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let incoming = match variant_reference(transaction, 2) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let created_at = match timestamp(1_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let updated_at = match timestamp(2_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let open = crate::RawTransactionConflictSummary::try_new( + conflict.clone(), + crate::RawTransactionConflictStatus::Open, + 3, + canonical.clone(), + 2, + incoming.clone(), + crate::RawTransactionVariantRelation::Conflict, + crate::RawTransactionVariantRelationReason::CanonicalPayloadConflict, + std::option::Option::None, + std::option::Option::None, + 2, + created_at, + updated_at, + ); + assert!(open.is_ok()); + let invalid_resolved = crate::RawTransactionConflictSummary::try_new( + conflict, + crate::RawTransactionConflictStatus::Resolved, + 4, + canonical, + 3, + incoming, + crate::RawTransactionVariantRelation::Exact, + crate::RawTransactionVariantRelationReason::ExactCanonicalContent, + std::option::Option::None, + std::option::Option::None, + 2, + created_at, + updated_at, + ); + assert!(invalid_resolved.is_err()); + return; +} + +#[test] +fn v0_3_17_pre_002_conflict_detail_requires_unique_complete_participant_projection() { + let mainnet = match network("mainnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let transaction = transaction_reference(mainnet, 5); + let conflict = crate::RawTransactionConflictReference::new(transaction.clone()); + let canonical = match variant_reference(transaction.clone(), 1) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let incoming = match variant_reference(transaction, 2) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let created_at = match timestamp(1_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let updated_at = match timestamp(2_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let summary = crate::RawTransactionConflictSummary::try_new( + conflict, + crate::RawTransactionConflictStatus::Open, + 2, + canonical.clone(), + 1, + incoming.clone(), + crate::RawTransactionVariantRelation::Incomparable, + crate::RawTransactionVariantRelationReason::UnsupportedCanonicalDifference, + std::option::Option::None, + std::option::Option::None, + 2, + created_at, + updated_at, + ); + let summary = match summary { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let first = match crate::RawTransactionConflictParticipantSummary::try_new(canonical, 1, created_at) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let second = match crate::RawTransactionConflictParticipantSummary::try_new(incoming, 2, updated_at) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert!(crate::RawTransactionConflictDetail::try_new(summary, std::vec![first, second]).is_ok()); + return; +} + +#[test] +fn v0_3_17_pre_002_history_event_requires_resolution_action_only_for_resolution_events() { + let mainnet = match network("mainnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let transaction = transaction_reference(mainnet, 6); + let conflict = crate::RawTransactionConflictReference::new(transaction.clone()); + let canonical = match variant_reference(transaction.clone(), 1) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let incoming = match variant_reference(transaction, 2) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let comparison = crate::RawTransactionVariantComparison::try_new( + crate::RawTransactionVariantRelation::Conflict, + crate::RawTransactionVariantRelationReason::CanonicalPayloadConflict, + ); + let comparison = match comparison { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let at = match timestamp(1_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!( + crate::RawTransactionConflictEventSummary::try_new( + conflict.clone(), + 1, + crate::RawTransactionConflictEventKind::Opened, + crate::RawTransactionConflictEventOrigin::Automatic, + std::option::Option::Some(canonical.clone()), + std::option::Option::Some(incoming), + std::option::Option::Some(canonical.clone()), + std::option::Option::Some(comparison), + std::option::Option::None, + at, + ) + .is_ok() + ); + assert!( + crate::RawTransactionConflictEventSummary::try_new( + conflict, + 2, + crate::RawTransactionConflictEventKind::ResolvedKeepCanonical, + crate::RawTransactionConflictEventOrigin::Operator, + std::option::Option::Some(canonical.clone()), + std::option::Option::None, + std::option::Option::Some(canonical), + std::option::Option::None, + std::option::Option::None, + at, + ) + .is_err() + ); + return; +} + +#[test] +fn v0_3_17_pre_002_action_outcomes_and_codes_are_stable_and_distinct() { + assert_eq!(crate::RawTransactionConflictResolutionAction::KeepCurrentCanonical.code(), "keep_current_canonical"); + assert_eq!(crate::RawTransactionConflictEventKind::ParticipantAdded.code(), "participant_added"); + assert_eq!(crate::RawTransactionConflictEventOrigin::Operator.code(), "operator"); + assert_ne!(crate::RawTransactionConflictActionOutcome::Applied, crate::RawTransactionConflictActionOutcome::AlreadyAtTarget); + assert_ne!(crate::RawTransactionConflictActionOutcome::AlreadyAtTarget, crate::RawTransactionConflictActionOutcome::StaleRevision); + return; +} diff --git a/deltas/0.3.17/pre.002.md b/deltas/0.3.17/pre.002.md new file mode 100644 index 0000000..9fa839c --- /dev/null +++ b/deltas/0.3.17/pre.002.md @@ -0,0 +1,334 @@ + + + +# Delta `0.3.17-pre.002` — contrats Store API conflict lifecycle et classification d'erreur + +## Base requise + +```text +0.3.17-pre.001 appliquée +workspace.package.version = 0.3.17-pre.1 +``` + +Le gate opérateur reçu le 24 septembre 2026 est propre : + +```text +cargo fmt --all -- --check PASS +General Rust rule audit clean +Rust export completeness audit 0 candidate(s) +KSP workspace Rust rule audit clean +Markdown table audit clean (352 table(s), 965 file(s)) +cargo check --workspace PASS, 6.11 s +cargo clippy --workspace --all-targets --all-features -- -D warnings + PASS, 25.28 s +``` + +## Objet + +Figer la surface backend-neutral requise par le cycle de vie complet des variantes/conflits avant toute migration PostgreSQL V004. + +Cette tranche reste strictement dans `ksp-store-api` et ajoute : + +```text +inspection des variantes +inspection des conflict cases +participant ledger DTO +historique append-only DTO/query +actions compare-and-set à double revision +outcomes stale/idempotent explicites +StoreErrorClass Transient/Terminal +StoreErrorClassifier backend-neutral +``` + +Elle ne modifie encore : + +```text +aucune migration SQL +aucun backend PostgreSQL +aucun ksp-store-lib +aucun Worker +aucun Transport/Config +aucun Job Backfill +aucune application Desk +``` + +## Version workspace + +`Cargo.toml` : + +```text +header version : 652 -> 653 +workspace : 0.3.17-pre.1 -> 0.3.17-pre.2 +``` + +## Fichiers ajoutés + +```text +crates/ksp-store-api/src/capability/raw_conflict.rs +crates/ksp-store-api/src/model/raw_conflict.rs +crates/ksp-store-api/unit_tests/model/raw_conflict.rs +deltas/0.3.17/pre.002.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-store-api/src/capability.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 +crates/ksp-store-api/tests/release_completeness.rs +crates/ksp-store-api/tests/security_hardening.rs +docs/validation/034-V0_3_17_RAW_OPERATIONAL_RESILIENCE.md +``` + +## Contrats variantes + +La lecture opérateur des variantes est portée par : + +```text +RawTransactionVariantInspectionQuery +RawTransactionVariantSummary +RawTransactionVariantDetail +RawTransactionVariantInspectionRead +``` + +La query est random-access bornée via `RawInspectionPageRequest`, exige un network et accepte seulement un filtre transaction exact cohérent avec ce network. + +Le summary reste payload-free et expose uniquement les métadonnées nécessaires : + +```text +variant reference +origin +slot / block_time +format id/version +retention state +payload size si disponible +is_current_canonical +observation_count +created_at +``` + +`RawTransactionVariantDetail` peut porter la transaction RAW complète uniquement dans la lecture explicite d'un variant. Le constructeur vérifie transaction identity, slot, block time, format et taille déclarée ; un variant `Purged` ne peut pas prétendre contenir encore le payload. + +## Contrats conflict case + +Identité : + +```text +RawTransactionConflictReference + -> RawTransactionReference +``` + +Inspection : + +```text +RawTransactionConflictInspectionQuery +RawTransactionConflictSummary +RawTransactionConflictParticipantSummary +RawTransactionConflictDetail +RawTransactionConflictInspectionRead +``` + +La projection courante conserve explicitement : + +```text +status Open/Resolved +conflict revision > 0 +canonical variant +canonical selector revision > 0 +latest incoming variant +latest Conflict/Incomparable relation + reason +resolved canonical variant si Resolved +latest resolution action si Resolved +participant_count >= 2 +created_at / updated_at +``` + +Les références canonical/incoming/resolved doivent appartenir à la même transaction que le conflict case. + +Le detail valide que le participant ledger : + +```text +contient exactement participant_count entrées +n'a aucun variant_id dupliqué +reste dans le même scope transaction +n'annonce aucun first_seen_revision > current conflict revision +contient canonical/latest incoming/resolved target requis +``` + +## Historique append-only + +Types ajoutés : + +```text +RawTransactionConflictEventKind +RawTransactionConflictEventOrigin +RawTransactionConflictEventSummary +RawTransactionConflictHistoryQuery +RawTransactionConflictHistoryRead +``` + +Event kinds figés : + +```text +Opened +ParticipantAdded +ResolvedKeepCanonical +ResolvedPromoteVariant +ResolvedRestoreVariant +Reopened +``` + +Origins sûres : + +```text +Automatic +Operator +``` + +Aucun acteur libre, texte backend, SQL ou payload RAW n'entre dans le DTO d'historique. + +Une transition de résolution exige exactement le `RawTransactionConflictResolutionAction` correspondant. `Opened` et `ParticipantAdded` exigent une comparaison durable `Conflict` ou `Incomparable`. + +## Actions compare-and-set + +Types : + +```text +RawTransactionConflictResolutionAction +RawTransactionConflictResolution +RawTransactionConflictAction +RawTransactionConflictActionRequest +RawTransactionConflictActionOutcome +RawTransactionConflictActionWrite +``` + +Actions disponibles : + +```text +KeepCurrentCanonical +PromoteVariant(target) +RestoreVariant(target) +Resolve(explicit canonical resolution) +Reopen +``` + +Chaque request transporte obligatoirement : + +```text +conflict reference +expected_conflict_revision > 0 +expected_canonical_revision > 0 +action +``` + +Tout target variant doit appartenir à la transaction du conflict case. + +Outcomes backend-neutral : + +```text +Applied +AlreadyAtTarget +StaleRevision +ConflictNotOpen +ConflictNotResolved +VariantNotParticipant +VariantPayloadUnavailable +``` + +`AlreadyAtTarget` et `StaleRevision` restent volontairement distincts : une répétition idempotente n'est pas une perte de compare-and-set concurrente. + +## Classification Store error + +`ksp-store-api` expose désormais : + +```text +StoreErrorClass::Transient +StoreErrorClass::Terminal +StoreErrorClassifier +``` + +Le classifier reçoit une `ksp_core_lib::Error` déjà projetée/redacted. Le Worker futur consommera ce contrat au lieu de comparer des codes PostgreSQL ou des chaînes libres. + +Cette tranche ne classe encore aucun SQLSTATE concret : le raffinement physique reste réservé à `pre.006`. La politique fail-closed définie par le plan reste inchangée : tout code non explicitement reconnu par l'implémentation future sera `Terminal`. + +## Frontières préservées + +Le manifest `ksp-store-api` reste inchangé : + +```text +runtime dependency exacte = ksp-core-lib uniquement +``` + +Aucun nouveau crate externe n'est introduit. + +Tous les nouveaux items publics sont réexportés depuis la racine `ksp-store-api`; aucun `pub mod` n'est ajouté. Les enums évolutifs sont `#[non_exhaustive]`. + +Les capabilities restent object-safe et implémentables par un backend externe sans dépendre de `ksp-store-lib` ou PostgreSQL. + +## Tests/canaris ajoutés ou étendus + +La tranche couvre statiquement et par tests Rust : + +```text +network coherence des queries variant/conflict +double revision strictement positive +scope transaction des targets d'action +status Open/Resolved et projection resolution cohérente +relation conflict/incomparable requise pour le conflict current +participant ledger complet, unique et revision-safe +history event/action consistency +outcomes Applied/AlreadyAtTarget/StaleRevision distincts +codes event/origin/resolution stables +variant detail payload debug redacted +object safety des quatre nouvelles capabilities + classifier +inventaire exact crate-root/module/capability +frontière dépendances backend-neutral +#[non_exhaustive] des nouveaux enums +``` + +## Validation locale de l'assemblage + +L'environnement d'assemblage ne fournit toujours pas `cargo`, `rustc` ou `rustfmt`. Aucun PASS Cargo n'est inventé pour `pre.002`. + +Audits réellement exécutés après les changements Rust : + +```text +General Rust rule audit clean +Rust export completeness audit 0 candidate(s) +KSP workspace Rust rule audit clean +Markdown table audit clean (352 table(s), 966 file(s)) +``` + +Le gate Cargo utilisateur reste autoritaire pour confirmer compilation, rustfmt, Clippy et tests. + +## Gate requis après application + +```bash +cargo fmt --all +cargo fmt --all -- --check + +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 + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-store-api --all-targets --all-features +``` + +Ne pas lancer de test PostgreSQL live pour cette tranche : aucune migration ni implémentation backend n'est encore modifiée. + +## Prochaine tranche + +Après gate propre : + +```text +0.3.17-pre.003 +``` + +Objet prévu : migration PostgreSQL V004 additive pour participants, historique append-only, projection de résolution et bootstrap legacy, avec canaris garantissant l'immuabilité byte/checksum de V000/V001/V002/V003. diff --git a/docs/validation/034-V0_3_17_RAW_OPERATIONAL_RESILIENCE.md b/docs/validation/034-V0_3_17_RAW_OPERATIONAL_RESILIENCE.md index d9f94d3..fe76091 100644 --- a/docs/validation/034-V0_3_17_RAW_OPERATIONAL_RESILIENCE.md +++ b/docs/validation/034-V0_3_17_RAW_OPERATIONAL_RESILIENCE.md @@ -1,5 +1,5 @@ - + # Validation `0.3.17` — résilience opérationnelle et cycle de vie des variantes RAW @@ -568,3 +568,63 @@ cargo clippy --workspace --all-targets --all-features -- -D warnings ``` Aucun test live n'est requis par cette tranche documentaire de planning. + +## 23. Gate opérateur `0.3.17-pre.001` reçu avant `pre.002` + +Le 24 septembre 2026, l'opérateur a exécuté sur son checkout après application de `0.3.17-pre.001` : + +```bash +cargo fmt --all -- --check +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 +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +Résultats fournis : + +```text +cargo fmt --all -- --check aucun diagnostic +General Rust rule audit clean +Rust export completeness audit 0 candidate(s) +KSP workspace Rust rule audit clean +Markdown table audit clean (352 table(s), 965 file(s)) +cargo check --workspace PASS, 0.3.17-pre.1, 6.11 s +cargo clippy --workspace --all-targets --all-features -- -D warnings + PASS, 0.3.17-pre.1, 25.28 s +``` + +Ce gate ferme `pre.001` et autorise `0.3.17-pre.002`. Aucun test runtime/live supplémentaire n'était requis par le delta `pre.001`. + +## 24. Assemblage `0.3.17-pre.002` + +La tranche `pre.002` matérialise uniquement les contrats backend-neutral prévus au sizing : + +```text +variant inspection +conflict inspection + participants +append-only history DTO/query +double-revision action request +stale/idempotent outcomes +StoreErrorClass + StoreErrorClassifier +``` + +Aucune migration V004, implémentation PostgreSQL, façade Store, politique Worker retry ou reconnexion Transport n'est introduite. + +Audits locaux après assemblage : + +```text +General Rust rule audit clean +Rust export completeness audit 0 candidate(s) +KSP workspace Rust rule audit clean +Markdown table audit clean (352 table(s), 966 file(s)) +``` + +Limitation inchangée de l'environnement d'assemblage : + +```text +cargo/rustc/rustfmt unavailable +cargo fmt/check/clippy/test pre.002 NOT RUN ici +``` + +Le gate opérateur requis pour fermer `pre.002` est celui de `deltas/0.3.17/pre.002.md`, incluant `cargo test -p ksp-store-api --all-targets --all-features`.