commit 640c5e9e12099c7a74e552f8b505aa6a7d51f240
parent 75488feb83ce93cc97121da52c60b332af8b3f71
Author: triesap <tyson@radroots.org>
Date: Mon, 14 Sep 2026 08:48:25 +0000
storage: retain late signing facts behind durable attempt proof
- Add exact signed-fact commands bound to original claim receipts
- Preserve signing stops and reject conflicting bytes or stale scheduling
- Append compatible SQLite storage and preserve legacy conversion
- Verify migration, rollback, API, coverage and workspace behavior
Diffstat:
20 files changed, 2401 insertions(+), 31 deletions(-)
diff --git a/contracts/api_baselines/radroots_storage.txt b/contracts/api_baselines/radroots_storage.txt
@@ -201,6 +201,7 @@ pub radroots_storage::authored_atomic::AuthoredAtomicCommand::Cancel(radroots_st
pub radroots_storage::authored_atomic::AuthoredAtomicCommand::Claim(radroots_storage::authored_atomic::ClaimAuthoredWork)
pub radroots_storage::authored_atomic::AuthoredAtomicCommand::Prepare(radroots_storage::authored_atomic::PrepareAuthoredOperation)
pub radroots_storage::authored_atomic::AuthoredAtomicCommand::PrepareFromDraft(alloc::boxed::Box<radroots_storage::authored_draft_submission::PrepareFromDraft>)
+pub radroots_storage::authored_atomic::AuthoredAtomicCommand::RecordSigned(radroots_storage::authored_atomic::RecordSignedArtifact)
impl radroots_storage::authored_atomic::AuthoredAtomicCommand
pub fn radroots_storage::authored_atomic::AuthoredAtomicCommand::commit_id(&self) -> radroots_storage::atomic::AtomicCommitId
pub fn radroots_storage::authored_atomic::AuthoredAtomicCommand::digest(&self) -> radroots_storage::atomic::AtomicCommitDigest
@@ -285,6 +286,16 @@ pub const fn radroots_storage::authored_atomic::PrepareAuthoredOperation::input_
pub fn radroots_storage::authored_atomic::PrepareAuthoredOperation::new(radroots_storage::authored::AuthoredOperation, alloc::vec::Vec<radroots_storage::authored::AuthoredArtifact>, alloc::vec::Vec<radroots_storage::authored_delivery::AuthoredDeliveryPlan>, radroots_storage::atomic::AtomicCommitDigest, u64) -> core::result::Result<Self, radroots_storage::Error>
pub const fn radroots_storage::authored_atomic::PrepareAuthoredOperation::operation(&self) -> &radroots_storage::authored::AuthoredOperation
pub const fn radroots_storage::authored_atomic::PrepareAuthoredOperation::requested_at_unix_ms(&self) -> u64
+pub struct radroots_storage::authored_atomic::RecordSignedArtifact
+impl radroots_storage::authored_atomic::RecordSignedArtifact
+pub fn radroots_storage::authored_atomic::RecordSignedArtifact::apply_to(&self, &mut radroots_storage::authored::AuthoredArtifact, &radroots_storage::authored_atomic::AuthoredAtomicReceipt) -> core::result::Result<(), radroots_storage::Error>
+pub const fn radroots_storage::authored_atomic::RecordSignedArtifact::artifact_id(&self) -> radroots_storage::authored::AuthoredArtifactId
+pub const fn radroots_storage::authored_atomic::RecordSignedArtifact::claim(&self) -> &radroots_storage::authored::WorkClaim
+pub fn radroots_storage::authored_atomic::RecordSignedArtifact::claim_command(&self) -> radroots_storage::authored_atomic::AuthoredAtomicCommand
+pub const fn radroots_storage::authored_atomic::RecordSignedArtifact::event(&self) -> &radroots_event::draft::SignedEvent
+pub fn radroots_storage::authored_atomic::RecordSignedArtifact::new(radroots_storage::journal::OperationInstanceId, radroots_storage::authored::AuthoredArtifactId, radroots_storage::authored::WorkClaim, radroots_event::draft::SignedEvent, u64) -> core::result::Result<Self, radroots_storage::Error>
+pub const fn radroots_storage::authored_atomic::RecordSignedArtifact::observed_at_unix_ms(&self) -> u64
+pub const fn radroots_storage::authored_atomic::RecordSignedArtifact::operation_id(&self) -> radroots_storage::journal::OperationInstanceId
pub struct radroots_storage::authored_atomic::WorkFence
impl radroots_storage::authored_atomic::WorkFence
pub const fn radroots_storage::authored_atomic::WorkFence::generation(&self) -> core::num::nonzero::NonZeroU64
diff --git a/contracts/api_baselines/radroots_storage_sqlite.txt b/contracts/api_baselines/radroots_storage_sqlite.txt
@@ -0,0 +1,618 @@
+pub mod radroots_storage_sqlite
+pub mod radroots_storage_sqlite::backup
+pub mod radroots_storage_sqlite::config
+pub struct radroots_storage_sqlite::config::OpenOptions
+impl radroots_storage_sqlite::config::OpenOptions
+pub fn radroots_storage_sqlite::config::OpenOptions::backup_root(&self) -> core::option::Option<&std::path::Path>
+pub fn radroots_storage_sqlite::config::OpenOptions::busy_timeout(&self) -> core::time::Duration
+pub fn radroots_storage_sqlite::config::OpenOptions::foreign_keys_enabled(&self) -> bool
+pub fn radroots_storage_sqlite::config::OpenOptions::mode(&self) -> radroots_storage_sqlite::open::OpenMode
+pub fn radroots_storage_sqlite::config::OpenOptions::new(radroots_storage_sqlite::open::Paths, radroots_storage_sqlite::open::OpenMode) -> Self
+pub fn radroots_storage_sqlite::config::OpenOptions::paths(&self) -> &radroots_storage_sqlite::open::Paths
+pub fn radroots_storage_sqlite::config::OpenOptions::source_generation(&self) -> core::option::Option<radroots_storage::event::SourceGeneration>
+pub fn radroots_storage_sqlite::config::OpenOptions::source_generation_created_at_unix_ms(&self) -> core::option::Option<u64>
+pub fn radroots_storage_sqlite::config::OpenOptions::validate_filesystem(&self) -> core::result::Result<(), radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::wal_enabled(&self) -> bool
+pub fn radroots_storage_sqlite::config::OpenOptions::with_backup_root(self, impl core::convert::Into<std::path::PathBuf>) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::with_busy_timeout(self, core::time::Duration) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::with_source_generation(self, radroots_storage::event::SourceGeneration, u64) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::writer_policy(&self) -> radroots_storage::status::WriterPolicy
+pub mod radroots_storage_sqlite::integrity
+pub mod radroots_storage_sqlite::legacy
+#[non_exhaustive] pub enum radroots_storage_sqlite::legacy::LegacyImportDisposition
+pub radroots_storage_sqlite::legacy::LegacyImportDisposition::HostHandoff
+pub radroots_storage_sqlite::legacy::LegacyImportDisposition::Import
+impl radroots_storage_sqlite::legacy::LegacyImportDisposition
+pub const fn radroots_storage_sqlite::legacy::LegacyImportDisposition::as_str(self) -> &'static str
+#[non_exhaustive] pub enum radroots_storage_sqlite::legacy::LegacyImportMemberState
+pub radroots_storage_sqlite::legacy::LegacyImportMemberState::Complete
+pub radroots_storage_sqlite::legacy::LegacyImportMemberState::Pending
+pub radroots_storage_sqlite::legacy::LegacyImportMemberState::Ready
+pub radroots_storage_sqlite::legacy::LegacyImportMemberState::Staging
+impl radroots_storage_sqlite::legacy::LegacyImportMemberState
+pub const fn radroots_storage_sqlite::legacy::LegacyImportMemberState::as_str(self) -> &'static str
+#[non_exhaustive] pub enum radroots_storage_sqlite::legacy::LegacyImportState
+pub radroots_storage_sqlite::legacy::LegacyImportState::Classified
+pub radroots_storage_sqlite::legacy::LegacyImportState::Committing
+pub radroots_storage_sqlite::legacy::LegacyImportState::Complete
+pub radroots_storage_sqlite::legacy::LegacyImportState::Ready
+pub radroots_storage_sqlite::legacy::LegacyImportState::Staging
+impl radroots_storage_sqlite::legacy::LegacyImportState
+pub const fn radroots_storage_sqlite::legacy::LegacyImportState::as_str(self) -> &'static str
+#[non_exhaustive] pub enum radroots_storage_sqlite::legacy::LegacyOutboxTable
+pub radroots_storage_sqlite::legacy::LegacyOutboxTable::DeliveryAttempts
+pub radroots_storage_sqlite::legacy::LegacyOutboxTable::DeliveryPlans
+pub radroots_storage_sqlite::legacy::LegacyOutboxTable::DeliveryTargets
+pub radroots_storage_sqlite::legacy::LegacyOutboxTable::Events
+pub radroots_storage_sqlite::legacy::LegacyOutboxTable::Operations
+impl radroots_storage_sqlite::legacy::LegacyOutboxTable
+pub const fn radroots_storage_sqlite::legacy::LegacyOutboxTable::as_str(self) -> &'static str
+#[non_exhaustive] pub enum radroots_storage_sqlite::legacy::LegacyPrivateTable
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::CursorKeys
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::FarmLocations
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::Metadata
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::Nip46Sessions
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::RotationProgress
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::SigningSecrets
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::TradeArtifacts
+pub radroots_storage_sqlite::legacy::LegacyPrivateTable::WrappedProfileKeys
+impl radroots_storage_sqlite::legacy::LegacyPrivateTable
+pub const fn radroots_storage_sqlite::legacy::LegacyPrivateTable::as_str(self) -> &'static str
+#[non_exhaustive] pub enum radroots_storage_sqlite::legacy::LegacySchema
+pub radroots_storage_sqlite::legacy::LegacySchema::EventStoreV1
+pub radroots_storage_sqlite::legacy::LegacySchema::EventStoreV2
+pub radroots_storage_sqlite::legacy::LegacySchema::EventStoreV3
+pub radroots_storage_sqlite::legacy::LegacySchema::EventStoreV4
+pub radroots_storage_sqlite::legacy::LegacySchema::OutboxV1
+pub radroots_storage_sqlite::legacy::LegacySchema::PrivateV1
+pub radroots_storage_sqlite::legacy::LegacySchema::StudioV1HostHandoff
+impl radroots_storage_sqlite::legacy::LegacySchema
+pub const fn radroots_storage_sqlite::legacy::LegacySchema::as_str(self) -> &'static str
+pub const fn radroots_storage_sqlite::legacy::LegacySchema::disposition(self) -> radroots_storage_sqlite::legacy::LegacyImportDisposition
+#[non_exhaustive] pub enum radroots_storage_sqlite::legacy::LegacySourceKind
+pub radroots_storage_sqlite::legacy::LegacySourceKind::EventStore
+pub radroots_storage_sqlite::legacy::LegacySourceKind::Outbox
+pub radroots_storage_sqlite::legacy::LegacySourceKind::Private
+pub radroots_storage_sqlite::legacy::LegacySourceKind::Studio
+impl radroots_storage_sqlite::legacy::LegacySourceKind
+pub const fn radroots_storage_sqlite::legacy::LegacySourceKind::as_str(self) -> &'static str
+pub struct radroots_storage_sqlite::legacy::ClassifiedLegacyImport
+impl radroots_storage_sqlite::legacy::ClassifiedLegacyImport
+pub fn radroots_storage_sqlite::legacy::ClassifiedLegacyImport::bundle_path(&self) -> &std::path::Path
+pub const fn radroots_storage_sqlite::legacy::ClassifiedLegacyImport::import_id(&self) -> radroots_storage_sqlite::legacy::LegacyImportId
+pub fn radroots_storage_sqlite::legacy::ClassifiedLegacyImport::sources(&self) -> &[radroots_storage_sqlite::legacy::LegacySourceClassification]
+pub const fn radroots_storage_sqlite::legacy::ClassifiedLegacyImport::target_generation(&self) -> radroots_storage::event::SourceGeneration
+pub struct radroots_storage_sqlite::legacy::LegacyEventStagePage
+impl radroots_storage_sqlite::legacy::LegacyEventStagePage
+pub const fn radroots_storage_sqlite::legacy::LegacyEventStagePage::is_complete(&self) -> bool
+pub const fn radroots_storage_sqlite::legacy::LegacyEventStagePage::resume_cursor(&self) -> core::option::Option<&[u8; 8]>
+pub const fn radroots_storage_sqlite::legacy::LegacyEventStagePage::staged_row_count(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyEventStagePage::staged_rows(&self) -> u16
+pub struct radroots_storage_sqlite::legacy::LegacyImportCommitReceipt
+impl radroots_storage_sqlite::legacy::LegacyImportCommitReceipt
+pub const fn radroots_storage_sqlite::legacy::LegacyImportCommitReceipt::completed_at_unix_ms(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyImportCommitReceipt::imported_row_count(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyImportCommitReceipt::validation_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub struct radroots_storage_sqlite::legacy::LegacyImportId(_)
+impl radroots_storage_sqlite::legacy::LegacyImportId
+pub const fn radroots_storage_sqlite::legacy::LegacyImportId::as_bytes(&self) -> &[u8; 16]
+pub const fn radroots_storage_sqlite::legacy::LegacyImportId::new([u8; 16]) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub struct radroots_storage_sqlite::legacy::LegacyImportJournal
+impl radroots_storage_sqlite::legacy::LegacyImportJournal
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::classification_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::completed_at_unix_ms(&self) -> core::option::Option<u64>
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::import_id(&self) -> radroots_storage_sqlite::legacy::LegacyImportId
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::manifest_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub fn radroots_storage_sqlite::legacy::LegacyImportJournal::members(&self) -> &[radroots_storage_sqlite::legacy::LegacyImportMemberJournal]
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::started_at_unix_ms(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::state(&self) -> radroots_storage_sqlite::legacy::LegacyImportState
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::target_generation(&self) -> radroots_storage::event::SourceGeneration
+pub const fn radroots_storage_sqlite::legacy::LegacyImportJournal::updated_at_unix_ms(&self) -> u64
+pub struct radroots_storage_sqlite::legacy::LegacyImportMemberJournal
+impl radroots_storage_sqlite::legacy::LegacyImportMemberJournal
+pub const fn radroots_storage_sqlite::legacy::LegacyImportMemberJournal::classification(&self) -> &radroots_storage_sqlite::legacy::LegacySourceClassification
+pub fn radroots_storage_sqlite::legacy::LegacyImportMemberJournal::resume_cursor(&self) -> core::option::Option<&[u8]>
+pub const fn radroots_storage_sqlite::legacy::LegacyImportMemberJournal::staged_row_count(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyImportMemberJournal::state(&self) -> radroots_storage_sqlite::legacy::LegacyImportMemberState
+pub const fn radroots_storage_sqlite::legacy::LegacyImportMemberJournal::updated_at_unix_ms(&self) -> u64
+pub struct radroots_storage_sqlite::legacy::LegacyImportPlan
+impl radroots_storage_sqlite::legacy::LegacyImportPlan
+pub fn radroots_storage_sqlite::legacy::LegacyImportPlan::backup_root(&self) -> &std::path::Path
+pub const fn radroots_storage_sqlite::legacy::LegacyImportPlan::import_id(&self) -> radroots_storage_sqlite::legacy::LegacyImportId
+pub fn radroots_storage_sqlite::legacy::LegacyImportPlan::new(radroots_storage_sqlite::legacy::LegacyImportId, alloc::vec::Vec<radroots_storage_sqlite::legacy::LegacySource>, impl core::convert::Into<std::path::PathBuf>, u64) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub const fn radroots_storage_sqlite::legacy::LegacyImportPlan::requested_at_unix_ms(&self) -> u64
+pub fn radroots_storage_sqlite::legacy::LegacyImportPlan::sources(&self) -> &[radroots_storage_sqlite::legacy::LegacySource]
+pub struct radroots_storage_sqlite::legacy::LegacyImportValidation
+impl radroots_storage_sqlite::legacy::LegacyImportValidation
+pub const fn radroots_storage_sqlite::legacy::LegacyImportValidation::imported_row_count(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyImportValidation::validation_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub struct radroots_storage_sqlite::legacy::LegacyOutboxStagePage
+impl radroots_storage_sqlite::legacy::LegacyOutboxStagePage
+pub const fn radroots_storage_sqlite::legacy::LegacyOutboxStagePage::is_complete(&self) -> bool
+pub const fn radroots_storage_sqlite::legacy::LegacyOutboxStagePage::resume_cursor(&self) -> &[u8; 9]
+pub const fn radroots_storage_sqlite::legacy::LegacyOutboxStagePage::staged_row_count(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyOutboxStagePage::staged_rows(&self) -> u16
+pub const fn radroots_storage_sqlite::legacy::LegacyOutboxStagePage::table(&self) -> radroots_storage_sqlite::legacy::LegacyOutboxTable
+pub struct radroots_storage_sqlite::legacy::LegacyPrivateStagePage
+impl radroots_storage_sqlite::legacy::LegacyPrivateStagePage
+pub const fn radroots_storage_sqlite::legacy::LegacyPrivateStagePage::is_complete(&self) -> bool
+pub fn radroots_storage_sqlite::legacy::LegacyPrivateStagePage::resume_cursor(&self) -> &[u8]
+pub const fn radroots_storage_sqlite::legacy::LegacyPrivateStagePage::staged_row_count(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyPrivateStagePage::staged_rows(&self) -> u16
+pub const fn radroots_storage_sqlite::legacy::LegacyPrivateStagePage::table(&self) -> radroots_storage_sqlite::legacy::LegacyPrivateTable
+pub struct radroots_storage_sqlite::legacy::LegacySource
+impl radroots_storage_sqlite::legacy::LegacySource
+pub const fn radroots_storage_sqlite::legacy::LegacySource::kind(&self) -> radroots_storage_sqlite::legacy::LegacySourceKind
+pub fn radroots_storage_sqlite::legacy::LegacySource::new(radroots_storage_sqlite::legacy::LegacySourceKind, impl core::convert::Into<std::path::PathBuf>) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::legacy::LegacySource::path(&self) -> &std::path::Path
+pub struct radroots_storage_sqlite::legacy::LegacySourceClassification
+impl radroots_storage_sqlite::legacy::LegacySourceClassification
+pub const fn radroots_storage_sqlite::legacy::LegacySourceClassification::catalog_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub const fn radroots_storage_sqlite::legacy::LegacySourceClassification::kind(&self) -> radroots_storage_sqlite::legacy::LegacySourceKind
+pub const fn radroots_storage_sqlite::legacy::LegacySourceClassification::schema(&self) -> radroots_storage_sqlite::legacy::LegacySchema
+pub const fn radroots_storage_sqlite::legacy::LegacySourceClassification::user_version(&self) -> u32
+pub struct radroots_storage_sqlite::legacy::LegacySourceSnapshot
+impl radroots_storage_sqlite::legacy::LegacySourceSnapshot
+pub const fn radroots_storage_sqlite::legacy::LegacySourceSnapshot::byte_length(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacySourceSnapshot::kind(&self) -> radroots_storage_sqlite::legacy::LegacySourceKind
+pub fn radroots_storage_sqlite::legacy::LegacySourceSnapshot::relative_path(&self) -> &str
+pub const fn radroots_storage_sqlite::legacy::LegacySourceSnapshot::sha256(&self) -> radroots_storage::backup::MemberDigest
+pub struct radroots_storage_sqlite::legacy::LegacyStudioHandoff
+impl radroots_storage_sqlite::legacy::LegacyStudioHandoff
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoff::byte_length(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoff::catalog_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub fn radroots_storage_sqlite::legacy::LegacyStudioHandoff::evidence_path(&self) -> &std::path::Path
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoff::handoff_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoff::import_id(&self) -> radroots_storage_sqlite::legacy::LegacyImportId
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoff::source_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub struct radroots_storage_sqlite::legacy::LegacyStudioHandoffReceipt
+impl radroots_storage_sqlite::legacy::LegacyStudioHandoffReceipt
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoffReceipt::handoff_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoffReceipt::host_commitment_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub const fn radroots_storage_sqlite::legacy::LegacyStudioHandoffReceipt::new(radroots_storage::backup::MemberDigest, radroots_storage::backup::MemberDigest) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub struct radroots_storage_sqlite::legacy::PreparedLegacyImport
+impl radroots_storage_sqlite::legacy::PreparedLegacyImport
+pub fn radroots_storage_sqlite::legacy::PreparedLegacyImport::bundle_path(&self) -> &std::path::Path
+pub const fn radroots_storage_sqlite::legacy::PreparedLegacyImport::import_id(&self) -> radroots_storage_sqlite::legacy::LegacyImportId
+pub const fn radroots_storage_sqlite::legacy::PreparedLegacyImport::manifest_byte_length(&self) -> u64
+pub const fn radroots_storage_sqlite::legacy::PreparedLegacyImport::manifest_sha256(&self) -> radroots_storage::backup::MemberDigest
+pub fn radroots_storage_sqlite::legacy::PreparedLegacyImport::snapshots(&self) -> &[radroots_storage_sqlite::legacy::LegacySourceSnapshot]
+pub const fn radroots_storage_sqlite::legacy::PreparedLegacyImport::target_generation(&self) -> radroots_storage::event::SourceGeneration
+pub const radroots_storage_sqlite::legacy::LEGACY_STAGE_PAGE_LIMIT_MAX: u16
+pub mod radroots_storage_sqlite::lock
+pub mod radroots_storage_sqlite::migration
+pub mod radroots_storage_sqlite::migration::private
+pub struct radroots_storage_sqlite::migration::private::MigrationDescriptor
+impl radroots_storage_sqlite::migration::private::MigrationDescriptor
+pub const fn radroots_storage_sqlite::migration::private::MigrationDescriptor::name(self) -> &'static str
+pub const fn radroots_storage_sqlite::migration::private::MigrationDescriptor::owned_objects(self) -> &'static [&'static str]
+pub const fn radroots_storage_sqlite::migration::private::MigrationDescriptor::up_sha256(self) -> &'static str
+pub const fn radroots_storage_sqlite::migration::private::MigrationDescriptor::version(self) -> u32
+pub const radroots_storage_sqlite::migration::private::CURRENT_VERSION: u32
+pub const radroots_storage_sqlite::migration::private::MIGRATIONS: &[radroots_storage_sqlite::migration::private::MigrationDescriptor]
+pub const radroots_storage_sqlite::migration::private::MINIMUM_VERSION: u32
+pub mod radroots_storage_sqlite::migration::runtime
+pub struct radroots_storage_sqlite::migration::runtime::MigrationDescriptor
+impl radroots_storage_sqlite::migration::runtime::MigrationDescriptor
+pub const fn radroots_storage_sqlite::migration::runtime::MigrationDescriptor::name(self) -> &'static str
+pub const fn radroots_storage_sqlite::migration::runtime::MigrationDescriptor::owned_objects(self) -> &'static [&'static str]
+pub const fn radroots_storage_sqlite::migration::runtime::MigrationDescriptor::up_sha256(self) -> &'static str
+pub const fn radroots_storage_sqlite::migration::runtime::MigrationDescriptor::version(self) -> u32
+pub const radroots_storage_sqlite::migration::runtime::CURRENT_VERSION: u32
+pub const radroots_storage_sqlite::migration::runtime::MIGRATIONS: &[radroots_storage_sqlite::migration::runtime::MigrationDescriptor]
+pub const radroots_storage_sqlite::migration::runtime::MINIMUM_VERSION: u32
+pub struct radroots_storage_sqlite::migration::AuthoredV10Preflight
+impl radroots_storage_sqlite::migration::AuthoredV10Preflight
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::attempt_count(&self) -> u64
+pub fn radroots_storage_sqlite::migration::AuthoredV10Preflight::blocked_operation_ids(&self) -> &[[u8; 16]]
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::event_count(&self) -> u64
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::importable_count(&self) -> u64
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::invalid_or_unsupported(&self) -> u64
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::is_eligible(&self) -> bool
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::operation_count(&self) -> u64
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::outbox_count(&self) -> u64
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::prepared_or_recoverable(&self) -> u64
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::signed_without_complete_event(&self) -> u64
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::source_digest(&self) -> &[u8; 32]
+pub const fn radroots_storage_sqlite::migration::AuthoredV10Preflight::target_count(&self) -> u64
+pub async fn radroots_storage_sqlite::migration::preflight_authored_v10(&radroots_storage_sqlite::open::Paths) -> core::result::Result<radroots_storage_sqlite::migration::AuthoredV10Preflight, radroots_storage_sqlite::open::Error>
+pub mod radroots_storage_sqlite::open
+#[non_exhaustive] pub enum radroots_storage_sqlite::open::Error
+pub radroots_storage_sqlite::open::Error::AuthoredMigrationBlocked
+pub radroots_storage_sqlite::open::Error::AuthoredMigrationBlocked::invalid_or_unsupported: u64
+pub radroots_storage_sqlite::open::Error::AuthoredMigrationBlocked::prepared_or_recoverable: u64
+pub radroots_storage_sqlite::open::Error::AuthoredMigrationBlocked::signed_without_complete_event: u64
+pub radroots_storage_sqlite::open::Error::BackupBackendUnavailable
+pub radroots_storage_sqlite::open::Error::BackupBundleAlreadyExists(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::BackupBundleMissing(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::BackupCaptureFailed
+pub radroots_storage_sqlite::open::Error::BackupCaptureFailed::member: &'static str
+pub radroots_storage_sqlite::open::Error::BackupFilesystem
+pub radroots_storage_sqlite::open::Error::BackupFilesystem::operation: &'static str
+pub radroots_storage_sqlite::open::Error::BackupFilesystem::source: core::io::error::Error
+pub radroots_storage_sqlite::open::Error::BackupRootRequired
+pub radroots_storage_sqlite::open::Error::BackupUnexpectedEntry(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::BackupVerificationFailed
+pub radroots_storage_sqlite::open::Error::BackupVerificationFailed::member: &'static str
+pub radroots_storage_sqlite::open::Error::ConnectionPolicyMismatch
+pub radroots_storage_sqlite::open::Error::ConnectionPolicyMismatch::database: &'static str
+pub radroots_storage_sqlite::open::Error::CorruptSourceGeneration
+pub radroots_storage_sqlite::open::Error::DatabaseCloseFailed
+pub radroots_storage_sqlite::open::Error::DatabaseCloseFailed::database: &'static str
+pub radroots_storage_sqlite::open::Error::DatabaseCorrupt
+pub radroots_storage_sqlite::open::Error::DatabaseCorrupt::database: &'static str
+pub radroots_storage_sqlite::open::Error::DatabaseOpenFailed
+pub radroots_storage_sqlite::open::Error::DatabaseOpenFailed::database: &'static str
+pub radroots_storage_sqlite::open::Error::Inspect
+pub radroots_storage_sqlite::open::Error::Inspect::path: std::path::PathBuf
+pub radroots_storage_sqlite::open::Error::Inspect::source: core::io::error::Error
+pub radroots_storage_sqlite::open::Error::InvalidBackupRoot(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::InvalidBusyTimeout
+pub radroots_storage_sqlite::open::Error::InvalidBusyTimeout::actual: core::time::Duration
+pub radroots_storage_sqlite::open::Error::InvalidBusyTimeout::maximum: core::time::Duration
+pub radroots_storage_sqlite::open::Error::InvalidBusyTimeout::minimum: core::time::Duration
+pub radroots_storage_sqlite::open::Error::InvalidLegacyImportJournal
+pub radroots_storage_sqlite::open::Error::InvalidLegacyImportPlan
+pub radroots_storage_sqlite::open::Error::InvalidLegacyImportStageRequest
+pub radroots_storage_sqlite::open::Error::InvalidLegacySource(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::InvalidPath(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::InvalidSourceGenerationTimestamp
+pub radroots_storage_sqlite::open::Error::InvalidSourceGenerationTimestamp::actual: u64
+pub radroots_storage_sqlite::open::Error::LegacyImportBackupAlreadyExists(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::LegacyImportBackupFailed
+pub radroots_storage_sqlite::open::Error::LegacyImportBackupFailed::source_kind: &'static str
+pub radroots_storage_sqlite::open::Error::LegacyImportConflict
+pub radroots_storage_sqlite::open::Error::LegacyImportEvidenceInvalid
+pub radroots_storage_sqlite::open::Error::LegacyImportFilesystem
+pub radroots_storage_sqlite::open::Error::LegacyImportFilesystem::operation: &'static str
+pub radroots_storage_sqlite::open::Error::LegacyImportFilesystem::source: core::io::error::Error
+pub radroots_storage_sqlite::open::Error::LegacyImportJournalFailed
+pub radroots_storage_sqlite::open::Error::LegacyImportMigrationHistoryInvalid
+pub radroots_storage_sqlite::open::Error::LegacyImportRowInvalid
+pub radroots_storage_sqlite::open::Error::LegacyImportRowInvalid::legacy_sequence: i64
+pub radroots_storage_sqlite::open::Error::LegacyImportRowInvalid::source_kind: &'static str
+pub radroots_storage_sqlite::open::Error::LegacyImportSourceInvalid
+pub radroots_storage_sqlite::open::Error::LegacyImportSourceInvalid::source_kind: &'static str
+pub radroots_storage_sqlite::open::Error::LegacyImportStagingFailed
+pub radroots_storage_sqlite::open::Error::LegacyImportTargetMismatch
+pub radroots_storage_sqlite::open::Error::MissingFile(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::MissingParent(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::NotAFile(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::ParentNotDirectory(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::PathsOverlap(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::RestoreFilesystem
+pub radroots_storage_sqlite::open::Error::RestoreFilesystem::operation: &'static str
+pub radroots_storage_sqlite::open::Error::RestoreFilesystem::source: core::io::error::Error
+pub radroots_storage_sqlite::open::Error::RestoreMarkerCorrupt(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::RestoreRecoveryConflict(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::RestoreReplacementFailed
+pub radroots_storage_sqlite::open::Error::RestoreReplacementFailed::member: &'static str
+pub radroots_storage_sqlite::open::Error::RestoreRequiresWritableStorage
+pub radroots_storage_sqlite::open::Error::RestoreStagingAlreadyExists(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::RestoreStagingFailed
+pub radroots_storage_sqlite::open::Error::RestoreStagingFailed::member: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaCatalogMismatch
+pub radroots_storage_sqlite::open::Error::SchemaCatalogMismatch::database: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaCatalogMismatch::version: u32
+pub radroots_storage_sqlite::open::Error::SchemaIdentityMismatch
+pub radroots_storage_sqlite::open::Error::SchemaIdentityMismatch::actual: u32
+pub radroots_storage_sqlite::open::Error::SchemaIdentityMismatch::database: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaIdentityMismatch::expected: u32
+pub radroots_storage_sqlite::open::Error::SchemaMetadataUnavailable
+pub radroots_storage_sqlite::open::Error::SchemaMetadataUnavailable::database: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaMigrationFailed
+pub radroots_storage_sqlite::open::Error::SchemaMigrationFailed::database: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaMigrationFailed::target_version: u32
+pub radroots_storage_sqlite::open::Error::SchemaMigrationRequired
+pub radroots_storage_sqlite::open::Error::SchemaMigrationRequired::actual: u32
+pub radroots_storage_sqlite::open::Error::SchemaMigrationRequired::current: u32
+pub radroots_storage_sqlite::open::Error::SchemaMigrationRequired::database: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaTooNew
+pub radroots_storage_sqlite::open::Error::SchemaTooNew::actual: u32
+pub radroots_storage_sqlite::open::Error::SchemaTooNew::database: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaTooNew::supported: u32
+pub radroots_storage_sqlite::open::Error::SchemaTooOld
+pub radroots_storage_sqlite::open::Error::SchemaTooOld::actual: u32
+pub radroots_storage_sqlite::open::Error::SchemaTooOld::database: &'static str
+pub radroots_storage_sqlite::open::Error::SchemaTooOld::minimum: u32
+pub radroots_storage_sqlite::open::Error::SourceGenerationMismatch
+pub radroots_storage_sqlite::open::Error::SourceGenerationRequired
+pub radroots_storage_sqlite::open::Error::SourceGenerationUnavailable
+pub radroots_storage_sqlite::open::Error::SymlinkPath(std::path::PathBuf)
+pub radroots_storage_sqlite::open::Error::UnexpectedFileName
+pub radroots_storage_sqlite::open::Error::UnexpectedFileName::expected: &'static str
+pub radroots_storage_sqlite::open::Error::UnexpectedFileName::path: std::path::PathBuf
+pub radroots_storage_sqlite::open::Error::UnrecognizedSchema
+pub radroots_storage_sqlite::open::Error::UnrecognizedSchema::database: &'static str
+pub radroots_storage_sqlite::open::Error::UnsupportedBackupVersion
+pub radroots_storage_sqlite::open::Error::UnsupportedLegacySchema
+pub radroots_storage_sqlite::open::Error::UnsupportedLegacySchema::catalog_sha256: alloc::string::String
+pub radroots_storage_sqlite::open::Error::UnsupportedLegacySchema::source_kind: &'static str
+pub radroots_storage_sqlite::open::Error::UnsupportedLegacySchema::user_version: i64
+pub radroots_storage_sqlite::open::Error::WriterAlreadyActive
+pub radroots_storage_sqlite::open::Error::WriterAlreadyActive::path: std::path::PathBuf
+pub radroots_storage_sqlite::open::Error::WriterLockFailed
+pub radroots_storage_sqlite::open::Error::WriterLockFailed::path: std::path::PathBuf
+pub radroots_storage_sqlite::open::Error::WriterLockFailed::source: core::io::error::Error
+pub radroots_storage_sqlite::open::Error::WriterLockOpen
+pub radroots_storage_sqlite::open::Error::WriterLockOpen::path: std::path::PathBuf
+pub radroots_storage_sqlite::open::Error::WriterLockOpen::source: core::io::error::Error
+pub radroots_storage_sqlite::open::Error::WriterUnlockFailed
+pub radroots_storage_sqlite::open::Error::WriterUnlockFailed::path: std::path::PathBuf
+pub radroots_storage_sqlite::open::Error::WriterUnlockFailed::source: core::io::error::Error
+impl core::error::Error for radroots_storage_sqlite::open::Error
+pub fn radroots_storage_sqlite::open::Error::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)>
+impl core::fmt::Display for radroots_storage_sqlite::open::Error
+pub fn radroots_storage_sqlite::open::Error::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+#[non_exhaustive] pub enum radroots_storage_sqlite::open::OpenMode
+pub radroots_storage_sqlite::open::OpenMode::Create
+pub radroots_storage_sqlite::open::OpenMode::ReadOnly
+pub radroots_storage_sqlite::open::OpenMode::ReadWriteExisting
+impl radroots_storage_sqlite::open::OpenMode
+pub fn radroots_storage_sqlite::open::OpenMode::is_writable(self) -> bool
+pub fn radroots_storage_sqlite::open::OpenMode::may_create(self) -> bool
+pub struct radroots_storage_sqlite::open::Paths
+impl radroots_storage_sqlite::open::Paths
+pub fn radroots_storage_sqlite::open::Paths::from_directory(impl core::convert::AsRef<std::path::Path>) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::open::Paths::from_files(impl core::convert::Into<std::path::PathBuf>, impl core::convert::Into<std::path::PathBuf>) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::open::Paths::private(&self) -> &std::path::Path
+pub fn radroots_storage_sqlite::open::Paths::runtime(&self) -> &std::path::Path
+pub mod radroots_storage_sqlite::status
+#[non_exhaustive] pub enum radroots_storage_sqlite::Error
+pub radroots_storage_sqlite::Error::AuthoredMigrationBlocked
+pub radroots_storage_sqlite::Error::AuthoredMigrationBlocked::invalid_or_unsupported: u64
+pub radroots_storage_sqlite::Error::AuthoredMigrationBlocked::prepared_or_recoverable: u64
+pub radroots_storage_sqlite::Error::AuthoredMigrationBlocked::signed_without_complete_event: u64
+pub radroots_storage_sqlite::Error::BackupBackendUnavailable
+pub radroots_storage_sqlite::Error::BackupBundleAlreadyExists(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::BackupBundleMissing(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::BackupCaptureFailed
+pub radroots_storage_sqlite::Error::BackupCaptureFailed::member: &'static str
+pub radroots_storage_sqlite::Error::BackupFilesystem
+pub radroots_storage_sqlite::Error::BackupFilesystem::operation: &'static str
+pub radroots_storage_sqlite::Error::BackupFilesystem::source: core::io::error::Error
+pub radroots_storage_sqlite::Error::BackupRootRequired
+pub radroots_storage_sqlite::Error::BackupUnexpectedEntry(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::BackupVerificationFailed
+pub radroots_storage_sqlite::Error::BackupVerificationFailed::member: &'static str
+pub radroots_storage_sqlite::Error::ConnectionPolicyMismatch
+pub radroots_storage_sqlite::Error::ConnectionPolicyMismatch::database: &'static str
+pub radroots_storage_sqlite::Error::CorruptSourceGeneration
+pub radroots_storage_sqlite::Error::DatabaseCloseFailed
+pub radroots_storage_sqlite::Error::DatabaseCloseFailed::database: &'static str
+pub radroots_storage_sqlite::Error::DatabaseCorrupt
+pub radroots_storage_sqlite::Error::DatabaseCorrupt::database: &'static str
+pub radroots_storage_sqlite::Error::DatabaseOpenFailed
+pub radroots_storage_sqlite::Error::DatabaseOpenFailed::database: &'static str
+pub radroots_storage_sqlite::Error::Inspect
+pub radroots_storage_sqlite::Error::Inspect::path: std::path::PathBuf
+pub radroots_storage_sqlite::Error::Inspect::source: core::io::error::Error
+pub radroots_storage_sqlite::Error::InvalidBackupRoot(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::InvalidBusyTimeout
+pub radroots_storage_sqlite::Error::InvalidBusyTimeout::actual: core::time::Duration
+pub radroots_storage_sqlite::Error::InvalidBusyTimeout::maximum: core::time::Duration
+pub radroots_storage_sqlite::Error::InvalidBusyTimeout::minimum: core::time::Duration
+pub radroots_storage_sqlite::Error::InvalidLegacyImportJournal
+pub radroots_storage_sqlite::Error::InvalidLegacyImportPlan
+pub radroots_storage_sqlite::Error::InvalidLegacyImportStageRequest
+pub radroots_storage_sqlite::Error::InvalidLegacySource(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::InvalidPath(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::InvalidSourceGenerationTimestamp
+pub radroots_storage_sqlite::Error::InvalidSourceGenerationTimestamp::actual: u64
+pub radroots_storage_sqlite::Error::LegacyImportBackupAlreadyExists(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::LegacyImportBackupFailed
+pub radroots_storage_sqlite::Error::LegacyImportBackupFailed::source_kind: &'static str
+pub radroots_storage_sqlite::Error::LegacyImportConflict
+pub radroots_storage_sqlite::Error::LegacyImportEvidenceInvalid
+pub radroots_storage_sqlite::Error::LegacyImportFilesystem
+pub radroots_storage_sqlite::Error::LegacyImportFilesystem::operation: &'static str
+pub radroots_storage_sqlite::Error::LegacyImportFilesystem::source: core::io::error::Error
+pub radroots_storage_sqlite::Error::LegacyImportJournalFailed
+pub radroots_storage_sqlite::Error::LegacyImportMigrationHistoryInvalid
+pub radroots_storage_sqlite::Error::LegacyImportRowInvalid
+pub radroots_storage_sqlite::Error::LegacyImportRowInvalid::legacy_sequence: i64
+pub radroots_storage_sqlite::Error::LegacyImportRowInvalid::source_kind: &'static str
+pub radroots_storage_sqlite::Error::LegacyImportSourceInvalid
+pub radroots_storage_sqlite::Error::LegacyImportSourceInvalid::source_kind: &'static str
+pub radroots_storage_sqlite::Error::LegacyImportStagingFailed
+pub radroots_storage_sqlite::Error::LegacyImportTargetMismatch
+pub radroots_storage_sqlite::Error::MissingFile(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::MissingParent(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::NotAFile(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::ParentNotDirectory(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::PathsOverlap(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::RestoreFilesystem
+pub radroots_storage_sqlite::Error::RestoreFilesystem::operation: &'static str
+pub radroots_storage_sqlite::Error::RestoreFilesystem::source: core::io::error::Error
+pub radroots_storage_sqlite::Error::RestoreMarkerCorrupt(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::RestoreRecoveryConflict(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::RestoreReplacementFailed
+pub radroots_storage_sqlite::Error::RestoreReplacementFailed::member: &'static str
+pub radroots_storage_sqlite::Error::RestoreRequiresWritableStorage
+pub radroots_storage_sqlite::Error::RestoreStagingAlreadyExists(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::RestoreStagingFailed
+pub radroots_storage_sqlite::Error::RestoreStagingFailed::member: &'static str
+pub radroots_storage_sqlite::Error::SchemaCatalogMismatch
+pub radroots_storage_sqlite::Error::SchemaCatalogMismatch::database: &'static str
+pub radroots_storage_sqlite::Error::SchemaCatalogMismatch::version: u32
+pub radroots_storage_sqlite::Error::SchemaIdentityMismatch
+pub radroots_storage_sqlite::Error::SchemaIdentityMismatch::actual: u32
+pub radroots_storage_sqlite::Error::SchemaIdentityMismatch::database: &'static str
+pub radroots_storage_sqlite::Error::SchemaIdentityMismatch::expected: u32
+pub radroots_storage_sqlite::Error::SchemaMetadataUnavailable
+pub radroots_storage_sqlite::Error::SchemaMetadataUnavailable::database: &'static str
+pub radroots_storage_sqlite::Error::SchemaMigrationFailed
+pub radroots_storage_sqlite::Error::SchemaMigrationFailed::database: &'static str
+pub radroots_storage_sqlite::Error::SchemaMigrationFailed::target_version: u32
+pub radroots_storage_sqlite::Error::SchemaMigrationRequired
+pub radroots_storage_sqlite::Error::SchemaMigrationRequired::actual: u32
+pub radroots_storage_sqlite::Error::SchemaMigrationRequired::current: u32
+pub radroots_storage_sqlite::Error::SchemaMigrationRequired::database: &'static str
+pub radroots_storage_sqlite::Error::SchemaTooNew
+pub radroots_storage_sqlite::Error::SchemaTooNew::actual: u32
+pub radroots_storage_sqlite::Error::SchemaTooNew::database: &'static str
+pub radroots_storage_sqlite::Error::SchemaTooNew::supported: u32
+pub radroots_storage_sqlite::Error::SchemaTooOld
+pub radroots_storage_sqlite::Error::SchemaTooOld::actual: u32
+pub radroots_storage_sqlite::Error::SchemaTooOld::database: &'static str
+pub radroots_storage_sqlite::Error::SchemaTooOld::minimum: u32
+pub radroots_storage_sqlite::Error::SourceGenerationMismatch
+pub radroots_storage_sqlite::Error::SourceGenerationRequired
+pub radroots_storage_sqlite::Error::SourceGenerationUnavailable
+pub radroots_storage_sqlite::Error::SymlinkPath(std::path::PathBuf)
+pub radroots_storage_sqlite::Error::UnexpectedFileName
+pub radroots_storage_sqlite::Error::UnexpectedFileName::expected: &'static str
+pub radroots_storage_sqlite::Error::UnexpectedFileName::path: std::path::PathBuf
+pub radroots_storage_sqlite::Error::UnrecognizedSchema
+pub radroots_storage_sqlite::Error::UnrecognizedSchema::database: &'static str
+pub radroots_storage_sqlite::Error::UnsupportedBackupVersion
+pub radroots_storage_sqlite::Error::UnsupportedLegacySchema
+pub radroots_storage_sqlite::Error::UnsupportedLegacySchema::catalog_sha256: alloc::string::String
+pub radroots_storage_sqlite::Error::UnsupportedLegacySchema::source_kind: &'static str
+pub radroots_storage_sqlite::Error::UnsupportedLegacySchema::user_version: i64
+pub radroots_storage_sqlite::Error::WriterAlreadyActive
+pub radroots_storage_sqlite::Error::WriterAlreadyActive::path: std::path::PathBuf
+pub radroots_storage_sqlite::Error::WriterLockFailed
+pub radroots_storage_sqlite::Error::WriterLockFailed::path: std::path::PathBuf
+pub radroots_storage_sqlite::Error::WriterLockFailed::source: core::io::error::Error
+pub radroots_storage_sqlite::Error::WriterLockOpen
+pub radroots_storage_sqlite::Error::WriterLockOpen::path: std::path::PathBuf
+pub radroots_storage_sqlite::Error::WriterLockOpen::source: core::io::error::Error
+pub radroots_storage_sqlite::Error::WriterUnlockFailed
+pub radroots_storage_sqlite::Error::WriterUnlockFailed::path: std::path::PathBuf
+pub radroots_storage_sqlite::Error::WriterUnlockFailed::source: core::io::error::Error
+impl core::error::Error for radroots_storage_sqlite::open::Error
+pub fn radroots_storage_sqlite::open::Error::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)>
+impl core::fmt::Display for radroots_storage_sqlite::open::Error
+pub fn radroots_storage_sqlite::open::Error::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+#[non_exhaustive] pub enum radroots_storage_sqlite::OpenMode
+pub radroots_storage_sqlite::OpenMode::Create
+pub radroots_storage_sqlite::OpenMode::ReadOnly
+pub radroots_storage_sqlite::OpenMode::ReadWriteExisting
+impl radroots_storage_sqlite::open::OpenMode
+pub fn radroots_storage_sqlite::open::OpenMode::is_writable(self) -> bool
+pub fn radroots_storage_sqlite::open::OpenMode::may_create(self) -> bool
+pub struct radroots_storage_sqlite::OpenOptions
+impl radroots_storage_sqlite::config::OpenOptions
+pub fn radroots_storage_sqlite::config::OpenOptions::backup_root(&self) -> core::option::Option<&std::path::Path>
+pub fn radroots_storage_sqlite::config::OpenOptions::busy_timeout(&self) -> core::time::Duration
+pub fn radroots_storage_sqlite::config::OpenOptions::foreign_keys_enabled(&self) -> bool
+pub fn radroots_storage_sqlite::config::OpenOptions::mode(&self) -> radroots_storage_sqlite::open::OpenMode
+pub fn radroots_storage_sqlite::config::OpenOptions::new(radroots_storage_sqlite::open::Paths, radroots_storage_sqlite::open::OpenMode) -> Self
+pub fn radroots_storage_sqlite::config::OpenOptions::paths(&self) -> &radroots_storage_sqlite::open::Paths
+pub fn radroots_storage_sqlite::config::OpenOptions::source_generation(&self) -> core::option::Option<radroots_storage::event::SourceGeneration>
+pub fn radroots_storage_sqlite::config::OpenOptions::source_generation_created_at_unix_ms(&self) -> core::option::Option<u64>
+pub fn radroots_storage_sqlite::config::OpenOptions::validate_filesystem(&self) -> core::result::Result<(), radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::wal_enabled(&self) -> bool
+pub fn radroots_storage_sqlite::config::OpenOptions::with_backup_root(self, impl core::convert::Into<std::path::PathBuf>) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::with_busy_timeout(self, core::time::Duration) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::with_source_generation(self, radroots_storage::event::SourceGeneration, u64) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::config::OpenOptions::writer_policy(&self) -> radroots_storage::status::WriterPolicy
+pub struct radroots_storage_sqlite::Paths
+impl radroots_storage_sqlite::open::Paths
+pub fn radroots_storage_sqlite::open::Paths::from_directory(impl core::convert::AsRef<std::path::Path>) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::open::Paths::from_files(impl core::convert::Into<std::path::PathBuf>, impl core::convert::Into<std::path::PathBuf>) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+pub fn radroots_storage_sqlite::open::Paths::private(&self) -> &std::path::Path
+pub fn radroots_storage_sqlite::open::Paths::runtime(&self) -> &std::path::Path
+pub struct radroots_storage_sqlite::SqliteStorage
+impl radroots_storage_sqlite::SqliteStorage
+pub async fn radroots_storage_sqlite::SqliteStorage::acknowledge_legacy_studio_handoff(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport, radroots_storage_sqlite::legacy::LegacyStudioHandoffReceipt, u64) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyImportJournal, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::begin_legacy_import(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport, u64) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyImportJournal, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::classify_legacy_import(&self, &radroots_storage_sqlite::legacy::PreparedLegacyImport) -> core::result::Result<radroots_storage_sqlite::legacy::ClassifiedLegacyImport, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::finalize_legacy_import(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport, radroots_storage_sqlite::legacy::LegacyImportValidation, u64) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyImportCommitReceipt, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::legacy_import_journal(&self, radroots_storage_sqlite::legacy::LegacyImportId) -> core::result::Result<core::option::Option<radroots_storage_sqlite::legacy::LegacyImportJournal>, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::prepare_legacy_import(&self, &radroots_storage_sqlite::legacy::LegacyImportPlan) -> core::result::Result<radroots_storage_sqlite::legacy::PreparedLegacyImport, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::prepare_legacy_studio_handoff(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyStudioHandoff, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::stage_legacy_events(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport, u16, u64) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyEventStagePage, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::stage_legacy_outbox(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport, u16, u64) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyOutboxStagePage, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::stage_legacy_private(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport, u16, u64) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyPrivateStagePage, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::validate_legacy_import(&self, &radroots_storage_sqlite::legacy::ClassifiedLegacyImport) -> core::result::Result<radroots_storage_sqlite::legacy::LegacyImportValidation, radroots_storage_sqlite::open::Error>
+impl radroots_storage_sqlite::SqliteStorage
+pub async fn radroots_storage_sqlite::SqliteStorage::capture_backup(&self, &radroots_storage::backup::BackupPlan) -> core::result::Result<radroots_storage::backup::BackupManifest, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::finalize_backup(&self, &radroots_storage::backup::BackupPlan, &radroots_storage::backup::BackupManifest) -> core::result::Result<std::path::PathBuf, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::finalize_restore(&self, &radroots_storage::backup::RestorePlan) -> core::result::Result<(), radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::stage_restore(&self, &radroots_storage::backup::RestorePlan) -> core::result::Result<alloc::vec::Vec<radroots_storage::backup::RestoreMemberStatus>, radroots_storage_sqlite::open::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::verify_backup(&self, &radroots_storage::backup::BackupPlan, &radroots_storage::backup::BackupManifest) -> core::result::Result<(), radroots_storage_sqlite::open::Error>
+impl radroots_storage_sqlite::SqliteStorage
+pub async fn radroots_storage_sqlite::SqliteStorage::check_integrity(&self, u64) -> core::result::Result<radroots_storage::status::IntegrityStatus, radroots_storage::error::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::integrity(&self) -> core::result::Result<radroots_storage::status::IntegrityStatus, radroots_storage::error::Error>
+impl radroots_storage_sqlite::SqliteStorage
+pub async fn radroots_storage_sqlite::SqliteStorage::close(&self) -> core::result::Result<radroots_storage::status::StorageStatus, radroots_storage::error::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::storage_status(&self) -> core::result::Result<radroots_storage::status::StorageStatus, radroots_storage::error::Error>
+impl radroots_storage_sqlite::SqliteStorage
+pub async fn radroots_storage_sqlite::SqliteStorage::commit_private_artifact_reseal(&self, radroots_storage::private_artifact::PrivateArtifactResealRequest, &radroots_secrets::envelope::EncryptedEnvelope) -> core::result::Result<radroots_storage::private_artifact::PrivateArtifactResealReceipt, radroots_storage::error::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::encrypted_private_artifact(&self, radroots_storage::private_artifact::PrivateArtifactId) -> core::result::Result<core::option::Option<radroots_secrets::envelope::EncryptedEnvelope>, radroots_storage::error::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::private_artifact_envelope_migration_status(&self) -> core::result::Result<radroots_storage::private_artifact::PrivateArtifactEnvelopeMigrationStatus, radroots_storage::error::Error>
+pub async fn radroots_storage_sqlite::SqliteStorage::put_encrypted_private_artifact(&self, radroots_storage::private_artifact::PrivateArtifactMetadata, &radroots_secrets::envelope::EncryptedEnvelope) -> core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::error::Error>
+impl radroots_storage_sqlite::SqliteStorage
+pub async fn radroots_storage_sqlite::SqliteStorage::open(radroots_storage_sqlite::config::OpenOptions) -> core::result::Result<Self, radroots_storage_sqlite::open::Error>
+impl radroots_storage::atomic::AtomicStorage for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::commit(&self, radroots_storage::atomic::AtomicCommit) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::atomic::AtomicCommitReceipt, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::receipt(&self, radroots_storage::atomic::AtomicCommitId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::atomic::AtomicCommitReceipt>, radroots_storage::error::Error>>
+impl radroots_storage::authored_atomic::AuthoredAtomicStorage for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::authored_artifact(&self, radroots_storage::authored::AuthoredArtifactId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::authored::AuthoredArtifact>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::authored_delivery_plan(&self, radroots_storage::authored_delivery::AuthoredDeliveryPlanId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::authored_delivery::AuthoredDeliveryPlan>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::authored_operation(&self, radroots_storage::journal::OperationInstanceId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::authored::AuthoredOperation>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::authored_receipt(&self, radroots_storage::atomic::AtomicCommitId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::authored_atomic::AuthoredAtomicReceipt>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::execute_authored(&self, radroots_storage::authored_atomic::AuthoredAtomicCommand) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::authored_atomic::AuthoredAtomicReceipt, radroots_storage::error::Error>>
+impl radroots_storage::authored_draft::AuthoredDraftStore for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::append_authored_draft(&self, radroots_storage::authored_draft::AuthoredDraft, core::option::Option<radroots_storage::authored_draft::AuthoredDraftRevision>) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::authored_draft::DraftAppendReceipt, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::authored_draft_head(&self, radroots_storage::authored_draft::AuthoredDraftId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::authored_draft::AuthoredDraft>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::authored_draft_heads(&self, [u8; 32], u16) -> radroots_transport::source::BoxFuture<'_, core::result::Result<alloc::vec::Vec<radroots_storage::authored_draft::AuthoredDraft>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::authored_draft_revision(&self, radroots_storage::authored_draft::AuthoredDraftId, radroots_storage::authored_draft::AuthoredDraftRevision) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::authored_draft::AuthoredDraft>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::query_authored_drafts(&self, radroots_storage::authored_draft_query::AuthoredDraftQuery) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::authored_draft_query::AuthoredDraftPage, radroots_storage::error::Error>>
+impl radroots_storage::backup::StorageReliability for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::begin_backup(&self, radroots_storage::backup::BackupPlan) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::backup::BackupOperation, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::begin_restore(&self, radroots_storage::backup::RestorePlan) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::backup::RestoreOperation, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::close(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::status::StorageStatus, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::integrity(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::status::IntegrityStatus, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::status::StorageStatus, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::transition_backup(&self, radroots_storage::backup::BackupId, radroots_storage::backup::ReliabilityRevision, radroots_storage::backup::BackupTransition, u64) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::backup::BackupOperation, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::transition_restore(&self, radroots_storage::backup::BackupId, radroots_storage::backup::ReliabilityRevision, radroots_storage::backup::RestoreTransition, u64) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::backup::RestoreOperation, radroots_storage::error::Error>>
+impl radroots_storage::event::EventStore for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::admit(&self, radroots_storage::event::EventAdmission) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::event::AdmissionReceipt, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::query_provenance(&self, radroots_event::id::EventId, radroots_storage::event::EventQueryBounds) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::event::EventPage<radroots_storage::event::StoredEventProvenance>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::query_raw(&self, radroots_storage::event::EventQuery) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::event::EventPage<radroots_storage::event::StoredRawEvent>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::query_verified(&self, radroots_storage::event::EventQuery) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::event::EventPage<radroots_storage::event::StoredVerifiedEvent>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::query_visible(&self, radroots_storage::event::EventQuery) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::event::EventPage<radroots_storage::event::StoredVisibleEvent>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::rebuild_visibility(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::event::VisibilitySnapshot, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::status::EventStoreStatus, radroots_storage::error::Error>>
+impl radroots_storage::journal::Journal for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::by_idempotency_key(&self, radroots_protocol::runtime::v1::OperationId, radroots_storage::journal::IdempotencyKey) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::journal::OperationRecord>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::operation(&self, radroots_storage::journal::OperationInstanceId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::journal::OperationRecord>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::prepare(&self, radroots_storage::journal::PrepareOperation) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::journal::PrepareReceipt, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::recoverable(&self, u16) -> radroots_transport::source::BoxFuture<'_, core::result::Result<alloc::vec::Vec<radroots_storage::journal::OperationRecord>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::transition(&self, radroots_storage::journal::JournalTransition) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::journal::OperationRecord, radroots_storage::error::Error>>
+impl radroots_storage::outbox::Outbox for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::claim(&self, radroots_storage::outbox::ClaimOutboxItems) -> radroots_transport::source::BoxFuture<'_, core::result::Result<alloc::vec::Vec<radroots_storage::outbox::ClaimedOutboxItem>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::enqueue(&self, radroots_storage::outbox::EnqueueOutboxItem) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::outbox::EnqueueReceipt, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::item(&self, radroots_storage::outbox::OutboxItemId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::outbox::OutboxRecord>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::record_attempt(&self, radroots_storage::outbox::DeliveryAttemptEvidence) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::outbox::OutboxRecord, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::release(&self, radroots_storage::outbox::OutboxItemId, radroots_storage::outbox::LeaseId, radroots_storage::outbox::OutboxRevision, u64, core::option::Option<u64>) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::outbox::OutboxRecord, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::outbox::OutboxStatus, radroots_storage::error::Error>>
+impl radroots_storage::private_artifact::PrivateArtifactStore for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::expired(&self, u64, u16) -> radroots_transport::source::BoxFuture<'_, core::result::Result<alloc::vec::Vec<radroots_storage::private_artifact::PrivateArtifactMetadata>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::mark_expired(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::metadata(&self, radroots_storage::private_artifact::PrivateArtifactId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::private_artifact::PrivateArtifactMetadata>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::put_metadata(&self, radroots_storage::private_artifact::PrivateArtifactMetadata) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::reseal_metadata(&self, radroots_storage::private_artifact::PrivateArtifactResealRequest) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactResealReceipt, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactStatus, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::tombstone(&self, radroots_storage::private_artifact::PrivateArtifactId, radroots_storage::private_artifact::PrivateArtifactRevision, u64, radroots_storage::private_artifact::DeletionReason) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_storage::error::Error>>
+impl radroots_storage::projection::ProjectionStore for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::checkpoint(&self, radroots_storage::projection::ProjectionCheckpoint) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::projection::ProjectionStatus, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::event_index_checkpoint(&self, radroots_storage::projection::ProjectionGeneration) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::projection::EventIndexCheckpoint>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::event_index_manifest(&self, radroots_storage::projection::ProjectionGeneration) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::projection::EventIndexManifest>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::invalidate(&self, radroots_storage::projection::ProjectionInvalidation) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::projection::ProjectionStatus, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::invalidation(&self, radroots_storage::projection::ProjectionId, radroots_storage::projection::ProjectionGeneration) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::projection::ProjectionInvalidation>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::projection_document(&self, radroots_storage::projection::ProjectionId, radroots_storage::projection::ProjectionGeneration, alloc::string::String) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::projection::ProjectionDocument>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::projection_snapshot(&self, radroots_storage::projection::ProjectionId, [u8; 32]) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::projection::ProjectionSnapshot>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::put_event_index_checkpoint(&self, radroots_storage::projection::EventIndexCheckpoint) -> radroots_transport::source::BoxFuture<'_, core::result::Result<(), radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::put_event_index_manifest(&self, radroots_storage::projection::EventIndexManifest) -> radroots_transport::source::BoxFuture<'_, core::result::Result<(), radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::put_projection_document(&self, radroots_storage::projection::ProjectionId, radroots_storage::projection::ProjectionGeneration, radroots_storage::projection::ProjectionDocument) -> radroots_transport::source::BoxFuture<'_, core::result::Result<(), radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::put_projection_snapshot(&self, radroots_storage::projection::ProjectionSnapshot) -> radroots_transport::source::BoxFuture<'_, core::result::Result<(), radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::rebuild(&self, radroots_storage::projection::RebuildTicketId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::projection::RebuildTicket>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::request_rebuild(&self, radroots_storage::projection::RebuildTicket) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::projection::RebuildTicket, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::status(&self, radroots_storage::projection::ProjectionId) -> radroots_transport::source::BoxFuture<'_, core::result::Result<core::option::Option<radroots_storage::projection::ProjectionStatus>, radroots_storage::error::Error>>
+pub fn radroots_storage_sqlite::SqliteStorage::transition_rebuild(&self, radroots_storage::projection::RebuildTransition) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::projection::RebuildTicket, radroots_storage::error::Error>>
+impl radroots_storage::status::StorageStatusProvider for radroots_storage_sqlite::SqliteStorage
+pub fn radroots_storage_sqlite::SqliteStorage::storage_status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_storage::status::StorageStatus, radroots_storage::error::Error>>
diff --git a/contracts/architecture/decisions/authored_signed_facts.v1.json b/contracts/architecture/decisions/authored_signed_facts.v1.json
@@ -0,0 +1,22 @@
+{
+ "schema": "radroots.authored-signed-facts.v1",
+ "status": "approved",
+ "owners": [
+ "radroots_storage",
+ "radroots_storage_sqlite"
+ ],
+ "command": "authored_atomic::RecordSignedArtifact; AuthoredAtomicCommand::RecordSigned",
+ "provenance": "The owning transaction retrieves the immutable original signing Claim receipt. Exact operation, artifact, plan, complete claim and original row identity must match; lease expiry is irrelevant to evidence and remains binding on scheduling.",
+ "cryptography": "Verify event ID and Schnorr signature using existing event-codec; compare every retained plan field. No signing dependency, signer call, ambient clock or network.",
+ "immutability": "First exact signed bytes are immutable. Equal bytes from the same or another valid historical attempt are idempotent. Different raw bytes conflict even for the same event ID. The new logical signed_fact_v1 hash includes operation, artifact, full claim and raw event, excluding observation time; existing command hashes remain unchanged.",
+ "stop": "Cancelled and FailedTerminal logical signing states may retain signed bytes. Their admission state stays Pending and cannot acquire admission or delivery claims. Terminal delivery plans remain unchanged. Remaining global delivery stop/attempt semantics are separate.",
+ "time": "Observation time is positive and at least claim acquisition. Artifact row and committed receipt time use the maximum of observation and current artifact row time; the command retains its requested observation. Earlier observations cannot move durable row or commit time backwards. Existing command time rules remain unchanged.",
+ "migration": {
+ "version": 15,
+ "name": "authored_signed_facts",
+ "sha256": "d7ed7312b36f6ae633d2018d71d1bc0117097523ff46ecfaca8a99ca739aa026",
+ "compatibility": "Preserve every prior SQL/checksum, v1-v9 contract, old snapshot and receipt bytes. A nullable signing_stop column projects stopped-plus-signed snapshots as physical signed state plus stop, preserving the v11 CHECK. An update guard preserves first bytes and stops. Prior schema policy rejects version15; prior model validation rejects stopped-plus-signed snapshots."
+ },
+ "atomicity": "Memory commits a validated candidate; SQLite records artifact, eligible delivery bindings and receipt in one BEGIN IMMEDIATE transaction and returns only after COMMIT. Failures roll back all writes.",
+ "consumer": "Sync and concrete hosts must revalidate request identities, reconcile current durable state after receipt replay, and apply cancellation/deadline scheduling policy before further effects."
+}
diff --git a/crates/storage/README.md b/crates/storage/README.md
@@ -85,6 +85,19 @@ local durable commit point:
`AtomicStorage::commit` is the aggregate local workflow boundary. Network
publication is outside this crate and is not implied by either state.
+`AuthoredAtomicCommand::RecordSigned` retains an already-created, cryptographically
+verified signature after its original signing lease expires or is superseded.
+The backend retrieves and checks its immutable signing claim receipt, including
+the full claim, operation, artifact and exact retained plan. This command performs
+no signing and supplies no permission to schedule another phase. Active
+`ApplySigned` and work claims retain their strict fences.
+
+The first exact signed bytes are immutable. Repeated identical evidence is
+idempotent; different raw bytes conflict. Cancelled or terminally failed signing
+may retain valid bytes while preserving its stop and failure state, with no
+admission or delivery claim. Receipt replay returns historical state, so hosts
+must query current durable status before deciding what work may follow.
+
## Events, journal, outbox, and projections
Event storage preserves the exact signed event, verification/admission stage,
diff --git a/crates/storage/src/authored.rs b/crates/storage/src/authored.rs
@@ -722,7 +722,14 @@ impl AuthoredArtifact {
{
return Err(Error::InvalidAuthoredArtifact);
}
- if matches!(self.signing_state, SigningState::Signed) != self.signed.is_some() {
+ let stopped = matches!(
+ self.signing_state,
+ SigningState::Cancelled | SigningState::FailedTerminal
+ );
+ if (self.signing_state == SigningState::Signed && self.signed.is_none())
+ || (self.signed.is_some() && self.signing_state != SigningState::Signed && !stopped)
+ || (stopped && self.admission_state != AdmissionState::Pending)
+ {
return Err(Error::InvalidAuthoredArtifact);
}
if self.signed.is_none() && self.admission_state != AdmissionState::Pending {
@@ -744,6 +751,7 @@ impl AuthoredArtifact {
|| claim.acquired_at_unix_ms() != self.updated_at_unix_ms
}) || self.admission_claim.as_ref().is_some_and(|claim| {
claim.validate().is_err()
+ || self.signing_state != SigningState::Signed
|| self.signed.is_none()
|| !matches!(
self.admission_state,
@@ -806,7 +814,7 @@ impl AuthoredArtifact {
AdmissionState::Retryable | AdmissionState::Rejected | AdmissionState::Cancelled
));
if failure_required {
- if expected_failure != self.last_failure.as_ref() {
+ if expected_failure.is_none() || expected_failure != self.last_failure.as_ref() {
return Err(Error::InvalidAuthoredArtifact);
}
} else if self.last_failure.is_some() {
@@ -858,6 +866,41 @@ impl AuthoredArtifact {
Ok(())
}
+ pub(crate) fn record_signed_fact(
+ &mut self,
+ event: SignedEvent,
+ observed_at_unix_ms: u64,
+ ) -> Result<(), Error> {
+ if !self.origin.is_resignable() {
+ return Err(Error::InvalidAuthoredTransition);
+ }
+ if let Some(existing) = &self.signed {
+ return if existing.event() == &event {
+ Ok(())
+ } else {
+ Err(Error::AtomicCommitConflict)
+ };
+ }
+ let previous = self.clone();
+ self.signed = Some(ExactSignedArtifact::new(event));
+ self.signing_claim = None;
+ self.signing_retry = None;
+ if !matches!(
+ self.signing_state,
+ SigningState::Cancelled | SigningState::FailedTerminal
+ ) {
+ self.signing_state = SigningState::Signed;
+ self.last_failure = None;
+ }
+ // Out-of-order observation must not move row time backwards or erase facts.
+ let at_unix_ms = observed_at_unix_ms.max(self.updated_at_unix_ms);
+ if let Err(error) = self.advance(at_unix_ms).and_then(|()| self.validate()) {
+ *self = previous;
+ return Err(error);
+ }
+ Ok(())
+ }
+
pub fn set_signing_claim(&mut self, claim: WorkClaim, at_unix_ms: u64) -> Result<(), Error> {
let existing_blocks = self.signing_claim.as_ref().is_some_and(|existing| {
at_unix_ms < existing.expires_at_unix_ms()
diff --git a/crates/storage/src/authored_atomic.rs b/crates/storage/src/authored_atomic.rs
@@ -1,5 +1,8 @@
//! Atomic authored-operation commands with deterministic phase identities.
+mod signing_evidence;
+pub use signing_evidence::RecordSignedArtifact;
+
use core::num::NonZeroU64;
use radroots_event::SignedEvent;
use radroots_transport::BoxFuture;
@@ -417,6 +420,7 @@ pub enum AuthoredAtomicCommand {
PrepareFromDraft(Box<PrepareFromDraft>),
Claim(ClaimAuthoredWork),
ApplySigned(ApplySignedArtifact),
+ RecordSigned(RecordSignedArtifact),
ApplyAdmission(ApplyAdmissionResult),
ApplyDelivery(ApplyDeliveryAttempt),
ApplyFailure(ApplyWorkFailure),
@@ -462,6 +466,17 @@ impl AuthoredAtomicCommand {
hasher.update(value.claim.row_revision().get().to_be_bytes());
}
Self::ApplySigned(value) => hash_field(&mut hasher, value.event.raw_json().as_bytes()),
+ Self::RecordSigned(value) => {
+ hash_field(&mut hasher, value.operation_id().as_bytes());
+ let claim = value.claim();
+ hash_field(&mut hasher, claim.token());
+ hash_field(&mut hasher, claim.owner().as_bytes());
+ hasher.update(claim.generation().get().to_be_bytes());
+ hasher.update(claim.row_revision().get().to_be_bytes());
+ hasher.update(claim.acquired_at_unix_ms().to_be_bytes());
+ hasher.update(claim.expires_at_unix_ms().to_be_bytes());
+ hash_field(&mut hasher, value.event().raw_json().as_bytes());
+ }
Self::ApplyAdmission(value) => {
hasher.update([value.state as u8]);
hash_failure(&mut hasher, value.failure.as_ref());
@@ -479,6 +494,7 @@ impl AuthoredAtomicCommand {
Self::Prepare(value) => value.requested_at_unix_ms,
Self::Claim(value) => value.claim.acquired_at_unix_ms(),
Self::ApplySigned(value) => value.applied_at_unix_ms,
+ Self::RecordSigned(value) => value.observed_at_unix_ms(),
Self::ApplyAdmission(value) => value.applied_at_unix_ms,
Self::ApplyDelivery(value) => value.applied_at_unix_ms,
Self::ApplyFailure(value) => value.applied_at_unix_ms,
@@ -492,6 +508,7 @@ impl AuthoredAtomicCommand {
Self::Prepare(_) => b"prepare",
Self::Claim(_) => b"claim",
Self::ApplySigned(_) => b"signing",
+ Self::RecordSigned(_) => b"signed_fact_v1",
Self::ApplyAdmission(_) => b"admission",
Self::ApplyDelivery(_) => b"delivery",
Self::ApplyFailure(value) => match value.failure.phase() {
@@ -515,6 +532,7 @@ impl AuthoredAtomicCommand {
ClaimAuthoredTarget::DeliveryPlan(id) => *id.as_bytes(),
},
Self::ApplySigned(value) => *value.artifact_id.as_bytes(),
+ Self::RecordSigned(value) => *value.artifact_id().as_bytes(),
Self::ApplyAdmission(value) => *value.artifact_id.as_bytes(),
Self::ApplyDelivery(value) => *value.plan_id.as_bytes(),
Self::ApplyFailure(value) => match &value.target {
@@ -532,6 +550,7 @@ impl AuthoredAtomicCommand {
fn generation(&self) -> Option<NonZeroU64> {
match self {
Self::ApplySigned(value) => Some(value.fence.generation),
+ Self::RecordSigned(value) => Some(value.claim().generation()),
Self::Claim(value) => Some(value.claim.generation()),
Self::ApplyAdmission(value) => Some(value.fence.generation),
Self::ApplyDelivery(value) => Some(value.fence.generation),
@@ -572,6 +591,11 @@ impl AuthoredAtomicReceipt {
}
match (command, &self.outcome) {
(
+ AuthoredAtomicCommand::RecordSigned(value),
+ AuthoredAtomicOutcome::Artifact(artifact),
+ ) => signed_fact_matches(value, artifact),
+ (AuthoredAtomicCommand::RecordSigned(_), _) => false,
+ (
AuthoredAtomicCommand::PrepareFromDraft(request),
AuthoredAtomicOutcome::Submitted(committed),
) => request == committed,
@@ -590,8 +614,21 @@ impl AuthoredAtomicReceipt {
if committed_at_unix_ms < command.requested_at_unix_ms() {
return Err(Error::AtomicWorkflowMismatch);
}
+ if let (AuthoredAtomicCommand::RecordSigned(_), AuthoredAtomicOutcome::Artifact(artifact)) =
+ (command, &outcome)
+ && committed_at_unix_ms < artifact.updated_at_unix_ms()
+ {
+ return Err(Error::AtomicWorkflowMismatch);
+ }
match (command, &outcome) {
(
+ AuthoredAtomicCommand::RecordSigned(value),
+ AuthoredAtomicOutcome::Artifact(artifact),
+ ) if signed_fact_matches(value, artifact) => artifact.validate()?,
+ (AuthoredAtomicCommand::RecordSigned(_), _) => {
+ return Err(Error::AtomicWorkflowMismatch);
+ }
+ (
AuthoredAtomicCommand::PrepareFromDraft(request),
AuthoredAtomicOutcome::Submitted(value),
) if request == value => value.validate()?,
@@ -654,6 +691,14 @@ impl AuthoredAtomicReceipt {
}
}
+fn signed_fact_matches(value: &RecordSignedArtifact, artifact: &AuthoredArtifact) -> bool {
+ artifact.operation_id() == value.operation_id()
+ && artifact.artifact_id() == value.artifact_id()
+ && artifact
+ .signed()
+ .is_some_and(|signed| signed.event() == value.event())
+}
+
impl AuthoredAtomicOutcome {
fn is_valid(&self) -> bool {
match self {
diff --git a/crates/storage/src/authored_atomic/signing_evidence.rs b/crates/storage/src/authored_atomic/signing_evidence.rs
@@ -0,0 +1,111 @@
+//! Exact signature facts bound to an already committed signing attempt.
+
+use radroots_event::SignedEvent;
+use radroots_event_codec::verify::{self, Nip01SignatureVerifier, RawEvent};
+
+use super::{
+ AuthoredAtomicCommand, AuthoredAtomicOutcome, AuthoredAtomicReceipt, ClaimAuthoredTarget,
+ ClaimAuthoredWork,
+};
+use crate::{
+ Error,
+ authored::{AuthoredArtifact, AuthoredArtifactId, WorkClaim},
+ journal::OperationInstanceId,
+};
+
+/// Records an existing authored signature without granting scheduling authority.
+///
+/// The backend must retrieve its own immutable receipt for [`Self::claim_command`]
+/// inside the recording transaction. A caller-supplied receipt is not durable
+/// attempt authority. The original claim may have expired or been superseded;
+/// new signing, admission and delivery still require their ordinary fences.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct RecordSignedArtifact {
+ operation_id: OperationInstanceId,
+ artifact_id: AuthoredArtifactId,
+ claim: WorkClaim,
+ event: SignedEvent,
+ observed_at_unix_ms: u64,
+}
+
+impl RecordSignedArtifact {
+ pub fn new(
+ operation_id: OperationInstanceId,
+ artifact_id: AuthoredArtifactId,
+ claim: WorkClaim,
+ event: SignedEvent,
+ observed_at_unix_ms: u64,
+ ) -> Result<Self, Error> {
+ claim.validate()?;
+ if observed_at_unix_ms < claim.acquired_at_unix_ms() {
+ return Err(Error::AtomicWorkflowMismatch);
+ }
+ let id_verified = verify::id(RawEvent::new(event.envelope().clone()))
+ .map_err(|_| Error::InvalidAuthoredArtifact)?;
+ verify::signature(id_verified, &Nip01SignatureVerifier)
+ .map_err(|_| Error::InvalidAuthoredArtifact)?;
+ Ok(Self {
+ operation_id,
+ artifact_id,
+ claim,
+ event,
+ observed_at_unix_ms,
+ })
+ }
+
+ pub const fn operation_id(&self) -> OperationInstanceId {
+ self.operation_id
+ }
+ pub const fn artifact_id(&self) -> AuthoredArtifactId {
+ self.artifact_id
+ }
+ pub const fn claim(&self) -> &WorkClaim {
+ &self.claim
+ }
+ pub const fn event(&self) -> &SignedEvent {
+ &self.event
+ }
+ pub const fn observed_at_unix_ms(&self) -> u64 {
+ self.observed_at_unix_ms
+ }
+
+ pub fn claim_command(&self) -> AuthoredAtomicCommand {
+ AuthoredAtomicCommand::Claim(ClaimAuthoredWork::new(
+ ClaimAuthoredTarget::ArtifactSigning(self.artifact_id),
+ self.claim.clone(),
+ ))
+ }
+
+ /// Applies facts only after exact backend-owned attempt provenance is checked.
+ ///
+ /// The first signed bytes are immutable. An identical result changes nothing;
+ /// a different result conflicts. Cancellation and terminal failure survive.
+ pub fn apply_to(
+ &self,
+ artifact: &mut AuthoredArtifact,
+ original_claim: &AuthoredAtomicReceipt,
+ ) -> Result<(), Error> {
+ let AuthoredAtomicOutcome::Artifact(original) = original_claim.outcome() else {
+ return Err(Error::AtomicWorkflowMismatch);
+ };
+ original.validate()?;
+ artifact.validate()?;
+ if !original_claim.matches_command(&self.claim_command())
+ || original_claim.committed_at_unix_ms() != self.claim.acquired_at_unix_ms()
+ || original.signing_claim() != Some(&self.claim)
+ || original.operation_id() != self.operation_id
+ || artifact.operation_id() != self.operation_id
+ || original.artifact_id() != self.artifact_id
+ || artifact.artifact_id() != self.artifact_id
+ || original.plan() != artifact.plan()
+ || original.origin() != artifact.origin()
+ || original.ordinal() != artifact.ordinal()
+ || original.created_at_unix_ms() != artifact.created_at_unix_ms()
+ || original.revision() > artifact.revision()
+ || original.updated_at_unix_ms() > artifact.updated_at_unix_ms()
+ {
+ return Err(Error::AtomicWorkflowMismatch);
+ }
+ artifact.record_signed_fact(self.event.clone(), self.observed_at_unix_ms)
+ }
+}
diff --git a/crates/storage/src/memory.rs b/crates/storage/src/memory.rs
@@ -1549,6 +1549,20 @@ impl AuthoredAtomicStorage for MemoryStorage {
AuthoredAtomicOutcome::Artifact(artifact.clone())
}
ClaimAuthoredTarget::DeliveryPlan(plan_id) => {
+ let artifact_id = candidate
+ .authored_delivery_plans
+ .iter()
+ .find(|plan| plan.plan_id() == *plan_id)
+ .ok_or(Error::InvalidAuthoredDeliveryPlan)?
+ .artifact_id();
+ let artifact = candidate
+ .authored_artifacts
+ .iter()
+ .find(|artifact| artifact.artifact_id() == artifact_id)
+ .ok_or(Error::InvalidAuthoredArtifact)?;
+ if artifact.signing_state() != crate::authored::SigningState::Signed {
+ return Err(Error::InvalidAuthoredTransition);
+ }
let plan = candidate
.authored_delivery_plans
.iter_mut()
@@ -1599,6 +1613,35 @@ impl AuthoredAtomicStorage for MemoryStorage {
)?;
AuthoredAtomicOutcome::Artifact(artifact.clone())
}
+ AuthoredAtomicCommand::RecordSigned(value) => {
+ let claim_id = value.claim_command().commit_id();
+ let original = candidate
+ .authored_atomic_receipts
+ .iter()
+ .find(|receipt| receipt.commit_id() == claim_id)
+ .ok_or(Error::AtomicWorkflowMismatch)?;
+ let artifact = candidate
+ .authored_artifacts
+ .iter_mut()
+ .find(|artifact| artifact.artifact_id() == value.artifact_id())
+ .ok_or(Error::InvalidAuthoredArtifact)?;
+ let already_signed = artifact.signed().is_some();
+ value.apply_to(artifact, original)?;
+ let artifact = artifact.clone();
+ if !already_signed
+ && artifact.signing_state() == crate::authored::SigningState::Signed
+ {
+ for plan in candidate.authored_delivery_plans.iter_mut().filter(|plan| {
+ plan.artifact_id() == value.artifact_id() && !plan.state().is_terminal()
+ }) {
+ plan.bind_signed_event(
+ value.event().clone(),
+ value.observed_at_unix_ms().max(plan.updated_at_unix_ms()),
+ )?;
+ }
+ }
+ AuthoredAtomicOutcome::Artifact(artifact)
+ }
AuthoredAtomicCommand::ApplyDelivery(value) => {
let plan = candidate
.authored_delivery_plans
@@ -1762,10 +1805,19 @@ impl AuthoredAtomicStorage for MemoryStorage {
}
},
};
+ let committed_at = match (&command, &outcome) {
+ (
+ AuthoredAtomicCommand::RecordSigned(_),
+ AuthoredAtomicOutcome::Artifact(artifact),
+ ) => command
+ .requested_at_unix_ms()
+ .max(artifact.updated_at_unix_ms()),
+ _ => command.requested_at_unix_ms(),
+ };
let receipt = AuthoredAtomicReceipt::new(
&command,
AtomicCommitDisposition::Committed,
- command.requested_at_unix_ms(),
+ committed_at,
outcome,
)?;
candidate.authored_atomic_receipts.push(receipt.clone());
diff --git a/crates/storage/tests/authored_signed_facts.rs b/crates/storage/tests/authored_signed_facts.rs
@@ -0,0 +1,468 @@
+use std::num::NonZeroU64;
+
+use futures_executor::block_on;
+use radroots_event::SignedEvent;
+use radroots_storage::{
+ Error,
+ atomic::AtomicCommitDisposition,
+ authored::{
+ AdmissionState, AuthoredArtifact, FailureClass, SigningState, WorkClaim, WorkFailure,
+ WorkPhase,
+ },
+ authored_atomic::{
+ ApplySignedArtifact, ApplyWorkFailure, AuthoredAtomicCommand, AuthoredAtomicReceipt,
+ AuthoredAtomicStorage, AuthoredWorkTarget, CancelAuthoredTarget, CancelAuthoredWork,
+ ClaimAuthoredTarget, ClaimAuthoredWork, RecordSignedArtifact, WorkFence,
+ },
+ authored_delivery::AuthoredDeliveryState,
+ event::SourceGeneration,
+ memory::MemoryStorage,
+};
+
+#[path = "authored_signing/fixture.rs"]
+mod fixture;
+use fixture::*;
+
+fn prepared() -> (MemoryStorage, SignedEvent, WorkClaim, AuthoredAtomicReceipt) {
+ let storage = MemoryStorage::new(SourceGeneration::new([1; 32]).unwrap());
+ let (preparation, event) = prepare();
+ block_on(storage.execute_authored(preparation)).unwrap();
+ let active = claim(NonZeroU64::new(1).unwrap(), 4, 11);
+ let receipt = block_on(storage.execute_authored(AuthoredAtomicCommand::Claim(
+ ClaimAuthoredWork::new(
+ ClaimAuthoredTarget::ArtifactSigning(ids().1),
+ active.clone(),
+ ),
+ )))
+ .unwrap();
+ (storage, event, active, receipt)
+}
+
+fn artifact(storage: &MemoryStorage) -> AuthoredArtifact {
+ block_on(storage.authored_artifact(ids().1))
+ .unwrap()
+ .unwrap()
+}
+
+fn fence(active: &WorkClaim) -> WorkFence {
+ WorkFence::new(*active.token(), active.generation(), active.row_revision()).unwrap()
+}
+
+#[test]
+fn expired_claim_retains_exact_facts_without_relaxing_active_fences() {
+ let (storage, event, active, original) = prepared();
+ let stale = AuthoredAtomicCommand::ApplySigned(
+ ApplySignedArtifact::new(ids().1, fence(&active), event.clone(), 40).unwrap(),
+ );
+ assert_eq!(
+ block_on(storage.execute_authored(stale.clone())),
+ Err(Error::DeliveryPlanClaimConflict)
+ );
+ let fact = record(event.clone(), active.clone(), 40);
+ assert_ne!(fact.commit_id(), stale.commit_id());
+ let receipt = block_on(storage.execute_authored(fact.clone())).unwrap();
+ assert_eq!(receipt.disposition(), AtomicCommitDisposition::Committed);
+ let signed = artifact(&storage);
+ assert_eq!(signed.signing_state(), SigningState::Signed);
+ assert_eq!(signed.signed().unwrap().event().raw_json(), RAW);
+ assert!(signed.signing_claim().is_none());
+ let delivery = block_on(storage.authored_delivery_plan(ids().2))
+ .unwrap()
+ .unwrap();
+ assert_eq!(delivery.request().unwrap().payload().event(), &event);
+ assert!(delivery.attempts().is_empty());
+ assert_eq!(
+ block_on(storage.authored_receipt(original.commit_id()))
+ .unwrap()
+ .unwrap(),
+ original
+ );
+ let later = record(event, active, 90);
+ assert_eq!(later.commit_id(), fact.commit_id());
+ let replay = block_on(storage.execute_authored(later)).unwrap();
+ assert_eq!(replay.disposition(), AtomicCommitDisposition::Replay);
+ assert_eq!(replay.committed_at_unix_ms(), 40);
+ assert_eq!(artifact(&storage), signed);
+}
+
+#[test]
+fn cancellation_and_terminal_failure_survive_late_signed_bytes() {
+ for cancelled in [false, true] {
+ let (storage, event, active, _) = prepared();
+ let stop = if cancelled {
+ AuthoredAtomicCommand::Cancel(
+ CancelAuthoredWork::new(
+ CancelAuthoredTarget::ArtifactSigning(ids().1),
+ artifact(&storage).revision(),
+ 20,
+ )
+ .unwrap(),
+ )
+ } else {
+ AuthoredAtomicCommand::ApplyFailure(
+ ApplyWorkFailure::new(
+ AuthoredWorkTarget::Artifact(ids().1),
+ fence(&active),
+ WorkFailure::new(
+ "signing_stopped",
+ WorkPhase::Signing,
+ FailureClass::Terminal,
+ None,
+ None,
+ )
+ .unwrap(),
+ None,
+ 20,
+ )
+ .unwrap(),
+ )
+ };
+ let stop_receipt = block_on(storage.execute_authored(stop)).unwrap();
+ let stopped = artifact(&storage);
+ // An older observation still records the fact without moving row time back.
+ let fact_receipt = block_on(storage.execute_authored(record(event, active, 15))).unwrap();
+ assert_eq!(fact_receipt.committed_at_unix_ms(), 20);
+ let retained = artifact(&storage);
+ assert_eq!(retained.signing_state(), stopped.signing_state());
+ assert_eq!(retained.last_failure(), stopped.last_failure());
+ assert_eq!(retained.updated_at_unix_ms(), 20);
+ assert_eq!(retained.admission_state(), AdmissionState::Pending);
+ assert_eq!(retained.signed().unwrap().event().raw_json(), RAW);
+ let delivery = block_on(storage.authored_delivery_plan(ids().2))
+ .unwrap()
+ .unwrap();
+ assert!(delivery.request().is_none());
+ for target in [
+ ClaimAuthoredTarget::ArtifactSigning(ids().1),
+ ClaimAuthoredTarget::ArtifactAdmission(ids().1),
+ ClaimAuthoredTarget::DeliveryPlan(ids().2),
+ ] {
+ let revision = if matches!(target, ClaimAuthoredTarget::DeliveryPlan(_)) {
+ delivery.revision()
+ } else {
+ retained.revision()
+ };
+ assert!(
+ block_on(storage.execute_authored(AuthoredAtomicCommand::Claim(
+ ClaimAuthoredWork::new(target, claim(revision, 8, 50)),
+ )))
+ .is_err()
+ );
+ }
+ assert_eq!(artifact(&storage), retained);
+ assert_eq!(
+ block_on(storage.authored_receipt(stop_receipt.commit_id()))
+ .unwrap()
+ .unwrap(),
+ stop_receipt
+ );
+ let operation = block_on(storage.authored_operation(ids().0))
+ .unwrap()
+ .unwrap();
+ let settlement = radroots_storage::authored::OperationSettlement::evaluate(
+ &operation,
+ std::slice::from_ref(&retained),
+ )
+ .unwrap();
+ assert_eq!(settlement.signed(), 1);
+ assert_eq!(settlement.cancelled(), u16::from(cancelled));
+ assert_eq!(settlement.failed_terminal(), u16::from(!cancelled));
+ #[cfg(feature = "serde")]
+ assert_eq!(
+ serde_json::from_str::<AuthoredArtifact>(&serde_json::to_string(&retained).unwrap())
+ .unwrap(),
+ retained
+ );
+ }
+}
+
+#[test]
+fn superseded_attempts_cannot_overwrite_first_exact_bytes_or_restart_stopped_delivery() {
+ let (storage, event, first, _) = prepared();
+ let second = claim(artifact(&storage).revision(), 5, 40);
+ block_on(
+ storage.execute_authored(AuthoredAtomicCommand::Claim(ClaimAuthoredWork::new(
+ ClaimAuthoredTarget::ArtifactSigning(ids().1),
+ second.clone(),
+ ))),
+ )
+ .unwrap();
+ let plan = block_on(storage.authored_delivery_plan(ids().2))
+ .unwrap()
+ .unwrap();
+ block_on(
+ storage.execute_authored(AuthoredAtomicCommand::Cancel(
+ CancelAuthoredWork::new(
+ CancelAuthoredTarget::DeliveryPlan(ids().2),
+ plan.revision(),
+ 41,
+ )
+ .unwrap(),
+ )),
+ )
+ .unwrap();
+ block_on(storage.execute_authored(record(event.clone(), first.clone(), 42))).unwrap();
+ let retained = artifact(&storage);
+ assert!(retained.signing_claim().is_none());
+ block_on(storage.execute_authored(record(event, second.clone(), 43))).unwrap();
+ assert_eq!(artifact(&storage), retained);
+ let alternate = fixture::event(&format!(" {RAW} "));
+ assert_eq!(alternate.id(), retained.signed().unwrap().event().id());
+ assert_eq!(
+ block_on(storage.execute_authored(record(alternate, second, 44))),
+ Err(Error::AtomicCommitConflict)
+ );
+ assert_eq!(artifact(&storage), retained);
+ let plan = block_on(storage.authored_delivery_plan(ids().2))
+ .unwrap()
+ .unwrap();
+ assert_eq!(plan.state(), AuthoredDeliveryState::Cancelled);
+ assert!(plan.request().is_none());
+ assert!(plan.attempts().is_empty());
+}
+
+#[test]
+fn unrelated_plan_or_attempt_provenance_rolls_back_without_fact_receipts() {
+ let (storage, event, active, original) = prepared();
+ let before = artifact(&storage);
+ let wrong_claims = [
+ WorkClaim::new(
+ [9; 16],
+ active.owner(),
+ active.generation(),
+ 11,
+ 31,
+ active.row_revision(),
+ )
+ .unwrap(),
+ WorkClaim::new(
+ *active.token(),
+ "different-worker",
+ active.generation(),
+ 11,
+ 31,
+ active.row_revision(),
+ )
+ .unwrap(),
+ WorkClaim::new(
+ *active.token(),
+ active.owner(),
+ NonZeroU64::new(9).unwrap(),
+ 11,
+ 31,
+ active.row_revision(),
+ )
+ .unwrap(),
+ WorkClaim::new(
+ *active.token(),
+ active.owner(),
+ active.generation(),
+ 12,
+ 31,
+ active.row_revision(),
+ )
+ .unwrap(),
+ WorkClaim::new(
+ *active.token(),
+ active.owner(),
+ active.generation(),
+ 11,
+ 32,
+ active.row_revision(),
+ )
+ .unwrap(),
+ WorkClaim::new(
+ *active.token(),
+ active.owner(),
+ active.generation(),
+ 11,
+ 31,
+ NonZeroU64::new(9).unwrap(),
+ )
+ .unwrap(),
+ ];
+ let expected = record(event.clone(), active.clone(), 50);
+ for wrong in wrong_claims {
+ let command = record(event.clone(), wrong, 50);
+ assert_ne!(command.commit_id(), expected.commit_id());
+ assert!(block_on(storage.execute_authored(command.clone())).is_err());
+ assert!(
+ block_on(storage.authored_receipt(command.commit_id()))
+ .unwrap()
+ .is_none()
+ );
+ }
+ let wrong_operation = RecordSignedArtifact::new(
+ radroots_storage::journal::OperationInstanceId::new([9; 16]).unwrap(),
+ ids().1,
+ active.clone(),
+ event.clone(),
+ 50,
+ )
+ .unwrap();
+ assert!(
+ block_on(storage.execute_authored(AuthoredAtomicCommand::RecordSigned(wrong_operation)))
+ .is_err()
+ );
+ let mismatch = record(fixture::event(OTHER_RAW), active.clone(), 50);
+ assert!(block_on(storage.execute_authored(mismatch.clone())).is_err());
+ assert!(
+ block_on(storage.authored_receipt(mismatch.commit_id()))
+ .unwrap()
+ .is_none()
+ );
+ assert_eq!(artifact(&storage), before);
+ let AuthoredAtomicCommand::RecordSigned(value) = expected else {
+ unreachable!()
+ };
+ assert_eq!(value.operation_id(), ids().0);
+ assert_eq!(value.artifact_id(), ids().1);
+ assert_eq!(value.claim(), &active);
+ assert_eq!(value.event(), &event);
+ assert_eq!(value.observed_at_unix_ms(), 50);
+ assert_eq!(value.clone(), value);
+ let (preparation, _) = prepare();
+ let wrong_outcome = block_on(storage.execute_authored(preparation)).unwrap();
+ assert!(value.apply_to(&mut before.clone(), &wrong_outcome).is_err());
+ assert!(value.apply_to(&mut before.clone(), &original).is_ok());
+ #[cfg(feature = "serde")]
+ {
+ let mut regressed = serde_json::to_value(&before).unwrap();
+ regressed["signing_claim"] = serde_json::Value::Null;
+ regressed["updated_at_unix_ms"] = serde_json::Value::from(10);
+ let mut regressed = serde_json::from_value::<AuthoredArtifact>(regressed).unwrap();
+ assert!(value.apply_to(&mut regressed, &original).is_err());
+ }
+}
+
+#[test]
+fn invalid_signature_and_pre_attempt_observation_never_become_facts() {
+ let (_, event, active, _) = prepared();
+ for at in [0, 10] {
+ assert!(
+ RecordSignedArtifact::new(ids().0, ids().1, active.clone(), event.clone(), at).is_err()
+ );
+ }
+ let mut value: serde_json::Value = serde_json::from_str(RAW).unwrap();
+ value["sig"] = serde_json::Value::String("ff".repeat(64));
+ let hostile = fixture::event(&serde_json::to_string(&value).unwrap());
+ assert_eq!(
+ RecordSignedArtifact::new(ids().0, ids().1, active, hostile, 50),
+ Err(Error::InvalidAuthoredArtifact)
+ );
+}
+
+#[test]
+fn indeterminate_signing_is_resolved_by_original_verified_evidence() {
+ let (storage, event, active, _) = prepared();
+ block_on(
+ storage.execute_authored(AuthoredAtomicCommand::ApplyFailure(
+ ApplyWorkFailure::new(
+ AuthoredWorkTarget::Artifact(ids().1),
+ fence(&active),
+ WorkFailure::new(
+ "signing_unknown",
+ WorkPhase::Signing,
+ FailureClass::Indeterminate,
+ None,
+ None,
+ )
+ .unwrap(),
+ None,
+ 20,
+ )
+ .unwrap(),
+ )),
+ )
+ .unwrap();
+ assert_eq!(
+ artifact(&storage).signing_state(),
+ SigningState::Indeterminate
+ );
+ block_on(storage.execute_authored(record(event, active, 40))).unwrap();
+ let retained = artifact(&storage);
+ assert_eq!(retained.signing_state(), SigningState::Signed);
+ assert!(retained.last_failure().is_none());
+ assert_eq!(retained.signed().unwrap().event().raw_json(), RAW);
+}
+
+#[test]
+fn signed_fact_receipts_require_exact_outcome_and_monotonic_commit_time() {
+ let (storage, event, active, _) = prepared();
+ let command = record(event, active, 40);
+ let unsigned = artifact(&storage);
+ assert!(
+ AuthoredAtomicReceipt::new(
+ &command,
+ AtomicCommitDisposition::Committed,
+ 40,
+ radroots_storage::authored_atomic::AuthoredAtomicOutcome::Artifact(unsigned),
+ )
+ .is_err()
+ );
+ let receipt = block_on(storage.execute_authored(command.clone())).unwrap();
+ assert!(receipt.matches_command(&command));
+ assert!(
+ AuthoredAtomicReceipt::new(
+ &command,
+ AtomicCommitDisposition::Committed,
+ 39,
+ receipt.outcome().clone(),
+ )
+ .is_err()
+ );
+ let wrong = radroots_storage::authored_atomic::AuthoredAtomicOutcome::DeliveryPlan(
+ block_on(storage.authored_delivery_plan(ids().2))
+ .unwrap()
+ .unwrap(),
+ );
+ assert!(
+ AuthoredAtomicReceipt::new(
+ &command,
+ AtomicCommitDisposition::Committed,
+ 40,
+ wrong.clone()
+ )
+ .is_err()
+ );
+ let malformed = AuthoredAtomicReceipt::from_durable_parts(
+ command.commit_id(),
+ command.digest(),
+ AtomicCommitDisposition::Committed,
+ 40,
+ wrong,
+ )
+ .unwrap();
+ assert!(!malformed.matches_command(&command));
+}
+
+#[cfg(feature = "serde")]
+#[test]
+fn stopped_signed_snapshots_cannot_erase_the_required_terminal_failure() {
+ let (storage, event, active, _) = prepared();
+ block_on(
+ storage.execute_authored(AuthoredAtomicCommand::ApplyFailure(
+ ApplyWorkFailure::new(
+ AuthoredWorkTarget::Artifact(ids().1),
+ fence(&active),
+ WorkFailure::new(
+ "signing_stopped",
+ WorkPhase::Signing,
+ FailureClass::Terminal,
+ None,
+ None,
+ )
+ .unwrap(),
+ None,
+ 20,
+ )
+ .unwrap(),
+ )),
+ )
+ .unwrap();
+ block_on(storage.execute_authored(record(event, active, 40))).unwrap();
+ let retained = artifact(&storage);
+ let mut forged = serde_json::to_value(&retained).unwrap();
+ forged["last_failure"] = serde_json::Value::Null;
+ assert!(serde_json::from_value::<AuthoredArtifact>(forged).is_err());
+}
diff --git a/crates/storage/tests/authored_signing/fixture.rs b/crates/storage/tests/authored_signing/fixture.rs
@@ -0,0 +1,101 @@
+// Authentic public conformance vector generic_operational_listing_009.
+// Source: contracts/conformance/vectors/event/authored_operations.v1.json
+// Source SHA-256: 10a7fc63251e23bd7fb9f133a7754be90e6cdb907340ff190bae659d619ce65b
+use radroots_event::{GenericEventDraft, SignedEvent, wire::v1::Nip01EventWire};
+use radroots_event_codec::authoring::AuthoredEventPlan;
+use radroots_storage::{
+ atomic::AtomicCommitDigest,
+ authored::{AuthoredArtifact, AuthoredArtifactId, AuthoredOperation, WorkClaim},
+ authored_atomic::{AuthoredAtomicCommand, PrepareAuthoredOperation, RecordSignedArtifact},
+ authored_delivery::{AuthoredDeliveryIntent, AuthoredDeliveryPlan, AuthoredDeliveryPlanId},
+ journal::OperationInstanceId,
+};
+use radroots_transport::{
+ Target, TargetSet,
+ policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
+};
+use std::num::NonZeroU64;
+
+pub(super) const RAW: &str = r###"{"id":"da14c35c4afe472a2ddef6d7298cc736782eaf09b55511c3b5774ad79468ada8","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1784347200,"kind":30402,"tags":[["d","AAAAAAAAAAAAAAAAAAAAAg"],["p","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],["a","30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"],["key","carrot-nantes"],["title","Nantes Carrots"],["category","produce"],["summary","Fresh bunches harvested in Saanich"],["published_at","1700000000"],["radroots:primary_bin","bunch"],["radroots:bin","bunch","1","each"],["radroots:price","bunch","4","CAD","1","each"],["price","4","CAD"],["inventory","24"],["status","active"],["delivery","pickup"],["location","Saanich Peninsula","Victoria","BC","CA"],["g","c28hr"]],"content":"# Nantes Carrots\n\nFresh bunches harvested in Saanich","sig":"07edaeab0b05807d4987346c95fd2441b46949be03f455bfbb3678c07b50fa95fb563b509b33f5d547e7cb74f09475c04e281c66362a8db206e57ee757d60dbe"}"###;
+
+pub(super) fn event(raw: &str) -> SignedEvent {
+ let v: serde_json::Value = serde_json::from_str(raw).unwrap();
+ let wire = Nip01EventWire {
+ id: v["id"].as_str().unwrap().to_owned(),
+ pubkey: v["pubkey"].as_str().unwrap().to_owned(),
+ created_at: v["created_at"].as_u64().unwrap(),
+ kind: u32::try_from(v["kind"].as_u64().unwrap()).unwrap(),
+ tags: serde_json::from_value(v["tags"].clone()).unwrap(),
+ content: v["content"].as_str().unwrap().to_owned(),
+ sig: v["sig"].as_str().unwrap().to_owned(),
+ extra: Default::default(),
+ };
+ SignedEvent::from_wire_verified_id(wire, raw.to_owned()).unwrap()
+}
+
+pub(super) fn ids() -> (
+ OperationInstanceId,
+ AuthoredArtifactId,
+ AuthoredDeliveryPlanId,
+) {
+ (
+ OperationInstanceId::new([1; 16]).unwrap(),
+ AuthoredArtifactId::new([2; 16]).unwrap(),
+ AuthoredDeliveryPlanId::new([3; 16]).unwrap(),
+ )
+}
+
+pub(super) fn prepare() -> (AuthoredAtomicCommand, SignedEvent) {
+ let event = event(RAW);
+ let wire = event.wire();
+ let plan = AuthoredEventPlan::from_generic(
+ GenericEventDraft::new(
+ "radroots.operational_listing.published.v1",
+ wire.kind,
+ wire.created_at,
+ wire.tags.clone(),
+ wire.content.clone(),
+ wire.pubkey.clone(),
+ )
+ .unwrap(),
+ )
+ .unwrap();
+ let (operation, artifact, delivery) = ids();
+ let intent = AuthoredDeliveryIntent::new(
+ "signed-fact",
+ TargetSet::new(vec![Target::nostr_relay("wss://one.example").unwrap()]).unwrap(),
+ SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()),
+ 100,
+ )
+ .unwrap();
+ let preparation = PrepareAuthoredOperation::new(
+ AuthoredOperation::new(operation, vec![artifact], 10).unwrap(),
+ vec![AuthoredArtifact::planned(artifact, operation, 0, &plan, 10).unwrap()],
+ vec![AuthoredDeliveryPlan::new(delivery, artifact, intent, 10).unwrap()],
+ AtomicCommitDigest::new([7; 32]),
+ 10,
+ )
+ .unwrap();
+ (AuthoredAtomicCommand::Prepare(preparation), event)
+}
+
+pub(super) fn claim(revision: NonZeroU64, token: u8, at: u64) -> WorkClaim {
+ WorkClaim::new(
+ [token; 16],
+ format!("worker-{token}"),
+ NonZeroU64::new(u64::from(token)).unwrap(),
+ at,
+ at + 20,
+ revision,
+ )
+ .unwrap()
+}
+
+pub(super) fn record(event: SignedEvent, claim: WorkClaim, at: u64) -> AuthoredAtomicCommand {
+ AuthoredAtomicCommand::RecordSigned(
+ RecordSignedArtifact::new(ids().0, ids().1, claim, event, at).unwrap(),
+ )
+}
+
+// Authentic sibling vector typed_update_escaping_002, deliberately a different plan.
+pub(super) const OTHER_RAW: &str = r###"{"id":"11bcbaeab205194e26ae4d950190c03d18edb1b65f428048e589e4bbdcfa9d50","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1784347200,"kind":1,"tags":[],"content":"Farm update: \"ready\"\\\n🍓","sig":"a6a323774f1ce4aafa6c479e758eb350a7e5c4bdc55c7690beba2ccb1631839a1246a83e62f4b29e74f9afda62802794981570e10c7d4ad41c392dab4066b88c"}"###;
diff --git a/crates/storage_sqlite/README.md b/crates/storage_sqlite/README.md
@@ -2,6 +2,19 @@
SQLite storage backend for Radroots.
+Runtime schema v15 permits late verified authored signatures to remain durable
+alongside a signing stop. The logical snapshot preserves cancelled or terminal
+state; a separate SQL stop column preserves the original signed/raw constraint.
+Prior snapshots, receipt identities, migration bytes and checksums remain
+unchanged. Older schema policies reject the new version. An update guard prevents
+replacement of the first signed bytes or removal of the retained signing stop.
+
+Recording a late fact verifies the backend's immutable original claim receipt
+inside the same transaction as the artifact, eligible delivery bindings and new
+receipt. Expiry does not invalidate evidence, and evidence does not renew work
+authority. Explicitly stopped delivery plans remain stopped. A success receipt
+is returned only after the actual SQLite COMMIT.
+
The backend owns separate `runtime.sqlite` and `private.sqlite` files. Writable
opens hold a process advisory lock, use WAL with bounded busy handling, and
apply only the governed forward migrations. Fresh stores require a
diff --git a/crates/storage_sqlite/src/authored.rs b/crates/storage_sqlite/src/authored.rs
@@ -204,10 +204,18 @@ async fn commit_outcome(
command: &AuthoredAtomicCommand,
outcome: AuthoredAtomicOutcome,
) -> Result<AuthoredAtomicReceipt, Error> {
+ let committed_at = match (command, &outcome) {
+ (AuthoredAtomicCommand::RecordSigned(_), AuthoredAtomicOutcome::Artifact(artifact)) => {
+ command
+ .requested_at_unix_ms()
+ .max(artifact.updated_at_unix_ms())
+ }
+ _ => command.requested_at_unix_ms(),
+ };
let receipt = AuthoredAtomicReceipt::new(
command,
AtomicCommitDisposition::Committed,
- command.requested_at_unix_ms(),
+ committed_at,
outcome,
)?;
sqlx::query(
@@ -309,11 +317,54 @@ async fn execute_command(
}
ClaimAuthoredTarget::DeliveryPlan(id) => {
let mut plan = load_plan_tx(transaction, *id).await?;
+ let artifact = load_artifact_tx(transaction, plan.artifact_id()).await?;
+ if artifact.signing_state() != SigningState::Signed {
+ return Err(Error::InvalidAuthoredTransition);
+ }
plan.claim(value.claim().clone(), value.claim().acquired_at_unix_ms())?;
persist_plan(transaction, &plan).await?;
Ok(AuthoredAtomicOutcome::DeliveryPlan(plan))
}
},
+ AuthoredAtomicCommand::RecordSigned(value) => {
+ let row = sqlx::query(
+ "SELECT commit_id, commit_digest, requested_at_unix_ms,
+ committed_at_unix_ms, receipt
+ FROM radroots_runtime_authored_atomic_commits WHERE commit_id = ?",
+ )
+ .bind(value.claim_command().commit_id().as_bytes().as_slice())
+ .fetch_optional(&mut **transaction)
+ .await
+ .map_err(map_backend)?
+ .ok_or(Error::AtomicWorkflowMismatch)?;
+ let original = decode_receipt_row(&row)?;
+ let mut artifact = load_artifact_tx(transaction, value.artifact_id()).await?;
+ let already_signed = artifact.signed().is_some();
+ value.apply_to(&mut artifact, &original)?;
+ persist_artifact(transaction, &artifact).await?;
+ if !already_signed && artifact.signing_state() == SigningState::Signed {
+ let plan_ids = sqlx::query_scalar::<_, Vec<u8>>(
+ "SELECT plan_id FROM radroots_runtime_authored_delivery_plans
+ WHERE artifact_id = ? ORDER BY plan_id",
+ )
+ .bind(value.artifact_id().as_bytes().as_slice())
+ .fetch_all(&mut **transaction)
+ .await
+ .map_err(map_backend)?;
+ for bytes in plan_ids {
+ let id = AuthoredDeliveryPlanId::new(array(bytes)?)?;
+ let mut plan = load_plan_tx(transaction, id).await?;
+ if !plan.state().is_terminal() {
+ plan.bind_signed_event(
+ value.event().clone(),
+ value.observed_at_unix_ms().max(plan.updated_at_unix_ms()),
+ )?;
+ persist_plan(transaction, &plan).await?;
+ }
+ }
+ }
+ Ok(AuthoredAtomicOutcome::Artifact(artifact))
+ }
AuthoredAtomicCommand::ApplySigned(value) => {
let mut artifact = load_artifact_tx(transaction, value.artifact_id()).await?;
require_artifact_claim(
@@ -534,8 +585,9 @@ pub(crate) async fn persist_artifact(
signing_claim_expires_at_unix_ms, admission_claim_token,
admission_claim_generation, admission_claim_revision,
admission_claim_expires_at_unix_ms, retry_not_before_unix_ms,
- last_failure_code, created_at_unix_ms, updated_at_unix_ms, revision, snapshot
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ last_failure_code, created_at_unix_ms, updated_at_unix_ms, revision, snapshot,
+ signing_stop
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(artifact_id) DO UPDATE SET
operation_id=excluded.operation_id, ordinal=excluded.ordinal, origin=excluded.origin,
signing_state=excluded.signing_state, admission_state=excluded.admission_state,
@@ -552,13 +604,13 @@ pub(crate) async fn persist_artifact(
retry_not_before_unix_ms=excluded.retry_not_before_unix_ms,
last_failure_code=excluded.last_failure_code,
updated_at_unix_ms=excluded.updated_at_unix_ms, revision=excluded.revision,
- snapshot=excluded.snapshot",
+ snapshot=excluded.snapshot, signing_stop=excluded.signing_stop",
)
.bind(artifact.artifact_id().as_bytes().as_slice())
.bind(artifact.operation_id().as_bytes().as_slice())
.bind(i64::from(artifact.ordinal()))
.bind(origin_name(artifact.origin()))
- .bind(signing_name(artifact.signing_state()))
+ .bind(physical_signing_name(artifact))
.bind(admission_name(artifact.admission_state()))
.bind(artifact.plan().map(|plan| plan.wire_json()))
.bind(
@@ -609,6 +661,7 @@ pub(crate) async fn persist_artifact(
.bind(i64_from_u64(artifact.updated_at_unix_ms())?)
.bind(i64_from_u64(artifact.revision().get())?)
.bind(encode_snapshot(artifact)?)
+ .bind(signing_stop(artifact))
.execute(&mut **transaction)
.await
.map_err(map_backend)?;
@@ -879,7 +932,8 @@ fn decode_artifact_row(row: &SqliteRow) -> Result<AuthoredArtifact, Error> {
|| column::<Vec<u8>>(row, "operation_id")?.as_slice() != value.operation_id().as_bytes()
|| column::<i64>(row, "ordinal")? != i64::from(value.ordinal())
|| column::<String>(row, "origin")? != origin_name(value.origin())
- || column::<String>(row, "signing_state")? != signing_name(value.signing_state())
+ || column::<String>(row, "signing_state")? != physical_signing_name(&value)
+ || column::<Option<String>>(row, "signing_stop")?.as_deref() != signing_stop(&value)
|| column::<String>(row, "admission_state")? != admission_name(value.admission_state())
|| plan_wire.as_deref() != value.plan().map(|plan| plan.wire_json())
|| raw.as_deref()
@@ -1066,7 +1120,7 @@ fn require_revision(actual: u64, expected: u64) -> Result<(), Error> {
Ok(())
}
-fn encode_snapshot<T: Serialize>(value: &T) -> Result<Vec<u8>, Error> {
+pub(crate) fn encode_snapshot<T: Serialize>(value: &T) -> Result<Vec<u8>, Error> {
let bytes = serde_json::to_vec(value).map_err(|_| Error::AtomicCommitFailed)?;
if bytes.len() < 2 || bytes.len() > SNAPSHOT_MAX_BYTES {
return Err(Error::AtomicCommitFailed);
@@ -1093,6 +1147,7 @@ fn command_target(command: &AuthoredAtomicCommand) -> [u8; 16] {
ClaimAuthoredTarget::DeliveryPlan(id) => *id.as_bytes(),
},
AuthoredAtomicCommand::ApplySigned(value) => *value.artifact_id().as_bytes(),
+ AuthoredAtomicCommand::RecordSigned(value) => *value.artifact_id().as_bytes(),
AuthoredAtomicCommand::ApplyAdmission(value) => *value.artifact_id().as_bytes(),
AuthoredAtomicCommand::ApplyDelivery(value) => *value.plan_id().as_bytes(),
AuthoredAtomicCommand::ApplyFailure(value) => match value.target() {
@@ -1111,7 +1166,7 @@ fn command_phase(command: &AuthoredAtomicCommand) -> &'static str {
match command {
AuthoredAtomicCommand::Prepare(_) | AuthoredAtomicCommand::PrepareFromDraft(_) => "prepare",
AuthoredAtomicCommand::Claim(_) => "claim",
- AuthoredAtomicCommand::ApplySigned(_) => "signing",
+ AuthoredAtomicCommand::ApplySigned(_) | AuthoredAtomicCommand::RecordSigned(_) => "signing",
AuthoredAtomicCommand::ApplyAdmission(_) => "admission",
AuthoredAtomicCommand::ApplyDelivery(_) => "delivery",
AuthoredAtomicCommand::ApplyFailure(value) => match value.failure().phase() {
@@ -1141,6 +1196,27 @@ const fn signing_name(value: SigningState) -> &'static str {
}
}
+const fn physical_signing_name(artifact: &AuthoredArtifact) -> &'static str {
+ if artifact.signed().is_some() {
+ "signed"
+ } else {
+ signing_name(artifact.signing_state())
+ }
+}
+
+const fn signing_stop(artifact: &AuthoredArtifact) -> Option<&'static str> {
+ if artifact.signed().is_some()
+ && matches!(
+ artifact.signing_state(),
+ SigningState::Cancelled | SigningState::FailedTerminal
+ )
+ {
+ Some(signing_name(artifact.signing_state()))
+ } else {
+ None
+ }
+}
+
const fn admission_name(value: AdmissionState) -> &'static str {
match value {
AdmissionState::Pending => "pending",
@@ -2274,6 +2350,17 @@ mod tests {
let store = signed_store().await;
let artifact = store.authored_artifact(ids().1).await.unwrap().unwrap();
let signed = artifact.signed().expect("signed artifact");
+ assert!(
+ sqlx::query("UPDATE radroots_runtime_authored_artifacts SET signed_raw_json = x'7b7d'")
+ .execute(&store.pool)
+ .await
+ .is_err()
+ );
+ // Simulate corruption below the write guard to retain read-side validation coverage.
+ sqlx::query("DROP TRIGGER radroots_runtime_authored_artifacts_signed_fact_guard")
+ .execute(&store.pool)
+ .await
+ .unwrap();
sqlx::query("UPDATE radroots_runtime_authored_artifacts SET signed_raw_json = x'7b7d'")
.execute(&store.pool)
.await
@@ -2512,3 +2599,13 @@ mod draft_submission_tests;
#[cfg_attr(coverage_nightly, coverage(off))]
#[path = "authored_signed_durability_tests.rs"]
mod signed_durability_tests;
+
+#[cfg(test)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+#[path = "authored_signed_fact_tests.rs"]
+mod signed_fact_tests;
+
+#[cfg(test)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+#[path = "authored_signed_fact_fixture.rs"]
+pub(crate) mod signed_fact_fixture;
diff --git a/crates/storage_sqlite/src/authored_signed_fact_fixture.rs b/crates/storage_sqlite/src/authored_signed_fact_fixture.rs
@@ -0,0 +1,101 @@
+// Authentic public conformance vector generic_operational_listing_009.
+// Source: contracts/conformance/vectors/event/authored_operations.v1.json
+// Source SHA-256: 10a7fc63251e23bd7fb9f133a7754be90e6cdb907340ff190bae659d619ce65b
+use radroots_event::{GenericEventDraft, SignedEvent, wire::v1::Nip01EventWire};
+use radroots_event_codec::authoring::AuthoredEventPlan;
+use radroots_storage::{
+ atomic::AtomicCommitDigest,
+ authored::{AuthoredArtifact, AuthoredArtifactId, AuthoredOperation, WorkClaim},
+ authored_atomic::{AuthoredAtomicCommand, PrepareAuthoredOperation, RecordSignedArtifact},
+ authored_delivery::{AuthoredDeliveryIntent, AuthoredDeliveryPlan, AuthoredDeliveryPlanId},
+ journal::OperationInstanceId,
+};
+use radroots_transport::{
+ Target, TargetSet,
+ policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
+};
+use std::num::NonZeroU64;
+
+pub(crate) const RAW: &str = r###"{"id":"da14c35c4afe472a2ddef6d7298cc736782eaf09b55511c3b5774ad79468ada8","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1784347200,"kind":30402,"tags":[["d","AAAAAAAAAAAAAAAAAAAAAg"],["p","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],["a","30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"],["key","carrot-nantes"],["title","Nantes Carrots"],["category","produce"],["summary","Fresh bunches harvested in Saanich"],["published_at","1700000000"],["radroots:primary_bin","bunch"],["radroots:bin","bunch","1","each"],["radroots:price","bunch","4","CAD","1","each"],["price","4","CAD"],["inventory","24"],["status","active"],["delivery","pickup"],["location","Saanich Peninsula","Victoria","BC","CA"],["g","c28hr"]],"content":"# Nantes Carrots\n\nFresh bunches harvested in Saanich","sig":"07edaeab0b05807d4987346c95fd2441b46949be03f455bfbb3678c07b50fa95fb563b509b33f5d547e7cb74f09475c04e281c66362a8db206e57ee757d60dbe"}"###;
+
+pub(crate) fn event(raw: &str) -> SignedEvent {
+ let v: serde_json::Value = serde_json::from_str(raw).unwrap();
+ let wire = Nip01EventWire {
+ id: v["id"].as_str().unwrap().to_owned(),
+ pubkey: v["pubkey"].as_str().unwrap().to_owned(),
+ created_at: v["created_at"].as_u64().unwrap(),
+ kind: u32::try_from(v["kind"].as_u64().unwrap()).unwrap(),
+ tags: serde_json::from_value(v["tags"].clone()).unwrap(),
+ content: v["content"].as_str().unwrap().to_owned(),
+ sig: v["sig"].as_str().unwrap().to_owned(),
+ extra: Default::default(),
+ };
+ SignedEvent::from_wire_verified_id(wire, raw.to_owned()).unwrap()
+}
+
+pub(crate) fn ids() -> (
+ OperationInstanceId,
+ AuthoredArtifactId,
+ AuthoredDeliveryPlanId,
+) {
+ (
+ OperationInstanceId::new([1; 16]).unwrap(),
+ AuthoredArtifactId::new([2; 16]).unwrap(),
+ AuthoredDeliveryPlanId::new([3; 16]).unwrap(),
+ )
+}
+
+pub(crate) fn prepare() -> (AuthoredAtomicCommand, SignedEvent) {
+ let event = event(RAW);
+ let wire = event.wire();
+ let plan = AuthoredEventPlan::from_generic(
+ GenericEventDraft::new(
+ "radroots.operational_listing.published.v1",
+ wire.kind,
+ wire.created_at,
+ wire.tags.clone(),
+ wire.content.clone(),
+ wire.pubkey.clone(),
+ )
+ .unwrap(),
+ )
+ .unwrap();
+ let (operation, artifact, delivery) = ids();
+ let intent = AuthoredDeliveryIntent::new(
+ "signed-fact",
+ TargetSet::new(vec![Target::nostr_relay("wss://one.example").unwrap()]).unwrap(),
+ SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()),
+ 100,
+ )
+ .unwrap();
+ let preparation = PrepareAuthoredOperation::new(
+ AuthoredOperation::new(operation, vec![artifact], 10).unwrap(),
+ vec![AuthoredArtifact::planned(artifact, operation, 0, &plan, 10).unwrap()],
+ vec![AuthoredDeliveryPlan::new(delivery, artifact, intent, 10).unwrap()],
+ AtomicCommitDigest::new([7; 32]),
+ 10,
+ )
+ .unwrap();
+ (AuthoredAtomicCommand::Prepare(preparation), event)
+}
+
+pub(crate) fn claim(revision: NonZeroU64, token: u8, at: u64) -> WorkClaim {
+ WorkClaim::new(
+ [token; 16],
+ format!("worker-{token}"),
+ NonZeroU64::new(u64::from(token)).unwrap(),
+ at,
+ at + 20,
+ revision,
+ )
+ .unwrap()
+}
+
+pub(crate) fn record(event: SignedEvent, claim: WorkClaim, at: u64) -> AuthoredAtomicCommand {
+ AuthoredAtomicCommand::RecordSigned(
+ RecordSignedArtifact::new(ids().0, ids().1, claim, event, at).unwrap(),
+ )
+}
+
+// Authentic sibling vector typed_update_escaping_002, deliberately a different plan.
+pub(crate) const OTHER_RAW: &str = r###"{"id":"11bcbaeab205194e26ae4d950190c03d18edb1b65f428048e589e4bbdcfa9d50","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1784347200,"kind":1,"tags":[],"content":"Farm update: \"ready\"\\\n🍓","sig":"a6a323774f1ce4aafa6c479e758eb350a7e5c4bdc55c7690beba2ccb1631839a1246a83e62f4b29e74f9afda62802794981570e10c7d4ad41c392dab4066b88c"}"###;
diff --git a/crates/storage_sqlite/src/authored_signed_fact_tests.rs b/crates/storage_sqlite/src/authored_signed_fact_tests.rs
@@ -0,0 +1,327 @@
+use super::*;
+use crate::{OpenMode, OpenOptions, Paths};
+use radroots_storage::{
+ authored::WorkClaim,
+ authored_atomic::{
+ ApplySignedArtifact, ApplyWorkFailure, CancelAuthoredWork, ClaimAuthoredWork,
+ },
+ event::SourceGeneration,
+};
+use tempfile::TempDir;
+
+use super::signed_fact_fixture as fixture;
+use fixture::*;
+
+async fn open(temp: &TempDir, mode: OpenMode) -> SqliteStorage {
+ let options = OpenOptions::new(Paths::from_directory(temp.path()).unwrap(), mode);
+ let options = if matches!(mode, OpenMode::Create) {
+ options
+ .with_source_generation(SourceGeneration::new([9; 32]).unwrap(), 9)
+ .unwrap()
+ } else {
+ options
+ };
+ SqliteStorage::open(options).await.unwrap()
+}
+
+async fn prepared(temp: &TempDir) -> (SqliteStorage, radroots_event::SignedEvent, WorkClaim) {
+ let store = open(temp, OpenMode::Create).await;
+ let (preparation, event) = prepare();
+ store.execute_authored(preparation).await.unwrap();
+ let artifact = store.authored_artifact(ids().1).await.unwrap().unwrap();
+ let active = claim(artifact.revision(), 4, 11);
+ store
+ .execute_authored(AuthoredAtomicCommand::Claim(ClaimAuthoredWork::new(
+ ClaimAuthoredTarget::ArtifactSigning(ids().1),
+ active.clone(),
+ )))
+ .await
+ .unwrap();
+ (store, event, active)
+}
+
+fn fence(claim: &WorkClaim) -> WorkFence {
+ WorkFence::new(*claim.token(), claim.generation(), claim.row_revision()).unwrap()
+}
+
+#[tokio::test]
+async fn late_stopped_signature_reopens_with_exact_bytes_and_no_scheduling_authority() {
+ for cancelled in [false, true] {
+ let temp = TempDir::new().unwrap();
+ let (store, event, active) = prepared(&temp).await;
+ let artifact = store.authored_artifact(ids().1).await.unwrap().unwrap();
+ let stop = if cancelled {
+ AuthoredAtomicCommand::Cancel(
+ CancelAuthoredWork::new(
+ CancelAuthoredTarget::ArtifactSigning(ids().1),
+ artifact.revision(),
+ 20,
+ )
+ .unwrap(),
+ )
+ } else {
+ AuthoredAtomicCommand::ApplyFailure(
+ ApplyWorkFailure::new(
+ AuthoredWorkTarget::Artifact(ids().1),
+ fence(&active),
+ WorkFailure::new(
+ "signing_stopped",
+ WorkPhase::Signing,
+ FailureClass::Terminal,
+ None,
+ None,
+ )
+ .unwrap(),
+ None,
+ 20,
+ )
+ .unwrap(),
+ )
+ };
+ let stop_receipt = store.execute_authored(stop).await.unwrap();
+ let command = record(event, active, 40);
+ let receipt = store.execute_authored(command.clone()).await.unwrap();
+ let retained = store.authored_artifact(ids().1).await.unwrap().unwrap();
+ assert_eq!(
+ retained.signing_state(),
+ if cancelled {
+ SigningState::Cancelled
+ } else {
+ SigningState::FailedTerminal
+ }
+ );
+ assert_eq!(retained.signed().unwrap().event().raw_json(), RAW);
+ let (physical, stop): (String, Option<String>) = sqlx::query_as(
+ "SELECT signing_state, signing_stop FROM radroots_runtime_authored_artifacts",
+ )
+ .fetch_one(store.pool())
+ .await
+ .unwrap();
+ assert_eq!(physical, "signed");
+ assert_eq!(
+ stop.as_deref(),
+ Some(if cancelled {
+ "cancelled"
+ } else {
+ "failed_terminal"
+ })
+ );
+ for mutation in [
+ "UPDATE radroots_runtime_authored_artifacts SET signed_raw_json = x'7b7d'",
+ "UPDATE radroots_runtime_authored_artifacts SET signed_raw_sha256 = zeroblob(32)",
+ "UPDATE radroots_runtime_authored_artifacts SET signing_stop = NULL",
+ ] {
+ assert!(sqlx::query(mutation).execute(store.pool()).await.is_err());
+ }
+ for target in [
+ ClaimAuthoredTarget::ArtifactSigning(ids().1),
+ ClaimAuthoredTarget::ArtifactAdmission(ids().1),
+ ClaimAuthoredTarget::DeliveryPlan(ids().2),
+ ] {
+ assert!(
+ store
+ .execute_authored(AuthoredAtomicCommand::Claim(ClaimAuthoredWork::new(
+ target,
+ claim(retained.revision(), 8, 50),
+ )))
+ .await
+ .is_err()
+ );
+ }
+ store.close().await.unwrap();
+ let store = open(&temp, OpenMode::ReadWriteExisting).await;
+ assert_eq!(
+ store.authored_artifact(ids().1).await.unwrap().unwrap(),
+ retained
+ );
+ assert_eq!(
+ store
+ .authored_receipt(stop_receipt.commit_id())
+ .await
+ .unwrap()
+ .unwrap(),
+ stop_receipt
+ );
+ let replay = store.execute_authored(command).await.unwrap();
+ assert_eq!(replay.disposition(), AtomicCommitDisposition::Replay);
+ assert_eq!(replay.outcome(), receipt.outcome());
+ let delivery = store
+ .authored_delivery_plan(ids().2)
+ .await
+ .unwrap()
+ .unwrap();
+ assert!(delivery.request().is_none());
+ assert!(delivery.attempts().is_empty());
+ store.close().await.unwrap();
+ }
+}
+
+#[tokio::test]
+async fn actual_commit_failure_rolls_back_late_fact_binding_and_receipt_then_retries() {
+ let temp = TempDir::new().unwrap();
+ let (store, event, active) = prepared(&temp).await;
+ let before = store.authored_artifact(ids().1).await.unwrap().unwrap();
+ let command = record(event, active, 40);
+ sqlx::query("CREATE TABLE signed_fact_commit_fault (parent BLOB REFERENCES radroots_runtime_authored_operations(operation_id) DEFERRABLE INITIALLY DEFERRED)")
+ .execute(store.pool()).await.unwrap();
+ sqlx::query("CREATE TRIGGER signed_fact_commit_fault_trigger AFTER INSERT ON radroots_runtime_authored_atomic_commits WHEN NEW.phase = 'signing' BEGIN INSERT INTO signed_fact_commit_fault VALUES (x'99999999999999999999999999999999'); END")
+ .execute(store.pool()).await.unwrap();
+ assert!(store.execute_authored(command.clone()).await.is_err());
+ assert_eq!(
+ store.authored_artifact(ids().1).await.unwrap().unwrap(),
+ before
+ );
+ assert!(
+ store
+ .authored_delivery_plan(ids().2)
+ .await
+ .unwrap()
+ .unwrap()
+ .request()
+ .is_none()
+ );
+ assert!(
+ store
+ .authored_receipt(command.commit_id())
+ .await
+ .unwrap()
+ .is_none()
+ );
+ assert_eq!(
+ sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM signed_fact_commit_fault")
+ .fetch_one(store.pool())
+ .await
+ .unwrap(),
+ 0
+ );
+ sqlx::query("DROP TRIGGER signed_fact_commit_fault_trigger")
+ .execute(store.pool())
+ .await
+ .unwrap();
+ sqlx::query("DROP TABLE signed_fact_commit_fault")
+ .execute(store.pool())
+ .await
+ .unwrap();
+ store.close().await.unwrap();
+ let store = open(&temp, OpenMode::ReadWriteExisting).await;
+ assert_eq!(
+ store.authored_artifact(ids().1).await.unwrap().unwrap(),
+ before
+ );
+ let receipt = store.execute_authored(command.clone()).await.unwrap();
+ assert_eq!(receipt.disposition(), AtomicCommitDisposition::Committed);
+ assert_eq!(
+ store
+ .authored_delivery_plan(ids().2)
+ .await
+ .unwrap()
+ .unwrap()
+ .request()
+ .unwrap()
+ .payload()
+ .event()
+ .raw_json(),
+ RAW
+ );
+ store.close().await.unwrap();
+ let store = open(&temp, OpenMode::ReadOnly).await;
+ assert_eq!(
+ store
+ .authored_receipt(command.commit_id())
+ .await
+ .unwrap()
+ .unwrap(),
+ receipt
+ );
+ assert!(store.execute_authored(command).await.is_err());
+ store.close().await.unwrap();
+}
+
+#[tokio::test]
+async fn stale_active_fence_and_altered_provenance_cannot_install_late_facts() {
+ let temp = TempDir::new().unwrap();
+ let (store, event, active) = prepared(&temp).await;
+ let before = store.authored_artifact(ids().1).await.unwrap().unwrap();
+ assert!(
+ store
+ .execute_authored(AuthoredAtomicCommand::ApplySigned(
+ ApplySignedArtifact::new(ids().1, fence(&active), event.clone(), 40,).unwrap()
+ ))
+ .await
+ .is_err()
+ );
+ let wrong_owner = WorkClaim::new(
+ *active.token(),
+ "wrong-owner",
+ active.generation(),
+ 11,
+ 31,
+ active.row_revision(),
+ )
+ .unwrap();
+ for command in [
+ record(event.clone(), wrong_owner, 40),
+ record(fixture::event(OTHER_RAW), active.clone(), 40),
+ ] {
+ assert!(store.execute_authored(command.clone()).await.is_err());
+ assert!(
+ store
+ .authored_receipt(command.commit_id())
+ .await
+ .unwrap()
+ .is_none()
+ );
+ assert_eq!(
+ store.authored_artifact(ids().1).await.unwrap().unwrap(),
+ before
+ );
+ }
+ let command = record(event, active.clone(), 40);
+ store.execute_authored(command).await.unwrap();
+ let retained = store.authored_artifact(ids().1).await.unwrap().unwrap();
+ let alternate = record(fixture::event(&format!(" {RAW} ")), active, 50);
+ assert_eq!(
+ store.execute_authored(alternate.clone()).await,
+ Err(Error::AtomicCommitConflict)
+ );
+ assert!(
+ store
+ .authored_receipt(alternate.commit_id())
+ .await
+ .unwrap()
+ .is_none()
+ );
+ assert_eq!(
+ store.authored_artifact(ids().1).await.unwrap().unwrap(),
+ retained
+ );
+ store.close().await.unwrap();
+}
+
+#[tokio::test]
+async fn failure_while_binding_delivery_rolls_back_the_first_signed_fact() {
+ let temp = TempDir::new().unwrap();
+ let (store, event, active) = prepared(&temp).await;
+ let before = store.authored_artifact(ids().1).await.unwrap().unwrap();
+ let command = record(event, active, 40);
+ sqlx::query("CREATE TRIGGER signed_fact_binding_fault BEFORE UPDATE ON radroots_runtime_authored_delivery_plans BEGIN SELECT RAISE(ABORT, 'fixture delivery binding failure'); END")
+ .execute(store.pool()).await.unwrap();
+ assert!(store.execute_authored(command.clone()).await.is_err());
+ assert_eq!(
+ store.authored_artifact(ids().1).await.unwrap().unwrap(),
+ before
+ );
+ assert!(
+ store
+ .authored_receipt(command.commit_id())
+ .await
+ .unwrap()
+ .is_none()
+ );
+ sqlx::query("DROP TRIGGER signed_fact_binding_fault")
+ .execute(store.pool())
+ .await
+ .unwrap();
+ store.execute_authored(command).await.unwrap();
+ store.close().await.unwrap();
+}
diff --git a/crates/storage_sqlite/src/migration.rs b/crates/storage_sqlite/src/migration.rs
@@ -371,7 +371,7 @@ async fn metadata(
fn validate_plan(plan: &MigrationPlan) -> Result<(), Error> {
let valid = plan.minimum_version > 0
&& plan.minimum_version <= plan.current_version
- && plan.current_version <= 14
+ && plan.current_version <= 15
&& plan.steps.len() == usize::try_from(plan.current_version).unwrap_or(usize::MAX)
&& plan
.steps
@@ -490,6 +490,7 @@ const fn set_user_version_sql(version: u32) -> Option<&'static str> {
12 => Some("PRAGMA user_version = 12"),
13 => Some("PRAGMA user_version = 13"),
14 => Some("PRAGMA user_version = 14"),
+ 15 => Some("PRAGMA user_version = 15"),
_ => None,
}
}
@@ -716,7 +717,7 @@ mod tests {
.execute(&mut newer)
.await
.expect("application id");
- sqlx::raw_sql("PRAGMA user_version = 15")
+ sqlx::raw_sql("PRAGMA user_version = 16")
.execute(&mut newer)
.await
.expect("newer version");
@@ -725,10 +726,10 @@ mod tests {
Err(Error::SchemaTooNew {
database: RUNTIME_DATABASE,
supported: runtime::CURRENT_VERSION,
- actual: 15,
+ actual: 16,
})
));
- assert_eq!(pragma(&mut newer, "user_version").await, 15);
+ assert_eq!(pragma(&mut newer, "user_version").await, 16);
let mut wrong_identity = connection().await;
establish_runtime_version(&mut wrong_identity, 1).await;
@@ -862,3 +863,8 @@ mod tests {
#[cfg_attr(coverage_nightly, coverage(off))]
#[path = "migration_draft_query_tests.rs"]
mod draft_query_tests;
+
+#[cfg(test)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+#[path = "migration_signed_facts_tests.rs"]
+mod signed_facts_tests;
diff --git a/crates/storage_sqlite/src/migration/authored_v10.rs b/crates/storage_sqlite/src/migration/authored_v10.rs
@@ -4,8 +4,8 @@ use radroots_event::SignedEvent;
use radroots_event_codec::{Codec, verify};
use radroots_storage::{
authored::{
- AdmissionState, AuthoredArtifact, AuthoredArtifactId, AuthoredOperation, FailureClass,
- RetrySchedule, WorkClaim, WorkFailure, WorkPhase,
+ AdmissionState, ArtifactOrigin, AuthoredArtifact, AuthoredArtifactId, AuthoredOperation,
+ FailureClass, RetrySchedule, WorkClaim, WorkFailure, WorkPhase,
},
authored_delivery::{AuthoredDeliveryPlan, AuthoredDeliveryPlanId, AuthoredDeliveryState},
journal::{JournalState, OperationRecord},
@@ -311,9 +311,7 @@ pub(crate) async fn apply(
authored::persist_operation(transaction, &operation)
.await
.map_err(|_| metadata_error())?;
- authored::persist_artifact(transaction, &artifact)
- .await
- .map_err(|_| metadata_error())?;
+ persist_imported_artifact(transaction, &artifact).await?;
authored::persist_plan(transaction, &plan)
.await
.map_err(|_| metadata_error())?;
@@ -432,6 +430,40 @@ fn convert_candidate(
Ok((operation, artifact, plan))
}
+// This conversion runs immediately after v11 DDL, before successor columns exist.
+async fn persist_imported_artifact(
+ transaction: &mut sqlx::Transaction<'_, Sqlite>,
+ artifact: &AuthoredArtifact,
+) -> Result<(), Error> {
+ artifact.validate().map_err(|_| metadata_error())?;
+ if artifact.origin() != ArtifactOrigin::ImportedSigned
+ || artifact.admission_state() != AdmissionState::Inserted
+ {
+ return Err(metadata_error());
+ }
+ let signed = artifact.signed().ok_or_else(metadata_error)?;
+ sqlx::query(
+ "INSERT INTO radroots_runtime_authored_artifacts (
+ artifact_id, operation_id, ordinal, origin, signing_state, admission_state,
+ signed_raw_json, signed_raw_sha256,
+ created_at_unix_ms, updated_at_unix_ms, revision, snapshot
+ ) VALUES (?, ?, ?, 'imported_signed', 'signed', 'inserted', ?, ?, ?, ?, ?, ?)",
+ )
+ .bind(artifact.artifact_id().as_bytes().as_slice())
+ .bind(artifact.operation_id().as_bytes().as_slice())
+ .bind(i64::from(artifact.ordinal()))
+ .bind(signed.event().raw_json().as_bytes())
+ .bind(signed.raw_json_sha256().as_slice())
+ .bind(i64_from_u64(artifact.created_at_unix_ms())?)
+ .bind(i64_from_u64(artifact.updated_at_unix_ms())?)
+ .bind(i64_from_u64(artifact.revision().get())?)
+ .bind(authored::encode_snapshot(artifact).map_err(|_| metadata_error())?)
+ .execute(&mut **transaction)
+ .await
+ .map_err(|_| metadata_error())?;
+ Ok(())
+}
+
fn replay_evidence(plan: &mut AuthoredDeliveryPlan, legacy: &OutboxRecord) -> Result<(), Error> {
let Some(last_attempt) = legacy.last_attempt() else {
if !legacy.evidence().is_empty()
diff --git a/crates/storage_sqlite/src/migration/runtime/0015_authored_signed_facts.up.sql b/crates/storage_sqlite/src/migration/runtime/0015_authored_signed_facts.up.sql
@@ -0,0 +1,27 @@
+-- Keep prior snapshots, receipts and the v11 signed/raw CHECK unchanged.
+-- A stopped artifact can retain signature facts without regaining scheduling authority.
+ALTER TABLE radroots_runtime_authored_artifacts
+ADD COLUMN signing_stop TEXT CHECK(signing_stop IS NULL OR (
+ signing_stop IN ('cancelled', 'failed_terminal')
+ AND origin = 'planned'
+ AND signing_state = 'signed'
+ AND admission_state = 'pending'
+ AND signing_claim_token IS NULL
+ AND admission_claim_token IS NULL
+ AND retry_not_before_unix_ms IS NULL
+));
+
+CREATE TRIGGER radroots_runtime_authored_artifacts_signed_fact_guard
+BEFORE UPDATE ON radroots_runtime_authored_artifacts
+WHEN
+ (OLD.signed_raw_json IS NOT NULL AND (
+ NEW.signed_raw_json IS NOT OLD.signed_raw_json
+ OR NEW.signed_raw_sha256 IS NOT OLD.signed_raw_sha256
+ ))
+ OR (OLD.signing_stop IS NOT NULL AND NEW.signing_stop IS NOT OLD.signing_stop)
+ OR (OLD.signing_state IN ('cancelled', 'failed_terminal')
+ AND NEW.signed_raw_json IS NOT NULL
+ AND NEW.signing_stop IS NOT OLD.signing_state)
+BEGIN
+ SELECT RAISE(ABORT, 'signed facts and signing stops are immutable');
+END;
diff --git a/crates/storage_sqlite/src/migration/runtime/mod.rs b/crates/storage_sqlite/src/migration/runtime/mod.rs
@@ -6,7 +6,7 @@
/// Lowest runtime schema version this package can recognize.
pub const MINIMUM_VERSION: u32 = 1;
/// Current runtime schema version created by this package.
-pub const CURRENT_VERSION: u32 = 14;
+pub const CURRENT_VERSION: u32 = 15;
const RUNTIME_V1_SQL: &str = include_str!("0001_runtime.up.sql");
const CANONICAL_EVENT_STORAGE_V2_SQL: &str = include_str!("0002_canonical_event_storage.up.sql");
@@ -26,6 +26,7 @@ const AUTHORED_DRAFT_REVISIONS_V13_SQL: &str = include_str!("0013_authored_draft
const AUTHORED_DRAFT_QUERY_V14_SQL: &str =
include_str!("0014_authored_draft_query_metadata.up.sql");
+const AUTHORED_SIGNED_FACTS_V15_SQL: &str = include_str!("0015_authored_signed_facts.up.sql");
/// Stable, non-SQL description of one forward runtime migration.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -636,6 +637,85 @@ const RUNTIME_V14_OBJECTS: &[&str] = &[
"radroots_runtime_source_generations_sequence_guard",
];
+const RUNTIME_V15_OBJECTS: &[&str] = &[
+ "radroots_runtime_atomic_commits",
+ "radroots_runtime_authored_artifacts",
+ "radroots_runtime_authored_artifacts_admission_ready_idx",
+ "radroots_runtime_authored_artifacts_signed_fact_guard",
+ "radroots_runtime_authored_artifacts_signing_ready_idx",
+ "radroots_runtime_authored_atomic_commits",
+ "radroots_runtime_authored_atomic_commits_delete_guard",
+ "radroots_runtime_authored_atomic_commits_update_guard",
+ "radroots_runtime_authored_delivery_attempts",
+ "radroots_runtime_authored_delivery_plans",
+ "radroots_runtime_authored_delivery_ready_idx",
+ "radroots_runtime_authored_delivery_targets",
+ "radroots_runtime_authored_draft_author_head_idx",
+ "radroots_runtime_authored_draft_revisions",
+ "radroots_runtime_authored_draft_revisions_delete_guard",
+ "radroots_runtime_authored_draft_revisions_update_guard",
+ "radroots_runtime_authored_draft_scope_head_idx",
+ "radroots_runtime_authored_migration_evidence",
+ "radroots_runtime_authored_migration_evidence_delete_guard",
+ "radroots_runtime_authored_migration_evidence_update_guard",
+ "radroots_runtime_authored_operations",
+ "radroots_runtime_delivery_evidence",
+ "radroots_runtime_delivery_evidence_item_idx",
+ "radroots_runtime_event_index_checkpoints",
+ "radroots_runtime_event_index_manifests",
+ "radroots_runtime_event_index_shards",
+ "radroots_runtime_event_provenance",
+ "radroots_runtime_event_provenance_observed_idx",
+ "radroots_runtime_events",
+ "radroots_runtime_events_admission_idx",
+ "radroots_runtime_events_contract_metadata_guard",
+ "radroots_runtime_events_contract_metadata_insert_guard",
+ "radroots_runtime_events_delete_guard",
+ "radroots_runtime_events_event_id_idx",
+ "radroots_runtime_events_raw_update_guard",
+ "radroots_runtime_journal_idempotency_idx",
+ "radroots_runtime_journal_operations",
+ "radroots_runtime_journal_recovery_idx",
+ "radroots_runtime_legacy_event_staging",
+ "radroots_runtime_legacy_event_staging_delete_guard",
+ "radroots_runtime_legacy_event_staging_insert_guard",
+ "radroots_runtime_legacy_event_staging_update_guard",
+ "radroots_runtime_legacy_import_commit_delete_guard",
+ "radroots_runtime_legacy_import_commit_update_guard",
+ "radroots_runtime_legacy_import_commits",
+ "radroots_runtime_legacy_import_delete_guard",
+ "radroots_runtime_legacy_import_identity_guard",
+ "radroots_runtime_legacy_import_member_delete_guard",
+ "radroots_runtime_legacy_import_member_identity_guard",
+ "radroots_runtime_legacy_import_member_state_guard",
+ "radroots_runtime_legacy_import_members",
+ "radroots_runtime_legacy_import_state_guard",
+ "radroots_runtime_legacy_import_state_idx",
+ "radroots_runtime_legacy_imports",
+ "radroots_runtime_legacy_outbox_staging",
+ "radroots_runtime_legacy_outbox_staging_delete_guard",
+ "radroots_runtime_legacy_outbox_staging_insert_guard",
+ "radroots_runtime_legacy_outbox_staging_parent_idx",
+ "radroots_runtime_legacy_outbox_staging_update_guard",
+ "radroots_runtime_outbox_items",
+ "radroots_runtime_outbox_operation_idx",
+ "radroots_runtime_outbox_ready_idx",
+ "radroots_runtime_outbox_targets",
+ "radroots_runtime_projection_checkpoints",
+ "radroots_runtime_projection_documents",
+ "radroots_runtime_projection_invalidations",
+ "radroots_runtime_projection_rebuilds",
+ "radroots_runtime_projection_rebuilds_stage_idx",
+ "radroots_runtime_projection_snapshots",
+ "radroots_runtime_projection_snapshots_created_idx",
+ "radroots_runtime_projection_snapshots_update_guard",
+ "radroots_runtime_source_generations",
+ "radroots_runtime_source_generations_active_idx",
+ "radroots_runtime_source_generations_delete_guard",
+ "radroots_runtime_source_generations_identity_guard",
+ "radroots_runtime_source_generations_sequence_guard",
+];
+
/// Ordered, immutable runtime migration plan.
pub const MIGRATIONS: &[MigrationDescriptor] = &[
MigrationDescriptor {
@@ -722,6 +802,12 @@ pub const MIGRATIONS: &[MigrationDescriptor] = &[
up_sha256: "3e108ddf9fbbc559b5e36bbdcb1032343b771f91a074aebc62ae8ad6c1521b55",
owned_objects: RUNTIME_V14_OBJECTS,
},
+ MigrationDescriptor {
+ version: 15,
+ name: "authored_signed_facts",
+ up_sha256: "d7ed7312b36f6ae633d2018d71d1bc0117097523ff46ecfaca8a99ca739aa026",
+ owned_objects: RUNTIME_V15_OBJECTS,
+ },
];
pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> {
@@ -740,6 +826,7 @@ pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> {
12 => Some(MATERIALIZED_PROJECTION_DOCUMENTS_V12_SQL),
13 => Some(AUTHORED_DRAFT_REVISIONS_V13_SQL),
14 => Some(AUTHORED_DRAFT_QUERY_V14_SQL),
+ 15 => Some(AUTHORED_SIGNED_FACTS_V15_SQL),
_ => None,
}
}
@@ -783,8 +870,8 @@ mod tests {
fn migration_plan_matches_governed_snapshot() {
let snapshot = toml::from_str::<PlanSnapshot>(PLAN_SNAPSHOT).expect("valid snapshot");
assert_eq!(MINIMUM_VERSION, 1);
- assert_eq!(CURRENT_VERSION, 14);
- assert_eq!(MIGRATIONS.len(), 14);
+ assert_eq!(CURRENT_VERSION, 15);
+ assert_eq!(MIGRATIONS.len(), 15);
let migration = MIGRATIONS[8];
assert_eq!(snapshot.schema_version, 1);
assert_eq!(snapshot.database, "runtime.sqlite");
diff --git a/crates/storage_sqlite/src/migration_draft_query_tests.rs b/crates/storage_sqlite/src/migration_draft_query_tests.rs
@@ -50,16 +50,20 @@ async fn draft_metadata_upgrade_preserves_source_and_rejects_prior_schema_policy
establish_runtime_version(&mut connection, 13).await;
let snapshots = seed(&mut connection).await;
assert!(matches!(
- migrate_runtime(&mut connection, OpenMode::ReadOnly).await,
+ migrate(&mut connection, OpenMode::ReadOnly, &plan(14, false)).await,
Err(Error::SchemaMigrationRequired {
current: 14,
actual: 13,
..
})
));
- let report = migrate_runtime(&mut connection, OpenMode::ReadWriteExisting)
- .await
- .unwrap();
+ let report = migrate(
+ &mut connection,
+ OpenMode::ReadWriteExisting,
+ &plan(14, false),
+ )
+ .await
+ .unwrap();
assert_eq!(report.applied(), 1);
assert_eq!(pragma(&mut connection, "user_version").await, 14);
let actual: Vec<Vec<u8>> = sqlx::query_scalar(
@@ -93,7 +97,7 @@ async fn draft_metadata_upgrade_preserves_source_and_rejects_prior_schema_policy
.is_err()
);
assert_eq!(
- migrate_runtime(&mut connection, OpenMode::ReadOnly)
+ migrate(&mut connection, OpenMode::ReadOnly, &plan(14, false))
.await
.unwrap()
.applied(),
@@ -136,8 +140,12 @@ async fn metadata_migration_failure_rolls_back_columns_index_guards_and_version(
.await
.is_err()
);
- migrate_runtime(&mut connection, OpenMode::ReadWriteExisting)
- .await
- .unwrap();
+ migrate(
+ &mut connection,
+ OpenMode::ReadWriteExisting,
+ &plan(14, false),
+ )
+ .await
+ .unwrap();
connection.close().await.unwrap();
}
diff --git a/crates/storage_sqlite/src/migration_signed_facts_tests.rs b/crates/storage_sqlite/src/migration_signed_facts_tests.rs
@@ -0,0 +1,188 @@
+use super::{
+ tests::{connection, establish_runtime_version, pragma},
+ *,
+};
+use crate::authored::signed_fact_fixture;
+use radroots_storage::{
+ atomic::AtomicCommitDisposition,
+ authored_atomic::{AuthoredAtomicCommand, AuthoredAtomicOutcome, AuthoredAtomicReceipt},
+};
+
+const FAILING_V15: &str = concat!(
+ include_str!("migration/runtime/0015_authored_signed_facts.up.sql"),
+ "\nINSERT INTO missing_fixture_table VALUES (1);"
+);
+
+fn plan(current: u32, fail: bool) -> MigrationPlan {
+ MigrationPlan {
+ database: RUNTIME_DATABASE,
+ application_id: RUNTIME_APPLICATION_ID,
+ set_application_id_sql: SET_RUNTIME_APPLICATION_ID,
+ minimum_version: runtime::MINIMUM_VERSION,
+ current_version: current,
+ steps: runtime::MIGRATIONS
+ .iter()
+ .take(current as usize)
+ .map(|step| MigrationStep {
+ version: step.version(),
+ sql: if fail && step.version() == 15 {
+ FAILING_V15
+ } else {
+ runtime::migration_sql(step.version()).unwrap()
+ },
+ owned_objects: step.owned_objects(),
+ })
+ .collect(),
+ }
+}
+
+async fn seed(connection: &mut SqliteConnection) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
+ let (command, _) = signed_fact_fixture::prepare();
+ let AuthoredAtomicCommand::Prepare(prepared) = &command else {
+ unreachable!()
+ };
+ let operation = prepared.operation();
+ let artifact = &prepared.artifacts()[0];
+ let operation_snapshot = serde_json::to_vec(operation).unwrap();
+ let artifact_snapshot = serde_json::to_vec(artifact).unwrap();
+ sqlx::query("INSERT INTO radroots_runtime_authored_operations (operation_id, artifact_count, created_at_unix_ms, updated_at_unix_ms, revision, snapshot) VALUES (?, 1, 10, 10, 1, ?)")
+ .bind(operation.operation_id().as_bytes().as_slice()).bind(operation_snapshot).execute(&mut *connection).await.unwrap();
+ sqlx::query("INSERT INTO radroots_runtime_authored_artifacts (artifact_id, operation_id, ordinal, origin, signing_state, admission_state, plan_wire, created_at_unix_ms, updated_at_unix_ms, revision, snapshot) VALUES (?, ?, 0, 'planned', 'planned', 'pending', ?, 10, 10, 1, ?)")
+ .bind(artifact.artifact_id().as_bytes().as_slice()).bind(operation.operation_id().as_bytes().as_slice())
+ .bind(artifact.plan().unwrap().wire_json()).bind(&artifact_snapshot).execute(&mut *connection).await.unwrap();
+ for delivery in prepared.delivery_plans() {
+ sqlx::query("INSERT INTO radroots_runtime_authored_delivery_plans (plan_id, artifact_id, request_digest, state, attempt_count, created_at_unix_ms, updated_at_unix_ms, revision, snapshot) VALUES (?, ?, ?, 'pending', 0, 10, 10, 1, ?)")
+ .bind(delivery.plan_id().as_bytes().as_slice()).bind(delivery.artifact_id().as_bytes().as_slice())
+ .bind(delivery.request_digest().as_slice()).bind(serde_json::to_vec(delivery).unwrap()).execute(&mut *connection).await.unwrap();
+ for (ordinal, target) in delivery.intent().target_set().targets().iter().enumerate() {
+ sqlx::query("INSERT INTO radroots_runtime_authored_delivery_targets (plan_id, ordinal, target_fingerprint, target_snapshot) VALUES (?, ?, ?, ?)")
+ .bind(delivery.plan_id().as_bytes().as_slice()).bind(i64::try_from(ordinal).unwrap())
+ .bind(target.fingerprint().as_str()).bind(serde_json::to_vec(target).unwrap()).execute(&mut *connection).await.unwrap();
+ }
+ }
+ let outcome = AuthoredAtomicOutcome::Prepared {
+ operation: operation.clone(),
+ artifacts: prepared.artifacts().to_vec(),
+ delivery_plans: prepared.delivery_plans().to_vec(),
+ };
+ let receipt = AuthoredAtomicReceipt::new(
+ &command,
+ AtomicCommitDisposition::Committed,
+ 10,
+ outcome.clone(),
+ )
+ .unwrap();
+ let receipt_bytes = serde_json::to_vec(&serde_json::json!({"outcome": outcome})).unwrap();
+ sqlx::query("INSERT INTO radroots_runtime_authored_atomic_commits (commit_id, commit_digest, phase, target_id, requested_at_unix_ms, committed_at_unix_ms, receipt) VALUES (?, ?, 'prepare', ?, 10, 10, ?)")
+ .bind(receipt.commit_id().as_bytes().as_slice()).bind(receipt.digest().as_bytes().as_slice())
+ .bind(operation.operation_id().as_bytes().as_slice()).bind(&receipt_bytes).execute(&mut *connection).await.unwrap();
+ (
+ artifact_snapshot,
+ receipt_bytes,
+ receipt.commit_id().as_bytes().to_vec(),
+ )
+}
+
+async fn retained(connection: &mut SqliteConnection, expected: &(Vec<u8>, Vec<u8>, Vec<u8>)) {
+ let artifact: Vec<u8> =
+ sqlx::query_scalar("SELECT snapshot FROM radroots_runtime_authored_artifacts")
+ .fetch_one(&mut *connection)
+ .await
+ .unwrap();
+ let (receipt, id): (Vec<u8>, Vec<u8>) =
+ sqlx::query_as("SELECT receipt, commit_id FROM radroots_runtime_authored_atomic_commits")
+ .fetch_one(&mut *connection)
+ .await
+ .unwrap();
+ assert_eq!((artifact, receipt, id), *expected);
+}
+
+#[tokio::test]
+async fn v15_preserves_v14_rows_and_receipts_and_prior_schema_policy_fails_closed() {
+ let mut connection = connection().await;
+ establish_runtime_version(&mut connection, 14).await;
+ let old = seed(&mut connection).await;
+ assert!(matches!(
+ migrate_runtime(&mut connection, OpenMode::ReadOnly).await,
+ Err(Error::SchemaMigrationRequired {
+ actual: 14,
+ current: 15,
+ ..
+ })
+ ));
+ assert_eq!(
+ migrate_runtime(&mut connection, OpenMode::ReadWriteExisting)
+ .await
+ .unwrap()
+ .applied(),
+ 1
+ );
+ assert_eq!(pragma(&mut connection, "user_version").await, 15);
+ retained(&mut connection, &old).await;
+ let stop: Option<String> =
+ sqlx::query_scalar("SELECT signing_stop FROM radroots_runtime_authored_artifacts")
+ .fetch_one(&mut connection)
+ .await
+ .unwrap();
+ assert_eq!(stop, None);
+ for mode in [OpenMode::ReadOnly, OpenMode::ReadWriteExisting] {
+ assert!(matches!(
+ migrate(&mut connection, mode, &plan(14, false)).await,
+ Err(Error::SchemaTooNew {
+ actual: 15,
+ supported: 14,
+ ..
+ })
+ ));
+ }
+ assert_eq!(
+ migrate_runtime(&mut connection, OpenMode::ReadOnly)
+ .await
+ .unwrap()
+ .applied(),
+ 0
+ );
+ connection.close().await.unwrap();
+}
+
+#[tokio::test]
+async fn failed_v15_migration_rolls_back_column_guard_version_and_historical_bytes() {
+ let mut connection = connection().await;
+ establish_runtime_version(&mut connection, 14).await;
+ let old = seed(&mut connection).await;
+ assert!(
+ migrate(
+ &mut connection,
+ OpenMode::ReadWriteExisting,
+ &plan(15, true)
+ )
+ .await
+ .is_err()
+ );
+ assert_eq!(pragma(&mut connection, "user_version").await, 14);
+ retained(&mut connection, &old).await;
+ assert!(
+ sqlx::query("SELECT signing_stop FROM radroots_runtime_authored_artifacts")
+ .fetch_all(&mut connection)
+ .await
+ .is_err()
+ );
+ assert_eq!(sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM sqlite_schema WHERE name = 'radroots_runtime_authored_artifacts_signed_fact_guard'").fetch_one(&mut connection).await.unwrap(), 0);
+ migrate_runtime(&mut connection, OpenMode::ReadWriteExisting)
+ .await
+ .unwrap();
+ retained(&mut connection, &old).await;
+ connection.close().await.unwrap();
+}
+
+#[test]
+fn signed_fact_decision_binds_the_exact_successor_migration() {
+ let decision: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../contracts/architecture/decisions/authored_signed_facts.v1.json"
+ ))
+ .unwrap();
+ let migration = runtime::MIGRATIONS[14];
+ assert_eq!(decision["migration"]["version"], migration.version());
+ assert_eq!(decision["migration"]["name"], migration.name());
+ assert_eq!(decision["migration"]["sha256"], migration.up_sha256());
+}