commit c66740d5851ebff78dbe8d8d9e6fb5f03ee91d8e
parent 023faeb5a806a3494ebc08b42b0bda441dac41f7
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 11:44:55 +0000
sdk: migrate reliability operations
- delegate backup restore integrity and status to canonical storage contracts
- preserve optimistic staging interruption and explicit finalization semantics
- expose passive diagnostics with only native redacted status values
- verify memory behavior and guard against duplicate or Studio representations
Diffstat:
4 files changed, 341 insertions(+), 1 deletion(-)
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -214,6 +214,11 @@ impl Client {
Ok(self.inner.storage.as_ref())
}
+ /// Returns backend-neutral backup, restore, status, and integrity operations.
+ pub fn storage_operations(&self) -> Result<crate::storage::Operations<'_>> {
+ Ok(crate::storage::Operations::new(self.storage()?))
+ }
+
/// Returns the injected signer, when outbound authoring is enabled.
pub fn signer(&self) -> Result<Option<&dyn Signer>> {
self.require_open()?;
diff --git a/crates/sdk/src/diagnostics.rs b/crates/sdk/src/diagnostics.rs
@@ -1 +1,59 @@
-//! Redacted client diagnostics.
+//! Redacted client diagnostics composed from canonical capability and storage status.
+
+/// One passive diagnostics snapshot containing only lower-owned status types.
+#[derive(Clone, Debug)]
+pub struct Report {
+ capabilities: crate::capability::CapabilityReport,
+ storage: radroots_storage::StorageStatus,
+}
+
+impl Report {
+ /// Returns the side-effect-free SDK capability report.
+ #[must_use]
+ pub const fn capabilities(&self) -> &crate::capability::CapabilityReport {
+ &self.capabilities
+ }
+
+ /// Returns the canonical backend status without implementation details.
+ #[must_use]
+ pub const fn storage(&self) -> radroots_storage::StorageStatus {
+ self.storage
+ }
+}
+
+/// Captures passive, secret-safe diagnostics without filesystem paths, SQL,
+/// connection handles, private artifacts, or recovery side effects.
+pub async fn inspect(client: &crate::Client) -> crate::Result<Report> {
+ Ok(Report {
+ capabilities: client.capabilities(),
+ storage: client.storage_status().await?,
+ })
+}
+
+#[cfg(all(test, feature = "memory"))]
+mod tests {
+ use std::sync::Arc;
+
+ use radroots_storage::{
+ event::SourceGeneration, memory::MemoryStorage, status::StorageBackend,
+ };
+
+ use crate::ClientBuilder;
+
+ #[tokio::test]
+ async fn report_is_passive_native_and_redacted() {
+ let client = ClientBuilder::new()
+ .storage(Arc::new(MemoryStorage::new(
+ SourceGeneration::new([9; 32]).expect("generation"),
+ )))
+ .build()
+ .expect("client");
+ let report = super::inspect(&client).await.expect("report");
+
+ assert_eq!(report.storage().backend(), StorageBackend::Memory);
+ assert!(report.capabilities().iter().all(|status| {
+ !status.id().as_str().contains('/') && !status.id().as_str().contains('\\')
+ }));
+ assert!(!format!("{report:?}").contains("sqlite"));
+ }
+}
diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage.rs
@@ -17,3 +17,243 @@ pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode;
/// Validated SQLite-owned paths; this contains no backend handle.
#[cfg(feature = "sqlite")]
pub type SqlitePaths = radroots_storage_sqlite::Paths;
+
+use radroots_storage::backup::{
+ BackupId, BackupOperation, BackupPlan, BackupTransition, ReliabilityRevision, RestoreOperation,
+ RestorePlan, RestoreTransition, StorageReliability,
+};
+
+/// Borrowed reliability operations over the canonical backend-neutral SPI.
+#[derive(Clone, Copy)]
+pub struct Operations<'a> {
+ storage: &'a dyn radroots_storage::Storage,
+}
+
+impl<'a> Operations<'a> {
+ pub(crate) const fn new(storage: &'a dyn radroots_storage::Storage) -> Self {
+ Self { storage }
+ }
+
+ /// Begins or resumes one idempotent backup plan.
+ pub async fn begin_backup(
+ &self,
+ plan: BackupPlan,
+ ) -> Result<BackupOperation, radroots_storage::Error> {
+ StorageReliability::begin_backup(self.storage, plan).await
+ }
+
+ /// Applies one optimistic backup transition.
+ pub async fn transition_backup(
+ &self,
+ backup_id: BackupId,
+ expected_revision: ReliabilityRevision,
+ transition: BackupTransition,
+ at_unix_ms: u64,
+ ) -> Result<BackupOperation, radroots_storage::Error> {
+ StorageReliability::transition_backup(
+ self.storage,
+ backup_id,
+ expected_revision,
+ transition,
+ at_unix_ms,
+ )
+ .await
+ }
+
+ /// Begins or resumes one staged restore plan.
+ pub async fn begin_restore(
+ &self,
+ plan: RestorePlan,
+ ) -> Result<RestoreOperation, radroots_storage::Error> {
+ StorageReliability::begin_restore(self.storage, plan).await
+ }
+
+ /// Applies one optimistic staged restore transition.
+ pub async fn transition_restore(
+ &self,
+ backup_id: BackupId,
+ expected_revision: ReliabilityRevision,
+ transition: RestoreTransition,
+ at_unix_ms: u64,
+ ) -> Result<RestoreOperation, radroots_storage::Error> {
+ StorageReliability::transition_restore(
+ self.storage,
+ backup_id,
+ expected_revision,
+ transition,
+ at_unix_ms,
+ )
+ .await
+ }
+
+ /// Returns passive backend status without initiating recovery work.
+ pub async fn status(&self) -> Result<Status, radroots_storage::Error> {
+ StorageReliability::status(self.storage).await
+ }
+
+ /// Runs backend-owned integrity inspection.
+ pub async fn integrity(&self) -> Result<IntegrityStatus, radroots_storage::Error> {
+ StorageReliability::integrity(self.storage).await
+ }
+}
+
+impl std::fmt::Debug for Operations<'_> {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ formatter
+ .debug_struct("Operations")
+ .field("storage", &"<borrowed canonical storage>")
+ .finish()
+ }
+}
+
+#[cfg(all(test, feature = "memory"))]
+mod tests {
+ use std::sync::Arc;
+
+ use radroots_storage::{
+ backup::{
+ BackupFormatVersion, BackupManifest, BackupMember, BackupMemberKind,
+ BackupSecretPolicy, BackupStage, MemberDigest, MemberVerification, RestoreMemberStatus,
+ RestoreStage,
+ },
+ event::SourceGeneration,
+ memory::MemoryStorage,
+ status::IntegrityHealth,
+ };
+
+ use crate::ClientBuilder;
+
+ use super::*;
+
+ fn manifest(backup_id: BackupId) -> BackupManifest {
+ BackupManifest::new(
+ BackupFormatVersion::V1,
+ backup_id,
+ 1_800_000_000_100,
+ BackupSecretPolicy::ExcludeProtectedStorage,
+ vec![
+ BackupMember::new(
+ "runtime/events.bin",
+ BackupMemberKind::Runtime,
+ 16,
+ MemberDigest::new([3; 32]),
+ )
+ .expect("member"),
+ ],
+ )
+ .expect("manifest")
+ }
+
+ #[tokio::test]
+ async fn memory_reliability_preserves_staging_interruption_integrity_and_native_states() {
+ let storage = Arc::new(MemoryStorage::new(
+ SourceGeneration::new([8; 32]).expect("generation"),
+ ));
+ let client = ClientBuilder::new()
+ .storage(storage)
+ .build()
+ .expect("client");
+ let operations = client.storage_operations().expect("operations");
+ let backup_id = BackupId::new([9; 16]).expect("backup id");
+ let plan = BackupPlan::new(
+ backup_id,
+ BackupFormatVersion::V1,
+ BackupSecretPolicy::ExcludeProtectedStorage,
+ 1_800_000_000_000,
+ )
+ .expect("plan");
+
+ drop(operations.begin_backup(plan.clone()));
+ let planned = operations.begin_backup(plan).await.expect("planned");
+ assert_eq!(planned.stage(), BackupStage::Planned);
+ let captured = operations
+ .transition_backup(
+ backup_id,
+ planned.revision(),
+ BackupTransition::Captured(manifest(backup_id)),
+ 1_800_000_000_200,
+ )
+ .await
+ .expect("captured");
+ let verified = operations
+ .transition_backup(
+ backup_id,
+ captured.revision(),
+ BackupTransition::Verified,
+ 1_800_000_000_300,
+ )
+ .await
+ .expect("verified");
+ let finalized = operations
+ .transition_backup(
+ backup_id,
+ verified.revision(),
+ BackupTransition::Finalize,
+ 1_800_000_000_400,
+ )
+ .await
+ .expect("finalized");
+ assert_eq!(finalized.stage(), BackupStage::Finalized);
+
+ let restore_plan = RestorePlan::new(
+ manifest(backup_id),
+ BackupSecretPolicy::ExcludeProtectedStorage,
+ 1_800_000_001_000,
+ )
+ .expect("restore plan");
+ let staging = operations
+ .begin_restore(restore_plan.clone())
+ .await
+ .expect("staging");
+ assert_eq!(staging.stage(), RestoreStage::Staging);
+ let replayed = operations
+ .begin_restore(restore_plan)
+ .await
+ .expect("resume staging");
+ assert_eq!(replayed, staging);
+ let verifying = operations
+ .transition_restore(
+ backup_id,
+ staging.revision(),
+ RestoreTransition::Staged,
+ 1_800_000_001_100,
+ )
+ .await
+ .expect("verifying");
+ let finalizing = operations
+ .transition_restore(
+ backup_id,
+ verifying.revision(),
+ RestoreTransition::Verified(vec![
+ RestoreMemberStatus::new("runtime/events.bin", MemberVerification::Verified)
+ .expect("member status"),
+ ]),
+ 1_800_000_001_200,
+ )
+ .await
+ .expect("finalizing");
+ let restored = operations
+ .transition_restore(
+ backup_id,
+ finalizing.revision(),
+ RestoreTransition::Finalize,
+ 1_800_000_001_300,
+ )
+ .await
+ .expect("restored");
+ assert_eq!(restored.stage(), RestoreStage::Finalized);
+ assert_eq!(
+ operations.integrity().await.expect("integrity").health(),
+ IntegrityHealth::Healthy
+ );
+ assert_eq!(
+ operations
+ .status()
+ .await
+ .expect("status")
+ .integrity()
+ .health(),
+ IntegrityHealth::Healthy
+ );
+ }
+}
diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs
@@ -6,6 +6,8 @@ const CLIENT: &str = include_str!("../src/client.rs");
const FARM: &str = include_str!("../src/farm.rs");
const LISTING: &str = include_str!("../src/listing.rs");
const TRADE: &str = include_str!("../src/trade.rs");
+const STORAGE: &str = include_str!("../src/storage.rs");
+const DIAGNOSTICS: &str = include_str!("../src/diagnostics.rs");
const SYNC: &str = include_str!("../src/sync.rs");
const TRANSPORT: &str = include_str!("../src/transport.rs");
@@ -308,6 +310,41 @@ fn trade_operations_use_canonical_workflow_storage_sync_and_projection_types() {
);
}
+#[test]
+fn reliability_and_diagnostics_return_canonical_storage_contracts() {
+ for required in [
+ "StorageReliability::begin_backup",
+ "StorageReliability::transition_backup",
+ "StorageReliability::begin_restore",
+ "StorageReliability::transition_restore",
+ "StorageReliability::integrity",
+ "StorageReliability::status",
+ ] {
+ assert!(
+ STORAGE.contains(required),
+ "missing storage delegation `{required}`"
+ );
+ }
+ for forbidden in [
+ "struct BackupManifest",
+ "struct IntegrityStatus",
+ "Studio",
+ "sqlx::",
+ ] {
+ assert!(
+ !STORAGE.contains(forbidden),
+ "storage source duplicates `{forbidden}`"
+ );
+ }
+ assert!(DIAGNOSTICS.contains("radroots_storage::StorageStatus"));
+ for forbidden in ["path:", "SqlitePool", "private_artifact"] {
+ assert!(
+ !DIAGNOSTICS.contains(forbidden),
+ "diagnostics leaks `{forbidden}`"
+ );
+ }
+}
+
fn dependency_names(manifest: &str) -> BTreeSet<&str> {
let dependencies = manifest
.split_once("[dependencies]")