lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 9ad72b6c05b72ddbab7d1f51728bd2abcbdd07f0
parent c8af311bc58580e67b8202263dceb2e481df373d
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 06:01:28 +0000

storage-sqlite: qualify the legacy importer

- govern the complete predecessor qualification matrix
- exercise one mixed four-source import end to end
- prove reopen finalization retry and source retention
- retain zero live dual writes and zero Studio imports

Diffstat:
Acontracts/storage/legacy_import_qualification_v1.toml | 7+++++++
Mcrates/storage_sqlite/README.md | 5+++++
Mcrates/storage_sqlite/src/legacy.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 204 insertions(+), 0 deletions(-)

diff --git a/contracts/storage/legacy_import_qualification_v1.toml b/contracts/storage/legacy_import_qualification_v1.toml @@ -0,0 +1,7 @@ +schema_version = 1 +source_matrix = ["event_store_v1_to_v4", "outbox_v1", "private_v1", "studio_v1_host_handoff"] +required_cases = ["mandatory_backup", "unsupported_schema_rejection", "mixed_source_golden", "bounded_resume", "close_reopen", "invalid_row_rollback", "private_commit_recovery", "lost_success_retry", "conflicting_identity_rejection", "source_retention", "no_live_dual_write"] +mixed_imported_row_count = 14 +mixed_host_handoff_row_count = 0 +exact_retry = true +hidden_clock_or_entropy = false diff --git a/crates/storage_sqlite/README.md b/crates/storage_sqlite/README.md @@ -99,6 +99,11 @@ response reconstructs the exact receipt from both commit markers. Immutable legacy staging remains retained as owned migration evidence, while live product tables are not dual-written and predecessor evidence is not deleted. +The qualification matrix includes every supported predecessor family, mixed +four-source operation, bounded page interruption and reopen, invalid-row +rollback, unsupported-schema and identity conflicts, private-first recovery, +lost-success retry, source retention, and live-table isolation. + ```rust,no_run use radroots_storage::event::SourceGeneration; use radroots_storage_sqlite::{OpenMode, OpenOptions, Paths, SqliteStorage}; diff --git a/crates/storage_sqlite/src/legacy.rs b/crates/storage_sqlite/src/legacy.rs @@ -3521,6 +3521,8 @@ mod tests { include_str!("../../../contracts/storage/legacy_import_validation_policy_v1.toml"); const IMPORT_FINALIZE_POLICY: &str = include_str!("../../../contracts/storage/legacy_import_finalize_policy_v1.toml"); + const IMPORT_QUALIFICATION_POLICY: &str = + include_str!("../../../contracts/storage/legacy_import_qualification_v1.toml"); const SDK_PRIVATE_STORE_SOURCE: &str = include_str!("../../../../sdk/crates/sdk/src/private_store.rs"); @@ -3725,6 +3727,17 @@ mod tests { hidden_clock_or_entropy: bool, } + #[derive(Deserialize)] + struct ImportQualificationPolicy { + schema_version: u32, + source_matrix: Vec<String>, + required_cases: Vec<String>, + mixed_imported_row_count: u64, + mixed_host_handoff_row_count: u64, + exact_retry: bool, + hidden_clock_or_entropy: bool, + } + fn generation(byte: u8) -> SourceGeneration { SourceGeneration::new([byte; 32]).expect("source generation") } @@ -4436,6 +4449,42 @@ mod tests { assert!(!policy.hidden_clock_or_entropy); } + #[test] + fn implementation_matches_the_governed_import_qualification_policy() { + let policy = toml::from_str::<ImportQualificationPolicy>(IMPORT_QUALIFICATION_POLICY) + .expect("import qualification policy"); + assert_eq!(policy.schema_version, 1); + assert_eq!( + policy.source_matrix, + [ + "event_store_v1_to_v4", + "outbox_v1", + "private_v1", + "studio_v1_host_handoff" + ] + ); + assert_eq!( + policy.required_cases, + [ + "mandatory_backup", + "unsupported_schema_rejection", + "mixed_source_golden", + "bounded_resume", + "close_reopen", + "invalid_row_rollback", + "private_commit_recovery", + "lost_success_retry", + "conflicting_identity_rejection", + "source_retention", + "no_live_dual_write" + ] + ); + assert_eq!(policy.mixed_imported_row_count, 14); + assert_eq!(policy.mixed_host_handoff_row_count, 0); + assert!(policy.exact_retry); + assert!(!policy.hidden_clock_or_entropy); + } + fn assert_fixed_schema_policy( policy: &FixedSchemaPolicy, user_version: i64, @@ -5415,6 +5464,149 @@ mod tests { store.close().await.expect("close target"); } + #[tokio::test] + async fn mixed_source_import_qualifies_backup_resume_validation_and_completion() { + let target_root = tempfile::tempdir().expect("target root"); + let legacy_root = tempfile::tempdir().expect("legacy root"); + let backup_root = tempfile::tempdir().expect("backup root"); + let event_path = legacy_root.path().join("event_store.sqlite"); + let outbox_path = legacy_root.path().join("outbox.sqlite"); + let private_path = legacy_root.path().join("private.sqlite"); + let studio_path = legacy_root.path().join("studio.sqlite"); + let event_connection = + supported_event_database(&event_path, &[signed_event("mixed")]).await; + let outbox_connection = supported_outbox_database(&outbox_path).await; + let private_connection = supported_private_database(&private_path).await; + let studio_connection = supported_studio_database(&studio_path).await; + let plan = LegacyImportPlan::new( + LegacyImportId::new([133; 16]).expect("import id"), + vec![ + LegacySource::new(LegacySourceKind::Studio, &studio_path).expect("Studio source"), + LegacySource::new(LegacySourceKind::Private, &private_path) + .expect("private source"), + LegacySource::new(LegacySourceKind::Outbox, &outbox_path).expect("outbox source"), + LegacySource::new(LegacySourceKind::EventStore, &event_path).expect("event source"), + ], + backup_root.path(), + 14_000, + ) + .expect("mixed import plan"); + let (target_paths, store) = target(target_root.path()).await; + let prepared = store + .prepare_legacy_import(&plan) + .await + .expect("prepare mixed import"); + assert_eq!(prepared.snapshots().len(), 4); + let classified = store + .classify_legacy_import(&prepared) + .await + .expect("classify mixed import"); + store + .begin_legacy_import(&classified, 14_001) + .await + .expect("begin mixed import"); + + assert!( + store + .stage_legacy_events(&classified, 1, 14_002) + .await + .expect("stage mixed event") + .is_complete() + ); + for page in 0_u64..5 { + let result = store + .stage_legacy_outbox(&classified, 1, 14_003 + page) + .await + .expect("stage mixed outbox"); + assert_eq!(result.is_complete(), page == 4); + } + for page in 0_u64..8 { + let result = store + .stage_legacy_private(&classified, 1, 14_008 + page) + .await + .expect("stage mixed private"); + assert_eq!(result.is_complete(), page == 7); + } + let handoff = store + .prepare_legacy_studio_handoff(&classified) + .await + .expect("prepare mixed Studio handoff"); + let host_receipt = + LegacyStudioHandoffReceipt::new(handoff.handoff_sha256(), MemberDigest::new([88; 32])) + .expect("mixed Studio host receipt"); + store + .acknowledge_legacy_studio_handoff(&classified, host_receipt, 14_016) + .await + .expect("acknowledge mixed Studio handoff"); + let validation = store + .validate_legacy_import(&classified) + .await + .expect("validate mixed import"); + assert_eq!(validation.imported_row_count(), 14); + store.close().await.expect("close before mixed finalize"); + + let reopened = + SqliteStorage::open(OpenOptions::new(target_paths, OpenMode::ReadWriteExisting)) + .await + .expect("reopen mixed import"); + let receipt = reopened + .finalize_legacy_import(&classified, validation, 14_017) + .await + .expect("finalize mixed import"); + assert_eq!(receipt.imported_row_count(), 14); + assert_eq!(receipt.validation_sha256(), validation.validation_sha256()); + assert_eq!( + reopened + .finalize_legacy_import(&classified, validation, 14_999) + .await + .expect("retry mixed completion"), + receipt + ); + let journal = reopened + .legacy_import_journal(plan.import_id()) + .await + .expect("mixed journal") + .expect("durable mixed journal"); + assert_eq!(journal.state(), LegacyImportState::Complete); + assert!( + journal + .members() + .iter() + .all(|member| member.state() == LegacyImportMemberState::Complete) + ); + assert_eq!( + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM radroots_runtime_events") + .fetch_one(reopened.pool()) + .await + .expect("mixed live event isolation"), + 0 + ); + assert_eq!( + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM radroots_private_artifacts") + .fetch_one(reopened.private_pool()) + .await + .expect("mixed live private isolation"), + 0 + ); + for source in plan.sources() { + assert!(source.path().is_file(), "predecessor source was removed"); + } + event_connection.close().await.expect("close event source"); + outbox_connection + .close() + .await + .expect("close outbox source"); + private_connection + .close() + .await + .expect("close private source"); + studio_connection + .close() + .await + .expect("close Studio source"); + reopened.close().await.expect("close mixed target"); + } + #[test] fn pure_catalog_classification_rejects_mixed_unknown_and_newer_schemas() { let studio = vec![CatalogRow {