From 9300dccbe2f8a3b12f9fc028c542e3cf8be7dd31 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Thu, 3 Sep 2026 11:39:09 +0200 Subject: [PATCH] v0.3.8-pre.003 --- Cargo.toml | 4 +- crates/ksp-app-store-desk/frontend/ts/main.ts | 38 +- crates/ksp-app-store-desk/src/bootstrap.rs | 10 +- crates/ksp-app-store-desk/src/constants.rs | 4 +- crates/ksp-app-store-desk/src/lib.rs | 6 +- .../tests/desktop_security.rs | 18 +- .../src/capability/raw_account.rs | 16 +- .../src/capability/raw_transaction.rs | 16 +- crates/ksp-store-api/src/lib.rs | 18 +- crates/ksp-store-api/src/model.rs | 3 +- .../ksp-store-api/src/model/raw_inspection.rs | 366 ++++++++++++++++++ .../tests/dependency_boundary.rs | 10 +- .../ksp-store-api/tests/external_backend.rs | 32 +- crates/ksp-store-api/tests/public_api.rs | 20 +- .../tests/release_completeness.rs | 23 +- .../ksp-store-api/tests/security_hardening.rs | 46 ++- .../unit_tests/model/raw_inspection.rs | 156 ++++++++ crates/ksp-store-lib/src/lib.rs | 23 +- crates/ksp-store-lib/tests/public_api.rs | 7 +- deltas/0.3.8/pre.003.md | 273 +++++++++++++ docs/plans/029-V0_3_8_STORE_DESK_PLAN.md | 20 +- docs/validation/025-V0_3_8_STORE_DESK.md | 40 +- 22 files changed, 1109 insertions(+), 40 deletions(-) create mode 100644 crates/ksp-store-api/src/model/raw_inspection.rs create mode 100644 crates/ksp-store-api/unit_tests/model/raw_inspection.rs create mode 100644 deltas/0.3.8/pre.003.md diff --git a/Cargo.toml b/Cargo.toml index 3881e6c..cc3d97f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 447 +# version: 448 [workspace] resolver = "3" members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-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-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.8-pre.2.fix.1" +version = "0.3.8-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-app-store-desk/frontend/ts/main.ts b/crates/ksp-app-store-desk/frontend/ts/main.ts index d5dd6ce..4092eb3 100644 --- a/crates/ksp-app-store-desk/frontend/ts/main.ts +++ b/crates/ksp-app-store-desk/frontend/ts/main.ts @@ -1,5 +1,5 @@ // file: crates/ksp-app-store-desk/frontend/ts/main.ts -// version: 1 +// version: 2 import DataTable from "datatables.net-bs5"; import "datatables.net-bs5/css/dataTables.bootstrap5.css"; @@ -130,13 +130,41 @@ function installInteractions(): void { frontendDebug("main", "Store Desk diagnostics refresh button clicked"); void refreshDiagnostics(); }); - document.querySelectorAll("button").forEach(button => { - if (button.dataset.view || button.id === "refreshDiagnostics") { + document.addEventListener("click", event => { + const eventTarget = event.target; + if (!(eventTarget instanceof Element)) { return; } - button.addEventListener("click", () => frontendDebug("main", "Store Desk generic button clicked", { buttonId: button.id || "anonymous" })); + const control = eventTarget.closest("button, [role='tab'], [data-bs-toggle='tab']"); + if (!control) { + return; + } + if (control instanceof HTMLButtonElement && (control.dataset.view || control.id === "refreshDiagnostics")) { + return; + } + const controlId = control.id || control.getAttribute("data-bs-target") || control.getAttribute("aria-controls") || "anonymous"; + if (control.matches("[role='tab'], [data-bs-toggle='tab']")) { + frontendDebug("main", "Store Desk tab control clicked", { controlId }); + return; + } + frontendDebug("main", "Store Desk generic button clicked", { buttonId: controlId }); }); - frontendTrace("main", "Store Desk frontend interactions installed"); + document.addEventListener("shown.bs.tab", event => { + const target = event.target; + const tabId = target instanceof HTMLElement ? target.id || target.getAttribute("data-bs-target") || target.textContent?.trim() || "anonymous" : "unknown"; + frontendDebug("main", "Store Desk tab activated", { tabId }); + }); + document.addEventListener("change", event => { + const target = event.target; + if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement)) { + return; + } + frontendDebug("main", "Store Desk interactive control changed", { + controlId: target.id || target.getAttribute("name") || "anonymous", + controlType: target instanceof HTMLSelectElement ? "select" : target.type || "text", + }); + }); + frontendTrace("main", "Store Desk frontend interactions installed", { buttons: true, controls: true, tabs: true }); } async function initializeMain(): Promise { diff --git a/crates/ksp-app-store-desk/src/bootstrap.rs b/crates/ksp-app-store-desk/src/bootstrap.rs index b4ab53d..88159d2 100644 --- a/crates/ksp-app-store-desk/src/bootstrap.rs +++ b/crates/ksp-app-store-desk/src/bootstrap.rs @@ -1,7 +1,7 @@ // file: crates/ksp-app-store-desk/src/bootstrap.rs -// version: 1 +// version: 2 -//! Transient standard Config and Logging bootstrap for Store Desk scaffold. +//! Standard Config and verbose development Logging bootstrap for Store Desk. /// Crate-internal Logging startup state shared by the application state. pub(crate) struct LoggingStartup { @@ -31,7 +31,7 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine)); } -/// Initializes Logging from the standard Config document, with a trace-level in-memory fallback. +/// Initializes development Logging from the standard `supertrace` profile, with a trace-level in-memory fallback. pub(crate) fn initialize_logging( management: &ksp_config_lib::ConfigManagement, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, @@ -41,7 +41,7 @@ pub(crate) fn initialize_logging( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity), }; - let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment); + let resolved = management.engine().load_resolved_logging_config(std::option::Option::Some(crate::DEVELOPMENT_LOGGING_PROFILE), &environment); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity), @@ -55,7 +55,7 @@ pub(crate) fn initialize_logging( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, active_profile = active_profile_id.as_str(), - "initialized Store Desk scaffold logging from standard Config" + "initialized Store Desk development logging from explicit supertrace Config profile" ); std::result::Result::Ok(crate::LoggingStartup { guard, diff --git a/crates/ksp-app-store-desk/src/constants.rs b/crates/ksp-app-store-desk/src/constants.rs index ad7fccc..3bed6a3 100644 --- a/crates/ksp-app-store-desk/src/constants.rs +++ b/crates/ksp-app-store-desk/src/constants.rs @@ -1,8 +1,10 @@ // file: crates/ksp-app-store-desk/src/constants.rs -// version: 1 +// version: 2 //! Application-owned tracing targets and domains. +/// Logging profile used while Store Desk is under active development. +pub(crate) const DEVELOPMENT_LOGGING_PROFILE: &str = "supertrace"; /// Structured domain used while bootstrapping Config and Logging. pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "store.bootstrap"; /// Structured domain used by technical frontend events. diff --git a/crates/ksp-app-store-desk/src/lib.rs b/crates/ksp-app-store-desk/src/lib.rs index 3c32ebc..b6b7f26 100644 --- a/crates/ksp-app-store-desk/src/lib.rs +++ b/crates/ksp-app-store-desk/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-store-desk/src/lib.rs -// version: 1 +// version: 2 //! Tauri desktop scaffold for backend-neutral KSP Store inspection. @@ -28,8 +28,10 @@ pub(crate) use self::app_state::AppState; pub(crate) use self::bootstrap::LoggingStartup; /// Builds the Config management facade from the common KSP CLI bootstrap contract. pub(crate) use self::bootstrap::config_management; -/// Initializes Logging from the standard Config document for the scaffold. +/// Initializes verbose development Logging from the standard Config document. pub(crate) use self::bootstrap::initialize_logging; +/// Logging profile used while Store Desk is under active development. +pub(crate) use self::constants::DEVELOPMENT_LOGGING_PROFILE; /// Structured domain used while bootstrapping Config and Logging. pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP; /// Structured domain used by technical frontend events. diff --git a/crates/ksp-app-store-desk/tests/desktop_security.rs b/crates/ksp-app-store-desk/tests/desktop_security.rs index 2851306..84e9a69 100644 --- a/crates/ksp-app-store-desk/tests/desktop_security.rs +++ b/crates/ksp-app-store-desk/tests/desktop_security.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-store-desk/tests/desktop_security.rs -// version: 2 +// version: 3 //! Security boundary tests for the Store Desk scaffold. @@ -41,13 +41,27 @@ fn pre_002_frontend_avoids_network_browser_storage_and_blocking_dialogs() { } #[test] -fn pre_002_frontend_tracing_covers_navigation_buttons_refresh_and_ipc() { +fn pre_003_frontend_tracing_covers_navigation_buttons_tabs_controls_refresh_and_ipc() { let main = include_str!("../frontend/ts/main.ts"); let invoke = include_str!("../frontend/ts/invoke.ts"); assert!(main.contains("navigation button clicked")); assert!(main.contains("diagnostics refresh button clicked")); + assert!(main.contains("generic button clicked")); + assert!(main.contains("tab control clicked")); + assert!(main.contains("tab activated")); + assert!(main.contains("interactive control changed")); assert!(main.contains("view activated")); assert!(invoke.contains("IPC command requested")); assert!(invoke.contains("IPC command completed")); assert!(invoke.contains("IPC command failed")); } + +#[test] +fn pre_003_store_desk_development_logging_explicitly_selects_supertrace() { + let constants = include_str!("../src/constants.rs"); + let bootstrap = include_str!("../src/bootstrap.rs"); + assert!(constants.contains("DEVELOPMENT_LOGGING_PROFILE: &str = \"supertrace\"")); + assert!(bootstrap.contains("std::option::Option::Some(crate::DEVELOPMENT_LOGGING_PROFILE)")); + assert!(bootstrap.contains("LogFilterLevel::Trace")); + return; +} diff --git a/crates/ksp-store-api/src/capability/raw_account.rs b/crates/ksp-store-api/src/capability/raw_account.rs index da2f7d0..e4815bc 100644 --- a/crates/ksp-store-api/src/capability/raw_account.rs +++ b/crates/ksp-store-api/src/capability/raw_account.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/capability/raw_account.rs -// version: 2 +// version: 3 /// Read capability for complete canonical RAW account states. /// @@ -24,6 +24,20 @@ pub trait RawAccountStateRead: std::marker::Send + std::marker::Sync { ) -> crate::StoreApiFuture<'a, crate::Result>>; } +/// Read capability for data-free random-access RAW account-state inspection. +/// +/// This capability is intended for bounded operator/admin inspection. It must +/// return exact logical counts and summaries only; complete account bytes stay +/// behind [`RawAccountStateRead::get_raw_account_state`]. Worker, replay and +/// backfill traversal continue to use the opaque-cursor list operation. +pub trait RawAccountStateInspectionRead: std::marker::Send + std::marker::Sync { + /// Inspects one random-access account-state window with exact logical counts. + fn inspect_raw_account_states<'a>( + &'a self, + query: &'a crate::RawAccountStateInspectionQuery, + ) -> crate::StoreApiFuture<'a, crate::Result>>; +} + /// Write capability for complete RAW account-state acquisitions. /// /// The account state and its acquisition observation form one logical diff --git a/crates/ksp-store-api/src/capability/raw_transaction.rs b/crates/ksp-store-api/src/capability/raw_transaction.rs index 66d3ced..9e9d629 100644 --- a/crates/ksp-store-api/src/capability/raw_transaction.rs +++ b/crates/ksp-store-api/src/capability/raw_transaction.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/capability/raw_transaction.rs -// version: 2 +// version: 3 /// Read capability for canonical RAW transactions. /// @@ -24,6 +24,20 @@ pub trait RawTransactionRead: std::marker::Send + std::marker::Sync { ) -> crate::StoreApiFuture<'a, crate::Result>>; } +/// Read capability for payload-free random-access RAW transaction inspection. +/// +/// This capability is intended for bounded operator/admin inspection. It must +/// return exact logical counts and summaries only; canonical payload bytes stay +/// behind [`RawTransactionRead::get_raw_transaction`]. Worker, replay and +/// backfill traversal continue to use the opaque-cursor list operation. +pub trait RawTransactionInspectionRead: std::marker::Send + std::marker::Sync { + /// Inspects one random-access transaction window with exact logical counts. + fn inspect_raw_transactions<'a>( + &'a self, + query: &'a crate::RawTransactionInspectionQuery, + ) -> crate::StoreApiFuture<'a, crate::Result>>; +} + /// Write capability for canonical RAW transaction acquisitions. /// /// The transaction and its acquisition observation form one logical persistence diff --git a/crates/ksp-store-api/src/lib.rs b/crates/ksp-store-api/src/lib.rs index e19d407..78543ab 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: 5 +// version: 6 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -26,6 +26,8 @@ pub use self::capability::StoreApiFuture; pub use self::capability::raw_account::RawAccountObservationRead; /// Write capability for additional observations of already persisted RAW account states. pub use self::capability::raw_account::RawAccountObservationWrite; +/// Read capability for data-free random-access RAW account-state inspection. +pub use self::capability::raw_account::RawAccountStateInspectionRead; /// Read capability for complete canonical RAW account states. pub use self::capability::raw_account::RawAccountStateRead; /// Write capability for complete canonical RAW account-state acquisitions. @@ -34,6 +36,8 @@ pub use self::capability::raw_account::RawAccountStateWrite; pub use self::capability::raw_retention::RawTransactionRetentionRead; /// Write capability for policy-authorized RAW transaction retention transitions. pub use self::capability::raw_retention::RawTransactionRetentionWrite; +/// Read capability for payload-free random-access RAW transaction inspection. +pub use self::capability::raw_transaction::RawTransactionInspectionRead; /// Read capability for persisted RAW transaction observations. pub use self::capability::raw_transaction::RawTransactionObservationRead; /// Write capability for additional observations of already persisted RAW transactions. @@ -60,6 +64,18 @@ pub use self::model::raw_account::RawAccountObservation; pub use self::model::raw_account::RawAccountState; /// Durable backend-independent identity of one canonical RAW account state. pub use self::model::raw_account::RawAccountStateReference; +/// Backend-independent random-access inspection query for RAW account states. +pub use self::model::raw_inspection::RawAccountStateInspectionQuery; +/// Data-free summary of one canonical RAW account state for operator inspection. +pub use self::model::raw_inspection::RawAccountStateSummary; +/// One bounded random-access inspection page with exact logical counts. +pub use self::model::raw_inspection::RawInspectionPage; +/// Random-access page request dedicated to bounded interactive RAW inspection. +pub use self::model::raw_inspection::RawInspectionPageRequest; +/// Backend-independent random-access inspection query for RAW transactions. +pub use self::model::raw_inspection::RawTransactionInspectionQuery; +/// Payload-free summary of one canonical RAW transaction for operator inspection. +pub use self::model::raw_inspection::RawTransactionSummary; /// Combined outcome of one atomic canonical RAW entity plus observation acquisition. pub use self::model::raw_outcome::RawAcquisitionWriteOutcome; /// Outcome for one canonical RAW entity in an idempotent persistence operation. diff --git a/crates/ksp-store-api/src/model.rs b/crates/ksp-store-api/src/model.rs index e95d4b8..c1c4035 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: 4 +// version: 5 //! Private home for persistent Store models. //! @@ -9,6 +9,7 @@ //! one or more models. pub(crate) mod raw_account; +pub(crate) mod raw_inspection; pub(crate) mod raw_outcome; pub(crate) mod raw_pagination; pub(crate) mod raw_primitives; diff --git a/crates/ksp-store-api/src/model/raw_inspection.rs b/crates/ksp-store-api/src/model/raw_inspection.rs new file mode 100644 index 0000000..857a192 --- /dev/null +++ b/crates/ksp-store-api/src/model/raw_inspection.rs @@ -0,0 +1,366 @@ +// file: crates/ksp-store-api/src/model/raw_inspection.rs +// version: 1 + +/// Random-access page request dedicated to bounded interactive RAW inspection. +/// +/// This request is intentionally distinct from [`crate::RawPageRequest`]. It +/// provides offset/limit semantics for operator-facing inspection while the +/// canonical RAW list APIs keep their opaque backend cursors for replay, +/// backfill and worker traversal. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct RawInspectionPageRequest { + limit: crate::RawPageLimit, + offset: u64, +} + +impl RawInspectionPageRequest { + /// Creates one inspection request from an absolute zero-based offset and positive page limit. + #[must_use] + pub const fn new(offset: u64, limit: crate::RawPageLimit) -> Self { + return Self { limit, offset }; + } + + /// Returns the exact caller-requested page size. + #[must_use] + pub const fn limit(&self) -> crate::RawPageLimit { + return self.limit; + } + + /// Returns the absolute zero-based item offset within the filtered result set. + #[must_use] + pub const fn offset(&self) -> u64 { + return self.offset; + } +} + +/// One bounded random-access inspection page with exact logical counts. +/// +/// `total_items` is the exact count inside the mandatory query scope before +/// optional filters. `filtered_items` is the exact count after optional query +/// filters. The item vector contains only the requested page window. +#[derive(Debug)] +pub struct RawInspectionPage { + filtered_items: u64, + items: std::vec::Vec, + total_items: u64, +} + +impl RawInspectionPage { + /// Creates one inspection page after validating count consistency. + pub fn try_new(items: std::vec::Vec, total_items: u64, filtered_items: u64) -> crate::Result { + if filtered_items > total_items { + return std::result::Result::Err(raw_model_error("filtered_items")); + } + let item_count = u64::try_from(items.len()); + let item_count = match item_count { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("items")), + }; + if item_count > filtered_items { + return std::result::Result::Err(raw_model_error("items")); + } + return std::result::Result::Ok(Self { filtered_items, items, total_items }); + } + + /// Returns the exact count after optional query filters. + #[must_use] + pub const fn filtered_items(&self) -> u64 { + return self.filtered_items; + } + + /// Returns the current inspection page items. + #[must_use] + pub fn items(&self) -> &[T] { + return self.items.as_slice(); + } + + /// Consumes the page and returns its bounded inspection items. + #[must_use] + pub fn into_items(self) -> std::vec::Vec { + return self.items; + } + + /// Returns the exact count before optional query filters inside the mandatory scope. + #[must_use] + pub const fn total_items(&self) -> u64 { + return self.total_items; + } +} + +/// Backend-independent random-access inspection query for RAW transactions. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionInspectionQuery { + direction: crate::RawSortDirection, + network: crate::RawNetworkId, + page: crate::RawInspectionPageRequest, + slots: crate::RawSlotRange, +} + +impl RawTransactionInspectionQuery { + /// Creates one transaction inspection query. + #[must_use] + pub fn new(network: crate::RawNetworkId, slots: crate::RawSlotRange, direction: crate::RawSortDirection, page: crate::RawInspectionPageRequest) -> Self { + return Self { direction, network, page, slots }; + } + + /// Returns the requested deterministic traversal direction. + #[must_use] + pub const fn direction(&self) -> crate::RawSortDirection { + return self.direction; + } + + /// Returns the 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 optional inclusive slot filters. + #[must_use] + pub const fn slots(&self) -> crate::RawSlotRange { + return self.slots; + } +} + +/// Backend-independent random-access inspection query for RAW account states. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawAccountStateInspectionQuery { + direction: crate::RawSortDirection, + network: crate::RawNetworkId, + page: crate::RawInspectionPageRequest, + pubkey: std::option::Option, + slots: crate::RawSlotRange, +} + +impl RawAccountStateInspectionQuery { + /// Creates one account-state inspection query. + #[must_use] + pub fn new( + network: crate::RawNetworkId, + pubkey: std::option::Option, + slots: crate::RawSlotRange, + direction: crate::RawSortDirection, + page: crate::RawInspectionPageRequest, + ) -> Self { + return Self { direction, network, page, pubkey, slots }; + } + + /// Returns the requested deterministic traversal direction. + #[must_use] + pub const fn direction(&self) -> crate::RawSortDirection { + return self.direction; + } + + /// Returns the 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 an optional exact account-address filter. + #[must_use] + pub fn pubkey(&self) -> std::option::Option<&crate::Pubkey> { + return self.pubkey.as_ref(); + } + + /// Returns optional inclusive slot filters. + #[must_use] + pub const fn slots(&self) -> crate::RawSlotRange { + return self.slots; + } +} + +/// Payload-free summary of one canonical RAW transaction for operator inspection. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionSummary { + block_time: std::option::Option, + content_hash: crate::RawContentHash, + format_id: crate::RawFormatId, + format_version: u32, + payload_size_bytes: std::option::Option, + reference: crate::RawTransactionReference, + retention_state: crate::RawRetentionState, + slot: u64, +} + +impl RawTransactionSummary { + /// Creates one payload-free transaction summary after validating summary invariants. + pub fn try_new( + reference: crate::RawTransactionReference, + slot: u64, + block_time: std::option::Option, + format_id: crate::RawFormatId, + format_version: u32, + content_hash: crate::RawContentHash, + payload_size_bytes: std::option::Option, + retention_state: crate::RawRetentionState, + ) -> 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 = usize::try_from(size); + let size = match 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 { + if payload_size_bytes.is_some() { + return std::result::Result::Err(raw_model_error("payload_size_bytes")); + } + } else if payload_size_bytes.is_none() { + return std::result::Result::Err(raw_model_error("payload_size_bytes")); + } + return std::result::Result::Ok(Self { + block_time, + content_hash, + format_id, + format_version, + payload_size_bytes, + reference, + retention_state, + slot, + }); + } + + /// Returns the optional canonical block timestamp. + #[must_use] + pub const fn block_time(&self) -> std::option::Option { + return self.block_time; + } + + /// Returns the deterministic canonical content digest. + #[must_use] + pub const fn content_hash(&self) -> crate::RawContentHash { + return self.content_hash; + } + + /// Returns the KSP-owned canonical RAW format identifier. + #[must_use] + pub fn format_id(&self) -> &crate::RawFormatId { + return &self.format_id; + } + + /// Returns the KSP-owned canonical RAW format version. + #[must_use] + pub const fn format_version(&self) -> u32 { + return self.format_version; + } + + /// Returns the canonical payload size when payload content is logically retained. + #[must_use] + pub const fn payload_size_bytes(&self) -> std::option::Option { + return self.payload_size_bytes; + } + + /// Returns the durable backend-independent transaction identity. + #[must_use] + pub fn reference(&self) -> &crate::RawTransactionReference { + return &self.reference; + } + + /// Returns the logical RAW transaction retention state. + #[must_use] + pub const fn retention_state(&self) -> crate::RawRetentionState { + return self.retention_state; + } + + /// Returns the Solana slot containing the transaction. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } +} + +/// Data-free summary of one canonical RAW account state for operator inspection. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawAccountStateSummary { + data_length_bytes: u64, + executable: bool, + lamports: u64, + owner: crate::Pubkey, + reference: crate::RawAccountStateReference, + rent_epoch: u64, +} + +impl RawAccountStateSummary { + /// Creates one data-free account-state summary after validating its bounded data length. + pub fn try_new( + reference: crate::RawAccountStateReference, + lamports: u64, + owner: crate::Pubkey, + executable: bool, + rent_epoch: u64, + data_length_bytes: u64, + ) -> crate::Result { + let data_length = usize::try_from(data_length_bytes); + let data_length = match data_length { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(raw_model_error("data_length_bytes")), + }; + if data_length > crate::MAX_RAW_ACCOUNT_DATA_BYTES { + return std::result::Result::Err(raw_model_error("data_length_bytes")); + } + return std::result::Result::Ok(Self { data_length_bytes, executable, lamports, owner, reference, rent_epoch }); + } + + /// Returns the complete canonical account-data length without exposing account bytes. + #[must_use] + pub const fn data_length_bytes(&self) -> u64 { + return self.data_length_bytes; + } + + /// Returns whether the account is executable. + #[must_use] + pub const fn executable(&self) -> bool { + return self.executable; + } + + /// Returns the account lamport balance. + #[must_use] + pub const fn lamports(&self) -> u64 { + return self.lamports; + } + + /// Returns the account owner program public key. + #[must_use] + pub const fn owner(&self) -> &crate::Pubkey { + return &self.owner; + } + + /// Returns the durable backend-independent account-state identity. + #[must_use] + pub fn reference(&self) -> &crate::RawAccountStateReference { + return &self.reference; + } + + /// Returns the rent epoch reported for this account state. + #[must_use] + pub const fn rent_epoch(&self) -> u64 { + return self.rent_epoch; + } +} + +fn raw_model_error(field: &'static str) -> crate::Error { + return crate::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid backend-agnostic RAW inspection model").with_context("field", field); +} + +#[cfg(test)] +#[path = "../../unit_tests/model/raw_inspection.rs"] +mod tests; diff --git a/crates/ksp-store-api/tests/dependency_boundary.rs b/crates/ksp-store-api/tests/dependency_boundary.rs index 5d75f6f..fd3ca6f 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: 5 +// version: 6 //! 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_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"); let raw_primitives = include_str!("../src/model/raw_primitives.rs"); @@ -66,6 +67,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { assert!(crate_root.contains("mod error;")); assert!(crate_root.contains("mod model;")); assert!(model_home.contains("raw_account")); + assert!(model_home.contains("raw_inspection")); assert!(model_home.contains("raw_outcome")); assert!(model_home.contains("raw_pagination")); assert!(model_home.contains("raw_primitives")); @@ -78,6 +80,7 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { crate_root, model_home, raw_account, + raw_inspection, raw_outcome, raw_pagination, raw_primitives, @@ -109,12 +112,14 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { assert!(!crate_root.contains(forbidden), "deferred pre.006 model leaked into Store API surface: {forbidden}"); } assert!(raw_transaction_capability.contains("trait RawTransactionRead")); + assert!(raw_transaction_capability.contains("trait RawTransactionInspectionRead")); assert!(raw_transaction_capability.contains("trait RawTransactionWrite")); assert!(raw_transaction_capability.contains("trait RawTransactionObservationRead")); assert!(raw_transaction_capability.contains("trait RawTransactionObservationWrite")); assert!(raw_retention_capability.contains("trait RawTransactionRetentionRead")); assert!(raw_retention_capability.contains("trait RawTransactionRetentionWrite")); assert!(raw_account_capability.contains("trait RawAccountStateRead")); + assert!(raw_account_capability.contains("trait RawAccountStateInspectionRead")); assert!(raw_account_capability.contains("trait RawAccountStateWrite")); assert!(raw_account_capability.contains("trait RawAccountObservationRead")); assert!(raw_account_capability.contains("trait RawAccountObservationWrite")); @@ -126,6 +131,9 @@ fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() { } assert!(!raw_pagination.contains("u64::MAX")); assert!(!raw_pagination.contains("MAX_RAW_PAGE_ITEMS")); + for forbidden in ["DataTable", "recordsTotal", "recordsFiltered", "draw", "SQL", "OFFSET ", "LIMIT "] { + assert!(!raw_inspection.contains(forbidden), "UI/physical inspection concept leaked into Store API: {forbidden}"); + } return; } diff --git a/crates/ksp-store-api/tests/external_backend.rs b/crates/ksp-store-api/tests/external_backend.rs index 2ecfd0d..adc38c8 100644 --- a/crates/ksp-store-api/tests/external_backend.rs +++ b/crates/ksp-store-api/tests/external_backend.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/tests/external_backend.rs -// version: 2 +// version: 3 //! External-implementation canary for object-safe Store API capabilities. @@ -27,6 +27,18 @@ impl ksp_store_api::RawTransactionRead for ExternalMemoryBackend { } } +impl ksp_store_api::RawTransactionInspectionRead for ExternalMemoryBackend { + fn inspect_raw_transactions<'a>( + &'a self, + query: &'a ksp_store_api::RawTransactionInspectionQuery, + ) -> 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::RawTransactionWrite for ExternalMemoryBackend { fn persist_raw_transaction_acquisition<'a>( &'a self, @@ -92,6 +104,18 @@ impl ksp_store_api::RawAccountStateRead for ExternalMemoryBackend { } } +impl ksp_store_api::RawAccountStateInspectionRead for ExternalMemoryBackend { + fn inspect_raw_account_states<'a>( + &'a self, + query: &'a ksp_store_api::RawAccountStateInspectionQuery, + ) -> 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::RawAccountStateWrite for ExternalMemoryBackend { fn persist_raw_account_acquisition<'a>( &'a self, @@ -168,24 +192,28 @@ impl ksp_store_api::RawTransactionRetentionWrite for ExternalMemoryBackend { } #[test] -fn pre_006_external_backend_implements_each_capability_without_store_runtime_crate() { +fn v0_3_8_pre_003_external_backend_implements_canonical_and_inspection_capabilities_without_runtime_crate() { let backend = ExternalMemoryBackend; let transaction_read: &dyn ksp_store_api::RawTransactionRead = &backend; let transaction_write: &dyn ksp_store_api::RawTransactionWrite = &backend; + let transaction_inspection: &dyn ksp_store_api::RawTransactionInspectionRead = &backend; let transaction_observation_read: &dyn ksp_store_api::RawTransactionObservationRead = &backend; let transaction_observation_write: &dyn ksp_store_api::RawTransactionObservationWrite = &backend; let account_read: &dyn ksp_store_api::RawAccountStateRead = &backend; let account_write: &dyn ksp_store_api::RawAccountStateWrite = &backend; + let account_inspection: &dyn ksp_store_api::RawAccountStateInspectionRead = &backend; let account_observation_read: &dyn ksp_store_api::RawAccountObservationRead = &backend; let account_observation_write: &dyn ksp_store_api::RawAccountObservationWrite = &backend; let retention_read: &dyn ksp_store_api::RawTransactionRetentionRead = &backend; let retention_write: &dyn ksp_store_api::RawTransactionRetentionWrite = &backend; let _ = transaction_read; let _ = transaction_write; + let _ = transaction_inspection; let _ = transaction_observation_read; let _ = transaction_observation_write; let _ = account_read; let _ = account_write; + let _ = account_inspection; let _ = account_observation_read; let _ = account_observation_write; let _ = retention_read; diff --git a/crates/ksp-store-api/tests/public_api.rs b/crates/ksp-store-api/tests/public_api.rs index 1cdd718..2b9effc 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: 6 +// version: 7 //! Integration canaries for the public `ksp-store-api` surface. @@ -186,3 +186,21 @@ fn public_pre_007_retention_race_outcome_is_available_from_crate_root() { assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch); return; } + +#[test] +fn public_v0_3_8_pre_003_inspection_contracts_are_backend_neutral_and_dyn_compatible() { + let limit = match ksp_store_api::RawPageLimit::new(25) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let request = ksp_store_api::RawInspectionPageRequest::new(50, limit); + assert_eq!(request.offset(), 50); + assert_eq!(request.limit().get(), 25); + let page = ksp_store_api::RawInspectionPage::::try_new(std::vec![1, 2], 10, 5); + assert!(page.is_ok()); + let transaction_inspection: std::option::Option<&dyn ksp_store_api::RawTransactionInspectionRead> = std::option::Option::None; + let account_inspection: std::option::Option<&dyn ksp_store_api::RawAccountStateInspectionRead> = std::option::Option::None; + assert!(transaction_inspection.is_none()); + assert!(account_inspection.is_none()); + return; +} diff --git a/crates/ksp-store-api/tests/release_completeness.rs b/crates/ksp-store-api/tests/release_completeness.rs index 9e1ee4d..d409ee2 100644 --- a/crates/ksp-store-api/tests/release_completeness.rs +++ b/crates/ksp-store-api/tests/release_completeness.rs @@ -1,10 +1,10 @@ // file: crates/ksp-store-api/tests/release_completeness.rs -// version: 1 +// version: 2 -//! Release-level boundary and completeness canaries for the `0.3.1` Store API RAW surface. +//! Release-level boundary and completeness canaries for the backend-neutral Store API RAW surface. #[test] -fn pre_007_exact_crate_root_export_inventory_is_stable() { +fn v0_3_8_pre_003_exact_crate_root_export_inventory_is_stable() { let crate_root = include_str!("../src/lib.rs"); let mut actual = std::vec::Vec::new(); for line in crate_root.lines() { @@ -24,12 +24,14 @@ fn pre_007_exact_crate_root_export_inventory_is_stable() { "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::RawAccountStateWrite;", "pub use self::capability::raw_retention::RawTransactionRetentionRead;", "pub use self::capability::raw_retention::RawTransactionRetentionWrite;", "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,6 +42,12 @@ fn pre_007_exact_crate_root_export_inventory_is_stable() { "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_inspection::RawAccountStateInspectionQuery;", + "pub use self::model::raw_inspection::RawAccountStateSummary;", + "pub use self::model::raw_inspection::RawInspectionPage;", + "pub use self::model::raw_inspection::RawInspectionPageRequest;", + "pub use self::model::raw_inspection::RawTransactionInspectionQuery;", + "pub use self::model::raw_inspection::RawTransactionSummary;", "pub use self::model::raw_outcome::RawAcquisitionWriteOutcome;", "pub use self::model::raw_outcome::RawEntityWriteOutcome;", "pub use self::model::raw_outcome::RawObservationWriteOutcome;", @@ -83,7 +91,7 @@ fn pre_007_exact_crate_root_export_inventory_is_stable() { } #[test] -fn pre_007_exact_production_module_inventory_is_raw_only() { +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"); let root_names = rust_file_names(root.as_path()); assert!(root_names.is_ok()); @@ -100,7 +108,7 @@ fn pre_007_exact_production_module_inventory_is_raw_only() { }; assert_eq!( model_names, - std::vec!["raw_account.rs", "raw_outcome.rs", "raw_pagination.rs", "raw_primitives.rs", "raw_retention.rs", "raw_transaction.rs"] + std::vec!["raw_account.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()); @@ -141,6 +149,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_inspection.rs"), include_str!("../src/model/raw_outcome.rs"), include_str!("../src/model/raw_pagination.rs"), include_str!("../src/model/raw_primitives.rs"), @@ -176,7 +185,7 @@ fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() { } #[test] -fn pre_007_capability_inventory_stays_fine_grained_without_runtime_facade() { +fn v0_3_8_pre_003_capability_inventory_stays_fine_grained_without_runtime_facade() { let sources = [ include_str!("../src/capability/raw_account.rs"), include_str!("../src/capability/raw_retention.rs"), @@ -196,10 +205,12 @@ fn pre_007_capability_inventory_stays_fine_grained_without_runtime_facade() { "pub trait RawAccountObservationRead: std::marker::Send + std::marker::Sync {", "pub trait RawAccountObservationWrite: std::marker::Send + std::marker::Sync {", "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 RawTransactionObservationRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionObservationWrite: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionRead: std::marker::Send + std::marker::Sync {", + "pub trait RawTransactionInspectionRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionRetentionRead: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionRetentionWrite: std::marker::Send + std::marker::Sync {", "pub trait RawTransactionWrite: 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 a030b93..ee6047d 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: 1 +// version: 2 //! Adversarial and retention-race canaries for the Store API RAW foundation. @@ -122,3 +122,47 @@ fn pre_007_retention_outcome_distinguishes_lost_compare_and_transition_race() { assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch); return; } + +#[test] +fn v0_3_8_pre_003_inspection_summaries_and_counts_reject_payload_shaped_or_inconsistent_state() { + let network = match ksp_store_api::RawNetworkId::new("mainnet-beta".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + 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 transaction_reference = ksp_store_api::RawTransactionReference::new(network.clone(), ksp_store_api::RawTransactionSignature::new([0x61_u8; 64])); + let summary = ksp_store_api::RawTransactionSummary::try_new( + transaction_reference, + 42, + std::option::Option::None, + format, + 1, + ksp_store_api::RawContentHash::new([0x62_u8; 32]), + std::option::Option::Some(512), + ksp_store_api::RawRetentionState::Full, + ); + assert!(summary.is_ok()); + let summary = match summary { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let debug = std::format!("{summary:?}"); + assert!(!debug.contains(HOSTILE_MARKER)); + assert!(!debug.contains("payload:")); + let account_reference = ksp_store_api::RawAccountStateReference::new( + network, + ksp_store_api::Pubkey::new_from_array([0x63_u8; 32]), + 43, + ksp_store_api::RawContentHash::new([0x64_u8; 32]), + ); + assert!( + ksp_store_api::RawAccountStateSummary::try_new(account_reference, 1, ksp_store_api::Pubkey::new_from_array([0x65_u8; 32]), false, 0, 16_777_217,) + .is_err() + ); + assert!(ksp_store_api::RawInspectionPage::::try_new(std::vec![1], 0, 0).is_err()); + assert!(ksp_store_api::RawInspectionPage::::try_new(std::vec::Vec::new(), 1, 2).is_err()); + return; +} diff --git a/crates/ksp-store-api/unit_tests/model/raw_inspection.rs b/crates/ksp-store-api/unit_tests/model/raw_inspection.rs new file mode 100644 index 0000000..f080222 --- /dev/null +++ b/crates/ksp-store-api/unit_tests/model/raw_inspection.rs @@ -0,0 +1,156 @@ +// file: crates/ksp-store-api/unit_tests/model/raw_inspection.rs +// version: 1 + +//! Unit tests for backend-neutral RAW inspection contracts. + +fn network() -> std::option::Option { + return match crate::RawNetworkId::new("mainnet-beta".to_owned()) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; +} + +fn format_id() -> std::option::Option { + return match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; +} + +#[test] +fn inspection_page_request_preserves_large_offset_and_caller_limit_without_cursor_semantics() { + let limit = match crate::RawPageLimit::new(100) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let request = crate::RawInspectionPageRequest::new(u64::MAX, limit); + assert_eq!(request.offset(), u64::MAX); + assert_eq!(request.limit().get(), 100); + return; +} + +#[test] +fn inspection_page_rejects_inconsistent_exact_counts() { + assert!(crate::RawInspectionPage::::try_new(std::vec![1], 0, 0).is_err()); + assert!(crate::RawInspectionPage::::try_new(std::vec::Vec::new(), 4, 5).is_err()); + let page = crate::RawInspectionPage::::try_new(std::vec![1, 2], 5, 3); + assert!(page.is_ok()); + let page = match page { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(page.total_items(), 5); + assert_eq!(page.filtered_items(), 3); + assert_eq!(page.items(), &[1, 2]); + return; +} + +#[test] +fn inspection_queries_keep_backend_neutral_filters_and_random_access_page() { + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let slots = match crate::RawSlotRange::new(std::option::Option::Some(10), std::option::Option::Some(20)) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let limit = match crate::RawPageLimit::new(25) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let transaction = + crate::RawTransactionInspectionQuery::new(network.clone(), slots, crate::RawSortDirection::Descending, crate::RawInspectionPageRequest::new(50, limit)); + assert_eq!(transaction.network().as_str(), "mainnet-beta"); + assert_eq!(transaction.page().offset(), 50); + assert_eq!(transaction.slots().end_inclusive(), std::option::Option::Some(20)); + assert_eq!(transaction.direction(), crate::RawSortDirection::Descending); + let pubkey = crate::Pubkey::new_from_array([0x21_u8; 32]); + let account = crate::RawAccountStateInspectionQuery::new( + network, + std::option::Option::Some(pubkey), + slots, + crate::RawSortDirection::Ascending, + crate::RawInspectionPageRequest::new(75, limit), + ); + assert_eq!(account.page().offset(), 75); + assert_eq!(account.pubkey(), std::option::Option::Some(&pubkey)); + return; +} + +#[test] +fn transaction_summary_is_payload_free_and_retention_consistent() { + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let format_id = match format_id() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reference = crate::RawTransactionReference::new(network, crate::RawTransactionSignature::new([0x31_u8; 64])); + let full = crate::RawTransactionSummary::try_new( + reference.clone(), + 42, + std::option::Option::None, + format_id.clone(), + 1, + crate::RawContentHash::new([0x32_u8; 32]), + std::option::Option::Some(123), + crate::RawRetentionState::Full, + ); + assert!(full.is_ok()); + let full = match full { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(full.slot(), 42); + assert_eq!(full.payload_size_bytes(), std::option::Option::Some(123)); + assert_eq!(full.retention_state(), crate::RawRetentionState::Full); + assert!( + crate::RawTransactionSummary::try_new( + reference.clone(), + 42, + std::option::Option::None, + format_id.clone(), + 1, + crate::RawContentHash::new([0x32_u8; 32]), + std::option::Option::None, + crate::RawRetentionState::Full, + ) + .is_err() + ); + assert!( + crate::RawTransactionSummary::try_new( + reference, + 42, + std::option::Option::None, + format_id, + 1, + crate::RawContentHash::new([0x32_u8; 32]), + std::option::Option::Some(123), + crate::RawRetentionState::Purged, + ) + .is_err() + ); + return; +} + +#[test] +fn account_summary_exposes_length_only_and_enforces_raw_account_bound() { + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reference = crate::RawAccountStateReference::new(network, crate::Pubkey::new_from_array([0x41_u8; 32]), 88, crate::RawContentHash::new([0x42_u8; 32])); + let summary = crate::RawAccountStateSummary::try_new(reference.clone(), 1000, crate::Pubkey::new_from_array([0x43_u8; 32]), false, 9, 512); + assert!(summary.is_ok()); + let summary = match summary { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(summary.reference().slot(), 88); + assert_eq!(summary.data_length_bytes(), 512); + assert!(crate::RawAccountStateSummary::try_new(reference, 1000, crate::Pubkey::new_from_array([0x43_u8; 32]), false, 9, 16_777_217,).is_err()); + return; +} diff --git a/crates/ksp-store-lib/src/lib.rs b/crates/ksp-store-lib/src/lib.rs index 7984c96..485335f 100644 --- a/crates/ksp-store-lib/src/lib.rs +++ b/crates/ksp-store-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/lib.rs -// version: 9 +// version: 10 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -10,8 +10,9 @@ //! The runtime facade owns backend selection, lifecycle, safe diagnostics and //! backend-neutral capability dispatch. `0.3.3-pre.008` completed the six PostgreSQL //! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities -//! on both the physical backend and this common facade, completing the RAW inventory at -//! ten capabilities without exposing physical types. +//! on both the physical backend and this common facade, completing the canonical RAW runtime inventory at +//! ten capabilities without exposing physical types. The `0.3.8` inspection +//! contracts are reexported here before backend/facade dispatch is opened. //! //! The default `postgres` feature compiles the official PostgreSQL backend as //! an optional implementation dependency. No backend implementation type is @@ -125,12 +126,18 @@ pub use ksp_store_api::RawAccountObservationRead; pub use ksp_store_api::RawAccountObservationWrite; /// Canonical complete N1 RAW account state independent from acquisition transport. pub use ksp_store_api::RawAccountState; +/// Backend-independent random-access inspection query for RAW account states. +pub use ksp_store_api::RawAccountStateInspectionQuery; +/// Read capability for data-free random-access RAW account-state inspection. +pub use ksp_store_api::RawAccountStateInspectionRead; /// Backend-independent list query for complete canonical RAW account states. pub use ksp_store_api::RawAccountStateQuery; /// Read capability for complete canonical RAW account states. pub use ksp_store_api::RawAccountStateRead; /// Durable backend-independent identity of one canonical RAW account state. pub use ksp_store_api::RawAccountStateReference; +/// Data-free summary of one canonical RAW account state for operator inspection. +pub use ksp_store_api::RawAccountStateSummary; /// Write capability for complete canonical RAW account-state acquisitions. pub use ksp_store_api::RawAccountStateWrite; /// Origin category describing why one acquisition was performed. @@ -145,6 +152,10 @@ pub use ksp_store_api::RawContentHash; pub use ksp_store_api::RawEntityWriteOutcome; /// Bounded identifier of one KSP-owned source-independent RAW persistence format. pub use ksp_store_api::RawFormatId; +/// One bounded random-access inspection page with exact logical counts. +pub use ksp_store_api::RawInspectionPage; +/// Random-access page request dedicated to bounded interactive RAW inspection. +pub use ksp_store_api::RawInspectionPageRequest; /// Bounded logical network/cluster identifier used in backend-independent Store identities. pub use ksp_store_api::RawNetworkId; /// Stable deterministic idempotence key for one persisted acquisition observation. @@ -177,6 +188,10 @@ pub use ksp_store_api::RawTimestamp; pub use ksp_store_api::RawTransaction; /// Explicit write mode for canonical RAW transaction acquisitions. pub use ksp_store_api::RawTransactionAcquisitionMode; +/// Backend-independent random-access inspection query for RAW transactions. +pub use ksp_store_api::RawTransactionInspectionQuery; +/// Read capability for payload-free random-access RAW transaction inspection. +pub use ksp_store_api::RawTransactionInspectionRead; /// Persistable acquisition observation linked to one canonical RAW transaction. pub use ksp_store_api::RawTransactionObservation; /// Read capability for persisted RAW transaction observations. @@ -197,6 +212,8 @@ pub use ksp_store_api::RawTransactionRetentionTransition; pub use ksp_store_api::RawTransactionRetentionWrite; /// Canonical 64-byte Solana transaction signature used by Store identities. pub use ksp_store_api::RawTransactionSignature; +/// Payload-free summary of one canonical RAW transaction for operator inspection. +pub use ksp_store_api::RawTransactionSummary; /// Minimal durable identity retained after a canonical RAW transaction payload is purged. pub use ksp_store_api::RawTransactionTombstone; /// Write capability for canonical RAW transaction acquisitions. diff --git a/crates/ksp-store-lib/tests/public_api.rs b/crates/ksp-store-lib/tests/public_api.rs index 8ad3b0b..b8310c5 100644 --- a/crates/ksp-store-lib/tests/public_api.rs +++ b/crates/ksp-store-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/public_api.rs -// version: 8 +// version: 9 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -57,11 +57,14 @@ fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() { } #[test] -fn pre_003_facade_reexports_backend_agnostic_store_api_types() { +fn v0_3_8_pre_003_facade_reexports_backend_agnostic_inspection_types_without_dispatch() { let _raw_transaction = std::mem::size_of::>(); let _raw_account_state = std::mem::size_of::>(); let _query = std::mem::size_of::>(); + let _inspection_query = std::mem::size_of::>(); + let _inspection_page = std::mem::size_of::>>(); let _capability = std::mem::size_of::>(); + let _inspection_capability = std::mem::size_of::>(); let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(()); return; } diff --git a/deltas/0.3.8/pre.003.md b/deltas/0.3.8/pre.003.md new file mode 100644 index 0000000..14831af --- /dev/null +++ b/deltas/0.3.8/pre.003.md @@ -0,0 +1,273 @@ + + + +# Delta `0.3.8-pre.003` — contrats Store d’inspection et tracing Store Desk + +## Base requise + +Base directe attendue : + +```text +0.3.8-pre.002-fix.001 +workspace.package.version = 0.3.8-pre.2.fix.1 +``` + +Le gate opérateur fourni pour cette base est vert : audits Rust/Markdown, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, suite `ksp-app-store-desk` et arbres Cargo ont tous été exécutés sans échec après le fix Vite/Clippy. + +La livraison est : + +```text +0.3.8-pre.003 +workspace.package.version = 0.3.8-pre.3 +commit = v0.3.8-pre.003 +tag = aucun +``` + +## Objectif + +Matérialiser la primitive backend-neutral d’inspection RAW décidée en `pre.001`, sans modifier le chemin cursor/keyset canonique et sans ouvrir PostgreSQL dans cette tranche. + +Le delta ferme également deux invariants Store Desk explicitement demandés après `pre.002-fix.001` : + +```text +tracing développement Store Desk = supertrace +interaction frontend significative = événement KSP tracé +``` + +Aucun Store runtime n’est ouvert par l’application et aucun SQL n’est ajouté. + +## Contrats Store d’inspection + +`ksp-store-api` ajoute un modèle séparé de la pagination cursor : + +```text +RawInspectionPageRequest + offset: u64 + limit: RawPageLimit + +RawInspectionPage + items + total_items + filtered_items +``` + +La sémantique des counts est exacte : + +- `total_items` = nombre d’entités dans le scope obligatoire de la query avant filtres optionnels ; +- `filtered_items` = nombre d’entités après filtres optionnels ; +- `items` = uniquement la fenêtre random-access demandée. + +Les incohérences `filtered_items > total_items` ou `items.len() > filtered_items` sont rejetées par le modèle. + +Les queries ajoutées sont : + +```text +RawTransactionInspectionQuery + network + slots + direction + RawInspectionPageRequest + +RawAccountStateInspectionQuery + network + pubkey? + slots + direction + RawInspectionPageRequest +``` + +Le `u64` offset ne reçoit aucun plafond politique artificiel dans `ksp-store-api`. La conversion vers une limite physique backend et le rejet pré-I/O des offsets non représentables appartiennent aux vertical slices PostgreSQL `pre.004` / `pre.005`. + +## Summaries sans gros bytes + +`RawTransactionSummary` expose uniquement : + +```text +reference +slot +block_time +format_id +format_version +content_hash +payload_size_bytes? +retention_state +``` + +Le payload canonique n’est jamais contenu dans le summary. La taille est `None` pour `Purged`; un état qui conserve logiquement le payload exige une taille valide, non nulle et bornée par `MAX_RAW_PAYLOAD_BYTES`. + +`RawAccountStateSummary` expose uniquement : + +```text +reference +lamports +owner +executable +rent_epoch +data_length_bytes +``` + +Les bytes account data ne sont jamais contenus dans le summary et `data_length_bytes` reste borné par `MAX_RAW_ACCOUNT_DATA_BYTES`. + +## Capabilities + +Deux traits read object-safe sont ajoutés sans modifier les traits RAW historiques : + +```text +RawTransactionInspectionRead::inspect_raw_transactions +RawAccountStateInspectionRead::inspect_raw_account_states +``` + +Ils retournent `RawInspectionPage<...Summary>`. + +`ksp-store-lib` réexporte les nouveaux types et traits afin que les consommateurs futurs restent dépendants de la façade commune. En revanche : + +```text +Store n’implémente pas encore les deux traits d’inspection +PostgresBackend n’implémente pas encore les deux traits d’inspection +``` + +L’inventaire canonique existant reste donc exactement à dix implementations RAW sur la façade et dix sur le backend PostgreSQL. Les implementations d’inspection sont réservées à `pre.004` et `pre.005`. + +## Pagination canonique préservée + +Aucune modification n’est apportée à : + +```text +RawPageCursor +RawPageRequest +RawPage +RawTransactionQuery +RawAccountStateQuery +list_raw_transactions +list_raw_account_states +``` + +La frontière reste : + +```text +cursor/keyset = workers/backfill/replay/navigation durable +inspection offset/count = operator/admin random-access +DataTables = futur unique pager visuel de Store Desk +``` + +Aucun terme ou DTO DataTables (`draw`, `recordsTotal`, `recordsFiltered`) n’entre dans `ksp-store-api`. + +## Tracing Store Desk + +Le scaffold Store Desk demandait auparavant le profil par défaut de `std.logging` (`local_dev`). Il sélectionne maintenant explicitement : + +```text +DEVELOPMENT_LOGGING_PROFILE = supertrace +``` + +Le fallback local reste `LogFilterLevel::Trace` si la Config managed n’est pas disponible. + +Le frontend conserve les événements spécifiques existants pour navigation et Refresh et renforce le logging générique : + +- clics des boutons non spécialisés via un listener délégué au `document`, ce qui couvre aussi les boutons dynamiques créés ultérieurement par DataTables ; +- clic d’un contrôle tab ; +- activation effective d’un tab Bootstrap ; +- changement d’un `input`, `select` ou `textarea` ; +- IPC requested/completed/failed via le bridge existant. + +Les valeurs de filtres, payloads, URI et secrets ne sont pas ajoutés aux logs. Les tranches DataTables métier devront instrumenter explicitement page, page-length, redraw/query et actions détail sans logguer les données RAW. + +## Fichiers ajoutés + +```text +crates/ksp-store-api/src/model/raw_inspection.rs +crates/ksp-store-api/unit_tests/model/raw_inspection.rs +deltas/0.3.8/pre.003.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-app-store-desk/frontend/ts/main.ts +crates/ksp-app-store-desk/src/bootstrap.rs +crates/ksp-app-store-desk/src/constants.rs +crates/ksp-app-store-desk/src/lib.rs +crates/ksp-app-store-desk/tests/desktop_security.rs +crates/ksp-store-api/src/capability/raw_account.rs +crates/ksp-store-api/src/capability/raw_transaction.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 +crates/ksp-store-lib/src/lib.rs +crates/ksp-store-lib/tests/public_api.rs +docs/plans/029-V0_3_8_STORE_DESK_PLAN.md +docs/validation/025-V0_3_8_STORE_DESK.md +``` + +## Fichiers supprimés + +```text +aucun +``` + +## Validations exécutées dans l’environnement d’assemblage + +```text +python3 scripts/audit_rust_workspace_rules.py +-> General Rust rule audit: clean +-> Rust export completeness audit: 0 candidate(s) +-> KSP workspace Rust rule audit: clean + +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +-> Markdown table audit: clean +``` + +Contrôles ciblés supplémentaires : + +```text +Store API exact root export inventory: clean +Store API production module inventory: raw_inspection ajouté, aucun module physique +Store API UI/SQL boundary: clean +External backend object-safety canary: source complete pour 12 traits +ksp-store-lib inspection reexports: clean +Store facade canonical RAW implementations: 10 inchangées +PostgreSQL backend canonical RAW implementations: 10 inchangées +Store/PostgreSQL inspection implementations: absentes comme prévu +Store Desk supertrace selection: clean +Store Desk delegated button/tab/control tracing: clean +TypeScript syntax audit: clean (5 fichiers, TS 5.8.3) +Manifest parse: clean +``` + +## Validations non exécutées dans l’environnement d’assemblage + +`cargo` et `rustfmt` ne sont pas disponibles dans le sandbox d’assemblage. Le gate opérateur requis est : + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo check -p ksp-store-lib --no-default-features +cargo test -p ksp-app-store-desk +cargo tree -p ksp-store-api --edges normal +cargo tree -p ksp-store-lib --edges normal +``` + +## Invariants de fermeture + +- aucune reprise de code kbot3 ; +- aucun SQL ou type PostgreSQL dans `ksp-store-api`, `ksp-store-lib` ou Store Desk ; +- aucun remplacement du cursor/keyset RAW ; +- aucune implementation backend/facade des nouveaux traits dans cette tranche ; +- aucun `Store::open` supplémentaire dans Store Desk ; +- aucune dépendance Store directe ajoutée à Store Desk ; +- un seul pager DataTables restera visible quand l’UI métier sera branchée ; +- `supertrace` et le tracing des interactions frontend sont désormais des invariants explicites de la phase de développement. + +## Suite prévue + +Après gate opérateur vert, `pre.004` implémente **uniquement le vertical slice PostgreSQL + façade `RawTransactionInspectionRead`** : summary query, counts exacts, offset/limit checked et maintien intégral du listage keyset existant. diff --git a/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md b/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md index 993a9f7..cd19c70 100644 --- a/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md +++ b/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.8 — Store Desk V1 RAW @@ -96,6 +96,24 @@ kbot3 n'est jamais une source de package, HTML, SASS, assets, Rust, TypeScript o La sélection DataTables n'est pas justifiée par une action V1 : le détail s'ouvre par une action explicite dans la row. `datatables.net-select-bs5` n'est donc pas requis tant qu'une sélection persistante n'apporte pas une fonction réelle. +### 4.2 Invariant tracing Store Desk + +Pendant le développement de `ksp-app-store-desk`, le profil Logging demandé explicitement est `supertrace`; le fallback local reste `Trace`. Cette sélection est temporairement volontairement verbeuse et sera réévaluée avant `rel.001`. + +Toute interaction opérateur significative du frontend doit être tracée via le bridge KSP, sans contenu sensible : + +```text +clic bouton / action explicite +activation tab / vue +changement de filtre ou contrôle +Refresh +ouverture détail +pagination / changement de page length DataTables +redraw/query serverSide et échec IPC associé +``` + +Les événements transportent uniquement des identifiants de contrôle, catégories d'action et compteurs sûrs. Les valeurs de filtres, payloads, URI, credentials et contenu RAW ne sont jamais injectés dans les logs. Les handlers métier peuvent ajouter un événement plus précis, mais aucune interaction nouvellement introduite ne doit rester silencieuse. + ## 5. npm / DataTables map Les ranges KSP v0.3.7 observés dans Config/Wallet Desk sont : diff --git a/docs/validation/025-V0_3_8_STORE_DESK.md b/docs/validation/025-V0_3_8_STORE_DESK.md index 0d5bd55..41cf770 100644 --- a/docs/validation/025-V0_3_8_STORE_DESK.md +++ b/docs/validation/025-V0_3_8_STORE_DESK.md @@ -1,5 +1,5 @@ - + # Validation v0.3.8 — Store Desk V1 RAW @@ -89,7 +89,7 @@ Le journal opérateur fourni pour v0.3.7 rapporte un gate complet. Il reste une ## 8. Gates futures -- [ ] `pre.002` scaffold KSP strict + DataTables skeleton ; +- [X] `pre.002` scaffold KSP strict + DataTables skeleton ; - [ ] `pre.003` contrats Store inspection offset/count/summaries ; - [ ] `pre.004` PostgreSQL + façade inspection RawTransaction ; - [ ] `pre.005` PostgreSQL + façade inspection RawAccountState ; @@ -220,3 +220,39 @@ Le correctif `pre.002-fix.001` reste strictement dans le couloir scaffold de `pr - aucune capability Store, aucun SQL, aucun changement d’architecture pagination. La case `pre.002` reste ouverte jusqu’au gate opérateur du fix. + +## 14. Gate opérateur `pre.002-fix.001` + +Le gate complet rejoué par l’opérateur après le fix est vert : + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +KSP workspace Rust rule audit: clean +Markdown table audit: clean (314 table(s), 693 file(s)) +cargo check --workspace: PASS +cargo clippy --workspace --all-targets: PASS +cargo test -p ksp-app-store-desk: PASS +``` + +Détail Store Desk communiqué : 12 tests unitaires, 2 dependency-boundary, 6 desktop-contract, 3 desktop-security, 1 public-API, tous verts. Les arbres `cargo tree` normal/features ont également été produits. `pre.002` est donc fermé et `pre.003` peut s’ouvrir. + +## 15. `pre.003` — contrats Store d’inspection et tracing développement + +- [X] `workspace.package.version = 0.3.8-pre.3` ; +- [X] `RawInspectionPageRequest` sépare explicitement offset/limit de `RawPageRequest` cursor ; +- [X] `RawInspectionPage` expose `total_items` / `filtered_items` exacts avec validation de cohérence ; +- [X] `RawTransactionInspectionQuery` conserve network/range/direction et page random-access ; +- [X] `RawAccountStateInspectionQuery` conserve network/pubkey?/range/direction et page random-access ; +- [X] `RawTransactionSummary` exclut les payload bytes et expose uniquement metadata/retention/size ; +- [X] `RawAccountStateSummary` exclut les data bytes et expose uniquement metadata/data length ; +- [X] `RawTransactionInspectionRead` et `RawAccountStateInspectionRead` sont object-safe et backend-neutral ; +- [X] `ksp-store-lib` réexporte les nouveaux contrats sans encore les implémenter sur `Store` ; +- [X] le chemin cursor/keyset existant reste inchangé ; +- [X] aucun SQL, migration ou backend physique n’est introduit ; +- [X] aucun nom/protocole DataTables n’entre dans `ksp-store-api` ; +- [X] Store Desk demande explicitement le profil Logging `supertrace` pendant le développement ; +- [X] fallback Logging Store Desk reste `Trace` ; +- [X] tracing frontend couvre boutons, navigation, tabs, changements de contrôles, Refresh et IPC ; +- [X] les futurs événements DataTables page/length/query doivent être instrumentés lors de `pre.007`/`pre.008` sans logguer les valeurs de filtres ni payloads. +