diff --git a/Cargo.toml b/Cargo.toml index 3ae15c1..a4d52d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 638 +# version: 639 [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.16-pre.1" +version = "0.3.16-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-api/src/lib.rs b/crates/ksp-store-api/src/lib.rs index 0eff331..5ba08b8 100644 --- a/crates/ksp-store-api/src/lib.rs +++ b/crates/ksp-store-api/src/lib.rs @@ -1,10 +1,8 @@ // file: crates/ksp-store-api/src/lib.rs -// version: 7 - +// version: 8 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] - //! Backend-agnostic persistence contracts for KSP Store implementations. //! //! `ksp-store-api` owns persistent models plus the contracts that operate on @@ -15,11 +13,9 @@ //! Models and capabilities deliberately have separate private module homes. //! Backend implementations, SQL, migrations, Config, Transport and runtime //! dispatch remain outside this crate. - mod capability; mod error; mod model; - /// Boxed async operation returned by object-safe Store capability contracts. pub use self::capability::StoreApiFuture; /// Read capability for random-access RAW account-observation inspection. @@ -94,6 +90,8 @@ pub use self::model::raw_outcome::RawAcquisitionWriteOutcome; pub use self::model::raw_outcome::RawEntityWriteOutcome; /// Outcome for one deterministic acquisition observation write. pub use self::model::raw_outcome::RawObservationWriteOutcome; +/// Durable Store outcome for one variant-aware RAW transaction acquisition. +pub use self::model::raw_outcome::RawTransactionVariantWriteOutcome; /// 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. @@ -154,10 +152,24 @@ pub use self::model::raw_retention::RawTransactionRetentionTransition; pub use self::model::raw_retention::RawTransactionTombstone; /// Canonical source-independent N1 RAW transaction persisted by Store backends. pub use self::model::raw_transaction::RawTransaction; +/// Durable lifecycle status of one RAW transaction conflict case. +pub use self::model::raw_transaction::RawTransactionConflictStatus; /// Persistable acquisition observation linked to one canonical RAW transaction. pub use self::model::raw_transaction::RawTransactionObservation; /// Durable backend-independent identity of one canonical RAW transaction. pub use self::model::raw_transaction::RawTransactionReference; +/// Validated backend-neutral result of comparing canonical and incoming RAW transaction variants. +pub use self::model::raw_transaction::RawTransactionVariantComparison; +/// Stable non-zero Store identity of one persisted RAW transaction variant. +pub use self::model::raw_transaction::RawTransactionVariantId; +/// Origin of one persisted RAW transaction variant. +pub use self::model::raw_transaction::RawTransactionVariantOrigin; +/// Durable reference to one persisted RAW transaction variant. +pub use self::model::raw_transaction::RawTransactionVariantReference; +/// Backend-neutral quality relation between canonical and incoming RAW transaction variants. +pub use self::model::raw_transaction::RawTransactionVariantRelation; +/// Stable backend-neutral reason explaining one RAW transaction variant relation. +pub use self::model::raw_transaction::RawTransactionVariantRelationReason; /// Common KSP error type used by Store-facing contracts. pub use ksp_core_lib::Error; /// Stable structured code identifying a KSP error category and condition. diff --git a/crates/ksp-store-api/src/model/raw_outcome.rs b/crates/ksp-store-api/src/model/raw_outcome.rs index d22c105..b40ddb4 100644 --- a/crates/ksp-store-api/src/model/raw_outcome.rs +++ b/crates/ksp-store-api/src/model/raw_outcome.rs @@ -1,6 +1,5 @@ // file: crates/ksp-store-api/src/model/raw_outcome.rs -// version: 1 - +// version: 2 /// Outcome for one canonical RAW entity in an idempotent persistence operation. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -14,7 +13,6 @@ pub enum RawEntityWriteOutcome { /// Normal persistence skipped a durable purged tombstone. SkippedPurged, } - /// Outcome for one deterministic acquisition observation write. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -26,14 +24,12 @@ pub enum RawObservationWriteOutcome { /// 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] @@ -46,10 +42,33 @@ impl RawAcquisitionWriteOutcome { 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; } } + +/// Durable Store outcome for one RAW transaction acquisition after variant-aware convergence. +/// +/// Every variant represents a successful Store-domain result. In particular, +/// [`Self::QuarantinedConflict`] is durable success and must not be reinterpreted as a +/// transport failure or terminal persistence error by callers. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionVariantWriteOutcome { + /// First durable variant became the initial canonical transaction. + InsertedCanonical, + /// Incoming content exactly matched an already durable variant/canonical representation. + ObservedExact, + /// Incoming content was durably observed while the current canonical variant remained strictly more complete. + ObservedCompatibleLessComplete, + /// Incoming content was durably persisted and atomically promoted because it was strictly more complete. + PromotedCompatibleMoreComplete, + /// Divergent or incomparable content was durably preserved without changing the current canonical variant. + QuarantinedConflict, + /// Explicit force-rehydration restored a previously purged logical transaction into variant-aware storage. + Rehydrated, + /// Normal acquisition respected an existing purged tombstone and performed no rehydration. + SkippedPurged, +} diff --git a/crates/ksp-store-api/src/model/raw_transaction.rs b/crates/ksp-store-api/src/model/raw_transaction.rs index 9342e9e..56c2122 100644 --- a/crates/ksp-store-api/src/model/raw_transaction.rs +++ b/crates/ksp-store-api/src/model/raw_transaction.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/model/raw_transaction.rs -// version: 1 +// version: 2 /// Durable backend-independent identity of one canonical RAW transaction. #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -7,7 +7,6 @@ pub struct RawTransactionReference { network: crate::RawNetworkId, signature: crate::RawTransactionSignature, } - impl RawTransactionReference { /// Creates one durable transaction identity from network and canonical Solana signature. #[must_use] @@ -20,7 +19,6 @@ impl RawTransactionReference { pub fn network(&self) -> &crate::RawNetworkId { return &self.network; } - /// Returns the canonical transaction signature. #[must_use] pub const fn signature(&self) -> crate::RawTransactionSignature { @@ -28,6 +26,195 @@ impl RawTransactionReference { } } +/// Stable non-zero Store identity of one persisted RAW transaction variant. +/// +/// The identifier is a backend-neutral surrogate. It deliberately does not encode +/// `content_hash`, provider identity, canonicality or any physical database key layout. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct RawTransactionVariantId(u64); +impl RawTransactionVariantId { + /// Creates one non-zero stable variant identifier. + pub fn try_new(value: u64) -> crate::Result { + if value == 0 { + return std::result::Result::Err(raw_variant_model_error("variant_id")); + } + return std::result::Result::Ok(Self(value)); + } + + /// Returns the opaque numeric surrogate unchanged. + #[must_use] + pub const fn get(&self) -> u64 { + return self.0; + } +} + +/// Durable reference to one persisted RAW transaction variant. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct RawTransactionVariantReference { + transaction: crate::RawTransactionReference, + variant_id: crate::RawTransactionVariantId, +} +impl RawTransactionVariantReference { + /// Creates one durable variant reference from logical transaction identity and surrogate id. + #[must_use] + pub fn new(transaction: crate::RawTransactionReference, variant_id: crate::RawTransactionVariantId) -> Self { + return Self { transaction, variant_id }; + } + + /// Returns the logical transaction identity owning this variant. + #[must_use] + pub fn transaction(&self) -> &crate::RawTransactionReference { + return &self.transaction; + } + + /// Returns the stable surrogate variant identifier. + #[must_use] + pub const fn variant_id(&self) -> crate::RawTransactionVariantId { + return self.variant_id; + } +} + +/// Origin of one persisted RAW transaction variant. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionVariantOrigin { + /// Exact canonical RAW representation received from one acquisition source. + Native, + /// Explicitly synthesized canonical RAW representation derived from preserved parent variants. + Synthetic, +} +impl RawTransactionVariantOrigin { + /// Returns the stable persistence code for this origin. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Native => "native", + Self::Synthetic => "synthetic", + }; + } +} + +/// Backend-neutral quality relation between the current canonical RAW transaction and one incoming variant. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionVariantRelation { + /// Canonical transaction content is exactly equal after exact comparison. + Exact, + /// Incoming content is compatible but provably less complete than the current canonical variant. + CompatibleLessComplete, + /// Incoming content is compatible and provably more complete than the current canonical variant. + CompatibleMoreComplete, + /// A canonical transaction invariant is contradicted by the incoming variant. + Conflict, + /// No contradiction must be asserted, but no safe dominance relation is proved. + Incomparable, +} +impl RawTransactionVariantRelation { + /// Returns the stable persistence/diagnostic code for this relation. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Exact => "exact", + Self::CompatibleLessComplete => "compatible_less_complete", + Self::CompatibleMoreComplete => "compatible_more_complete", + Self::Conflict => "conflict", + Self::Incomparable => "incomparable", + }; + } +} + +/// Stable backend-neutral reason explaining one RAW transaction variant relation. +/// +/// These codes describe Store-domain comparison evidence only. They do not encode a +/// provider, route, backend implementation or retry classification. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionVariantRelationReason { + /// Exact canonical transaction content equality was proved. + ExactCanonicalContent, + /// The incoming variant differs only by explicitly proved truncated `logMessages`. + IncomingLogMessagesTruncated, + /// The current canonical variant differs only by explicitly proved truncated `logMessages`. + CanonicalLogMessagesTruncated, + /// The transaction slot contradicts the current canonical transaction. + SlotMismatch, + /// The optional block timestamp contradicts the current canonical transaction. + BlockTimeMismatch, + /// Canonical RAW format identity/version differs and no safe equivalence contract applies. + PayloadFormatMismatch, + /// Canonical RAW payload content contains a proved contradiction. + CanonicalPayloadConflict, + /// Equal content digests were observed for canonical representations that are not byte-identical. + ContentHashCollision, + /// Canonical representations differ but the current comparator cannot prove equality, dominance or contradiction safely. + UnsupportedCanonicalDifference, +} +impl RawTransactionVariantRelationReason { + /// Returns the stable persistence/diagnostic code for this reason. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::ExactCanonicalContent => "exact_canonical_content", + Self::IncomingLogMessagesTruncated => "incoming_log_messages_truncated", + Self::CanonicalLogMessagesTruncated => "canonical_log_messages_truncated", + Self::SlotMismatch => "slot_mismatch", + Self::BlockTimeMismatch => "block_time_mismatch", + Self::PayloadFormatMismatch => "payload_format_mismatch", + Self::CanonicalPayloadConflict => "canonical_payload_conflict", + Self::ContentHashCollision => "content_hash_collision", + Self::UnsupportedCanonicalDifference => "unsupported_canonical_difference", + }; + } +} + +/// Validated backend-neutral result of comparing the current canonical transaction with one incoming variant. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct RawTransactionVariantComparison { + reason: crate::RawTransactionVariantRelationReason, + relation: crate::RawTransactionVariantRelation, +} +impl RawTransactionVariantComparison { + /// Creates one relation/reason pair and rejects semantically inconsistent combinations. + pub fn try_new(relation: crate::RawTransactionVariantRelation, reason: crate::RawTransactionVariantRelationReason) -> crate::Result { + if !valid_variant_relation_reason(relation, reason) { + return std::result::Result::Err(raw_variant_model_error("relation_reason")); + } + return std::result::Result::Ok(Self { reason, relation }); + } + + /// Returns the stable reason code explaining the comparison. + #[must_use] + pub const fn reason(&self) -> crate::RawTransactionVariantRelationReason { + return self.reason; + } + + /// Returns the backend-neutral quality relation. + #[must_use] + pub const fn relation(&self) -> crate::RawTransactionVariantRelation { + return self.relation; + } +} + +/// Durable lifecycle status of one RAW transaction conflict case. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawTransactionConflictStatus { + /// At least one preserved variant still requires explicit or future automatic reconciliation. + Open, + /// The conflict currently has an explicit durable resolution while history remains preserved. + Resolved, +} +impl RawTransactionConflictStatus { + /// Returns the stable persistence code for this status. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Open => "open", + Self::Resolved => "resolved", + }; + } +} + /// Canonical source-independent N1 RAW transaction persisted by Store backends. /// /// The payload must contain the complete KSP canonical transaction representation required @@ -41,14 +228,12 @@ pub struct RawTransaction { reference: crate::RawTransactionReference, slot: u64, } - impl RawTransaction { /// Creates one complete canonical RAW transaction. #[must_use] pub fn new(reference: crate::RawTransactionReference, slot: u64, block_time: std::option::Option, payload: crate::RawPayload) -> Self { return Self { block_time, payload, reference, slot }; } - /// Returns the optional canonical block timestamp. #[must_use] pub const fn block_time(&self) -> std::option::Option { @@ -60,7 +245,6 @@ impl RawTransaction { pub fn payload(&self) -> &crate::RawPayload { return &self.payload; } - /// Returns the durable backend-independent transaction identity. #[must_use] pub fn reference(&self) -> &crate::RawTransactionReference { @@ -73,7 +257,6 @@ impl RawTransaction { return self.slot; } } - /// Persistable acquisition observation linked to one canonical RAW transaction. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RawTransactionObservation { @@ -81,14 +264,12 @@ pub struct RawTransactionObservation { provenance: crate::RawAcquisitionProvenance, transaction: crate::RawTransactionReference, } - impl RawTransactionObservation { /// Creates one successful observation of a complete canonical RAW transaction. #[must_use] pub fn new(observation_key: crate::RawObservationKey, transaction: crate::RawTransactionReference, provenance: crate::RawAcquisitionProvenance) -> Self { return Self { observation_key, provenance, transaction }; } - /// Returns the deterministic producer-owned observation idempotence key. #[must_use] pub const fn observation_key(&self) -> crate::RawObservationKey { @@ -100,7 +281,6 @@ impl RawTransactionObservation { pub fn provenance(&self) -> &crate::RawAcquisitionProvenance { return &self.provenance; } - /// Returns the durable transaction identity observed by this acquisition. #[must_use] pub fn transaction(&self) -> &crate::RawTransactionReference { @@ -108,6 +288,30 @@ impl RawTransactionObservation { } } +fn raw_variant_model_error(field: &'static str) -> crate::Error { + return crate::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid RAW transaction variant model").with_context("field", field); +} + +fn valid_variant_relation_reason(relation: crate::RawTransactionVariantRelation, reason: crate::RawTransactionVariantRelationReason) -> bool { + return matches!( + (relation, reason), + (crate::RawTransactionVariantRelation::Exact, crate::RawTransactionVariantRelationReason::ExactCanonicalContent) + | (crate::RawTransactionVariantRelation::CompatibleLessComplete, crate::RawTransactionVariantRelationReason::IncomingLogMessagesTruncated) + | (crate::RawTransactionVariantRelation::CompatibleMoreComplete, crate::RawTransactionVariantRelationReason::CanonicalLogMessagesTruncated) + | ( + crate::RawTransactionVariantRelation::Conflict, + crate::RawTransactionVariantRelationReason::SlotMismatch + | crate::RawTransactionVariantRelationReason::BlockTimeMismatch + | crate::RawTransactionVariantRelationReason::CanonicalPayloadConflict + | crate::RawTransactionVariantRelationReason::ContentHashCollision + ) + | ( + crate::RawTransactionVariantRelation::Incomparable, + crate::RawTransactionVariantRelationReason::PayloadFormatMismatch | crate::RawTransactionVariantRelationReason::UnsupportedCanonicalDifference + ) + ); +} + #[cfg(test)] #[path = "../../unit_tests/model/raw_transaction.rs"] mod tests; diff --git a/crates/ksp-store-api/tests/release_completeness.rs b/crates/ksp-store-api/tests/release_completeness.rs index ea9fe32..9dbefe8 100644 --- a/crates/ksp-store-api/tests/release_completeness.rs +++ b/crates/ksp-store-api/tests/release_completeness.rs @@ -1,8 +1,7 @@ // file: crates/ksp-store-api/tests/release_completeness.rs -// version: 3 +// version: 4 //! Release-level boundary and completeness canaries for the backend-neutral Store API RAW surface. - #[test] fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() { let crate_root = include_str!("../src/lib.rs"); @@ -57,6 +56,7 @@ fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() { "pub use self::model::raw_outcome::RawAcquisitionWriteOutcome;", "pub use self::model::raw_outcome::RawEntityWriteOutcome;", "pub use self::model::raw_outcome::RawObservationWriteOutcome;", + "pub use self::model::raw_outcome::RawTransactionVariantWriteOutcome;", "pub use self::model::raw_pagination::MAX_RAW_PAGE_CURSOR_BYTES;", "pub use self::model::raw_pagination::RawAccountStateQuery;", "pub use self::model::raw_pagination::RawPage;", @@ -87,15 +87,21 @@ 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::RawTransactionConflictStatus;", "pub use self::model::raw_transaction::RawTransactionObservation;", "pub use self::model::raw_transaction::RawTransactionReference;", + "pub use self::model::raw_transaction::RawTransactionVariantComparison;", + "pub use self::model::raw_transaction::RawTransactionVariantId;", + "pub use self::model::raw_transaction::RawTransactionVariantOrigin;", + "pub use self::model::raw_transaction::RawTransactionVariantReference;", + "pub use self::model::raw_transaction::RawTransactionVariantRelation;", + "pub use self::model::raw_transaction::RawTransactionVariantRelationReason;", ]; expected.sort_unstable(); assert_eq!(actual, expected); assert!(!crate_root.contains("pub mod ")); return; } - #[test] fn v0_3_8_pre_003_exact_production_module_inventory_is_raw_only() { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); @@ -125,7 +131,6 @@ fn v0_3_8_pre_003_exact_production_module_inventory_is_raw_only() { assert_eq!(capability_names, std::vec!["raw_account.rs", "raw_retention.rs", "raw_transaction.rs"]); return; } - #[test] fn pre_007_public_evolutive_enums_remain_non_exhaustive() { let sources = [ @@ -133,6 +138,7 @@ fn pre_007_public_evolutive_enums_remain_non_exhaustive() { include_str!("../src/model/raw_pagination.rs"), include_str!("../src/model/raw_primitives.rs"), include_str!("../src/model/raw_retention.rs"), + include_str!("../src/model/raw_transaction.rs"), ]; for enum_name in [ "RawAcquisitionOrigin", @@ -142,12 +148,16 @@ fn pre_007_public_evolutive_enums_remain_non_exhaustive() { "RawRetentionWriteOutcome", "RawSortDirection", "RawTransactionAcquisitionMode", + "RawTransactionConflictStatus", + "RawTransactionVariantOrigin", + "RawTransactionVariantRelation", + "RawTransactionVariantRelationReason", + "RawTransactionVariantWriteOutcome", ] { assert_non_exhaustive(&sources, enum_name); } return; } - #[test] fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() { let interface_root = include_str!("../../ksp-interface-lib/src/lib.rs"); @@ -189,7 +199,6 @@ fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() { } return; } - #[test] fn v0_3_8_pre_003_capability_inventory_stays_fine_grained_without_runtime_facade() { let sources = [ @@ -227,7 +236,6 @@ fn v0_3_8_pre_003_capability_inventory_stays_fine_grained_without_runtime_facade assert_eq!(traits, expected); return; } - fn assert_non_exhaustive(sources: &[&str], enum_name: &str) { let needle = "#[non_exhaustive]\n"; let declaration = std::format!("pub enum {enum_name}"); @@ -243,7 +251,6 @@ fn assert_non_exhaustive(sources: &[&str], enum_name: &str) { assert!(found, "public evolutive enum not found: {enum_name}"); return; } - fn rust_file_names(directory: &std::path::Path) -> std::io::Result> { let entries = match std::fs::read_dir(directory) { std::result::Result::Ok(value) => value, diff --git a/crates/ksp-store-api/unit_tests/model/raw_transaction.rs b/crates/ksp-store-api/unit_tests/model/raw_transaction.rs index bae4bda..5f0fde9 100644 --- a/crates/ksp-store-api/unit_tests/model/raw_transaction.rs +++ b/crates/ksp-store-api/unit_tests/model/raw_transaction.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/unit_tests/model/raw_transaction.rs -// version: 2 +// version: 3 fn network() -> std::option::Option { return match crate::RawNetworkId::new("mainnet".to_owned()) { @@ -83,3 +83,60 @@ fn transaction_observation_is_separate_from_canonical_raw_payload() { assert_eq!(observation.provenance().provider().as_str(), "publicnode"); return; } + +#[test] +fn transaction_variant_identity_is_non_zero_and_keeps_transaction_scope() { + assert!(crate::RawTransactionVariantId::try_new(0).is_err()); + let variant_id = match crate::RawTransactionVariantId::try_new(7) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let transaction = crate::RawTransactionReference::new(network, crate::RawTransactionSignature::new([13_u8; 64])); + let variant = crate::RawTransactionVariantReference::new(transaction, variant_id); + assert_eq!(variant.variant_id().get(), 7); + assert_eq!(variant.transaction().network().as_str(), "mainnet"); + assert_eq!(crate::RawTransactionVariantOrigin::Native.code(), "native"); + assert_eq!(crate::RawTransactionVariantOrigin::Synthetic.code(), "synthetic"); + return; +} + +#[test] +fn transaction_variant_relation_reason_pairs_are_validated() { + let exact = crate::RawTransactionVariantComparison::try_new( + crate::RawTransactionVariantRelation::Exact, + crate::RawTransactionVariantRelationReason::ExactCanonicalContent, + ); + assert!(exact.is_ok()); + let invalid = crate::RawTransactionVariantComparison::try_new( + crate::RawTransactionVariantRelation::Exact, + crate::RawTransactionVariantRelationReason::IncomingLogMessagesTruncated, + ); + assert!(invalid.is_err()); + let conflict = crate::RawTransactionVariantComparison::try_new( + crate::RawTransactionVariantRelation::Conflict, + crate::RawTransactionVariantRelationReason::ContentHashCollision, + ); + assert!(conflict.is_ok()); + let incomparable = crate::RawTransactionVariantComparison::try_new( + crate::RawTransactionVariantRelation::Incomparable, + crate::RawTransactionVariantRelationReason::PayloadFormatMismatch, + ); + assert!(incomparable.is_ok()); + return; +} + +#[test] +fn transaction_variant_codes_and_outcomes_are_stable_backend_neutral_contracts() { + assert_eq!(crate::RawTransactionVariantRelation::CompatibleLessComplete.code(), "compatible_less_complete"); + assert_eq!(crate::RawTransactionVariantRelationReason::IncomingLogMessagesTruncated.code(), "incoming_log_messages_truncated"); + assert_eq!(crate::RawTransactionVariantRelationReason::ContentHashCollision.code(), "content_hash_collision"); + assert_eq!(crate::RawTransactionConflictStatus::Open.code(), "open"); + assert_eq!(crate::RawTransactionConflictStatus::Resolved.code(), "resolved"); + assert_ne!(crate::RawTransactionVariantWriteOutcome::InsertedCanonical, crate::RawTransactionVariantWriteOutcome::ObservedExact); + assert_ne!(crate::RawTransactionVariantWriteOutcome::QuarantinedConflict, crate::RawTransactionVariantWriteOutcome::PromotedCompatibleMoreComplete); + return; +} diff --git a/deltas/0.3.16/pre.002.md b/deltas/0.3.16/pre.002.md new file mode 100644 index 0000000..e130df1 --- /dev/null +++ b/deltas/0.3.16/pre.002.md @@ -0,0 +1,262 @@ + + + +# Delta `0.3.16-pre.002` — contrats Store API RAW multi-variantes + +## Base requise + +```text +0.3.16-pre.001 appliquée +0.3.16-pre.001-fix.001 appliqué +workspace.package.version = 0.3.16-pre.1 +``` + +Le gate Markdown du fix précédent a été fourni propre le 20 septembre 2026 : + +```text +Markdown table audit: clean (352 table(s), 942 file(s)) +``` + +## Objet + +Figer les contrats backend-neutral indispensables à la persistance RAW transaction multi-variantes avant d'introduire la migration PostgreSQL V003. + +Cette tranche ajoute uniquement des modèles/types Store API. Elle ne modifie encore : + +- aucune migration SQL ; +- aucun backend PostgreSQL ; +- aucun `RawTransactionWrite` existant ; +- aucun Worker ; +- aucun comparateur de contenu `logMessages` ; +- aucune UI Store Desk. + +## Version workspace + +`Cargo.toml` : + +```text +header version : 638 -> 639 +workspace : 0.3.16-pre.1 -> 0.3.16-pre.2 +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-store-api/src/lib.rs +crates/ksp-store-api/src/model/raw_outcome.rs +crates/ksp-store-api/src/model/raw_transaction.rs +crates/ksp-store-api/tests/release_completeness.rs +crates/ksp-store-api/unit_tests/model/raw_transaction.rs +docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md +``` + +## Fichier ajouté + +```text +deltas/0.3.16/pre.002.md +``` + +## Contrats ajoutés + +### Identité de variante + +```text +RawTransactionVariantId +RawTransactionVariantReference +``` + +`RawTransactionVariantId` est un surrogate backend-neutral strictement non nul. Il ne dérive ni de `content_hash`, ni du provider, ni d'une clé physique SQL. + +`RawTransactionVariantReference` conserve explicitement : + +```text +RawTransactionReference ++ +RawTransactionVariantId +``` + +Le hash reste donc un attribut de contenu et jamais l'identité métier de la variante. + +### Origine de variante + +```text +RawTransactionVariantOrigin::Native +RawTransactionVariantOrigin::Synthetic +``` + +Les codes persistables stables sont respectivement : + +```text +native +synthetic +``` + +Une variante synthétique est seulement un contrat réservé ; aucune fusion synthétique n'est implémentée dans cette tranche. + +### Relation de qualité + +```text +RawTransactionVariantRelation::Exact +RawTransactionVariantRelation::CompatibleLessComplete +RawTransactionVariantRelation::CompatibleMoreComplete +RawTransactionVariantRelation::Conflict +RawTransactionVariantRelation::Incomparable +``` + +La direction du contrat est toujours : + +```text +current canonical +vs +incoming variant +``` + +Aucune heuristique provider-majority ou source authority n'est introduite. + +### Reason codes + +`RawTransactionVariantRelationReason` fige les raisons backend-neutral suivantes : + +```text +ExactCanonicalContent +IncomingLogMessagesTruncated +CanonicalLogMessagesTruncated +SlotMismatch +BlockTimeMismatch +PayloadFormatMismatch +CanonicalPayloadConflict +ContentHashCollision +UnsupportedCanonicalDifference +``` + +Chaque raison possède un `code()` stable destiné à la persistance et aux diagnostics sûrs. + +Les reason codes ne transportent ni URL, ni provider concret, ni secret, ni payload RAW. + +### Couple relation/reason validé + +```text +RawTransactionVariantComparison +``` + +`try_new(...)` refuse les couples incohérents. + +Contrats actuellement admis : + +```text +Exact <-> ExactCanonicalContent +CompatibleLessComplete <-> IncomingLogMessagesTruncated +CompatibleMoreComplete <-> CanonicalLogMessagesTruncated +Conflict <-> SlotMismatch | BlockTimeMismatch | CanonicalPayloadConflict | ContentHashCollision +Incomparable <-> PayloadFormatMismatch | UnsupportedCanonicalDifference +``` + +Cette validation ne constitue pas encore le comparateur de contenu. Le comparateur pur est prévu en `0.3.16-pre.005`. + +### Outcome de persistance variant-aware + +```text +RawTransactionVariantWriteOutcome::InsertedCanonical +RawTransactionVariantWriteOutcome::ObservedExact +RawTransactionVariantWriteOutcome::ObservedCompatibleLessComplete +RawTransactionVariantWriteOutcome::PromotedCompatibleMoreComplete +RawTransactionVariantWriteOutcome::QuarantinedConflict +RawTransactionVariantWriteOutcome::Rehydrated +RawTransactionVariantWriteOutcome::SkippedPurged +``` + +`QuarantinedConflict` est explicitement un succès durable Store : une divergence conservée ne doit pas être transformée en faute Transport ou en erreur terminale Worker. + +Aucune capability existante ne retourne encore ce nouvel outcome dans `pre.002`; son adoption backend intervient après V003. + +### Statut minimal du conflict case + +```text +RawTransactionConflictStatus::Open +RawTransactionConflictStatus::Resolved +``` + +Le détail des actions opérateur, revisions attendues, reopen et historique append-only reste réservé au programme `0.3.17`. + +## Compatibilité + +Les contrats historiques restent intacts : + +```text +RawTransaction +RawTransactionObservation +RawTransactionReference +RawAcquisitionWriteOutcome +RawEntityWriteOutcome +RawObservationWriteOutcome +RawTransactionWrite +RawTransactionRead +``` + +`pre.002` n'impose donc aucune migration immédiate aux consumers existants. + +Tous les nouveaux enums évolutifs sont `#[non_exhaustive]` et les exports restent disponibles uniquement depuis la racine `ksp-store-api` conformément aux règles KSP. + +## Tests/canaris ajoutés ou étendus + +Les tests couvrent notamment : + +```text +variant_id = 0 rejeté +variant_id non nul conservé sans encoder content_hash +référence variante conserve le scope transaction +codes Native/Synthetic stables +couples relation/reason valides acceptés +couples incohérents rejetés +codes relation/reason/status stables +outcomes variant-aware distincts +inventaire exact des exports crate-root mis à jour +nouveaux enums evolutifs maintenus #[non_exhaustive] +absence de nouveau module production dans ksp-store-api +``` + +## Validation de l'assemblage + +L'environnement d'assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Aucun gate Rust n'est donc déclaré PASS localement. + +Ont été vérifiés localement : + +```text +inventaire exact du delta +workspace.package.version = 0.3.16-pre.2 +headers file/version des fichiers modifiés +newline finale +lignes Rust <= 160 caractères +absence de lockfile/cache/secret +cohérence statique exports <-> release_completeness +cohérence du plan 038 avec les noms Rust désormais figés +``` + +## Gate requis après application + +Comme cette tranche modifie du Rust et le manifest workspace, exécuter : + +```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 exécuter `npm run build` pour ce gate ; aucune application Desk n'est modifiée. + +## Prochaine tranche + +Après gate propre : + +```text +0.3.16-pre.003 +``` + +Objet prévu : migration PostgreSQL V003, registre/resources et schéma minimal variant ledger + selector + conflit, avec canaris garantissant l'immuabilité byte/checksum des migrations V000/V001/V002. diff --git a/docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md b/docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md index 0bd9bdf..39815ef 100644 --- a/docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md +++ b/docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.16` -> `0.3.18` — résilience RAW, variantes, conflits et récupération @@ -648,7 +648,7 @@ Décision d'ownership : - `ksp-raw-transaction-lib` reste l'autorité sur la canonicalisation RAW v1 et fournit les golden/canaris utiles, sans devenir propriétaire de la politique Store de convergence ; - le Worker peut utiliser la relation à titre de métrique/optimisation, mais le Store verrouillé reste l'autorité de la décision durable. -La tranche API devra encore vérifier le graphe Cargo avant de fixer le fichier/module Rust exact, mais la frontière de crate est fermée : le comparateur backend-neutral appartient à `ksp-store-api`. Aucune nouvelle dépendance circulaire n'est admise. +`0.3.16-pre.002` fixe les contrats de variante, relation, reason code et outcome dans les homes privés existants `model/raw_transaction.rs` et `model/raw_outcome.rs`, réexportés depuis la racine de `ksp-store-api`. Le comparateur pur lui-même reste réservé à `0.3.16-pre.005` et devra utiliser ces contrats sans introduire de nouvelle dépendance circulaire. ## 12. Rétention, archive et rehydration @@ -709,7 +709,7 @@ Le statut est volontairement petit. Le détail de la décision vit dans le journ ## 14. Outcomes Store cibles -Les noms Rust exacts seront figés dans `pre.002`, mais les catégories suivantes sont requises : +`0.3.16-pre.002` fige l'enum public `RawTransactionVariantWriteOutcome` avec les variantes suivantes : ```text InsertedCanonical @@ -955,7 +955,7 @@ Gate d'audit, architecture, sizing, plan et validation. Cette tranche est déjà #### `0.3.16-pre.002` -Contrats Store API backend-neutral : variant identity, relation de qualité, outcomes de persistance, reason codes et contrats minimaux nécessaires au conflit durable. +Contrats Store API backend-neutral : variant identity, relation de qualité, outcomes de persistance, reason codes et contrats minimaux nécessaires au conflit durable. Implémentation livrée par cette tranche ; gate opérateur requis avant `pre.003`. #### `0.3.16-pre.003`