rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

commit 46d77b432290040e236273a01f3556c933c3732b
parent a907604040814954fc02fde21a4d1bb1a19f0beb
Author: triesap <tyson@radroots.org>
Date:   Sun, 23 Aug 2026 21:56:56 +0000

state(rhi): define typed repository topology

Diffstat:
MREADME | 12++++++++++++
Acontracts/services_hardening/state_repository_topology.v1.json | 35+++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 13+++++++++++++
Msrc/state_host.rs | 10++++++++--
Asrc/state_repository.rs | 491+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/services_hardening_state_host.rs | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Atests/services_hardening_state_repository_topology.rs | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 908 insertions(+), 5 deletions(-)

diff --git a/README b/README @@ -139,6 +139,18 @@ credential remain excluded from the state-backup contract. Actual online backup, offline restore, and recovery execution remain owned by their later ordered checkpoint and are not claimed by this boundary proof. +RHI's state surface is partitioned into eighteen distinct non-forgeable typed +repository capabilities bound to one already-opened `RhiStateHost`. Their +closed topology covers source results, cursors, completions, admitted signed +events, canonical mutations, provenance, dirty generations, reconciliation +jobs and attempts, immutable manifests/projections/reports, supersession, +signed attestation events, publication outbox/targets/attempts, and desired +presence. Each capability has one exact backing-table identity and an +append-only, compare-and-swap, or immutable write class. It exposes no raw +pool, connection, transaction, SQL, path, or cloneable write authority. +Schema migration and verification, CRUD behavior, backup/restore, network I/O, +and task supervision remain owned by their later ordered checkpoints. + Validate the standalone crate through extbuild: ```text diff --git a/contracts/services_hardening/state_repository_topology.v1.json b/contracts/services_hardening/state_repository_topology.v1.json @@ -0,0 +1,35 @@ +{ + "schema": "radroots.rhi.state-repository-topology", + "schema_version": 1, + "contract_version": 1, + "repository_count": 18, + "construction": "sealed_to_open_rhi_state_host", + "raw_sqlite_authority_exposed": false, + "repositories": [ + { "kind": "source", "backing_table": "evidence_reconciliation_sources", "write_class": "append_only" }, + { "kind": "source_cursor", "backing_table": "relay_checkpoints", "write_class": "compare_and_swap" }, + { "kind": "source_completion", "backing_table": "evidence_reconciliation_sources", "write_class": "append_only" }, + { "kind": "signed_event", "backing_table": "nostr_events", "write_class": "append_only" }, + { "kind": "mutation", "backing_table": "trade_mutations", "write_class": "append_only" }, + { "kind": "provenance", "backing_table": "relay_observations", "write_class": "append_only" }, + { "kind": "dirty_trade", "backing_table": "trade_dirty_generations", "write_class": "compare_and_swap" }, + { "kind": "reconciliation_job", "backing_table": "reconciliation_jobs", "write_class": "compare_and_swap" }, + { "kind": "reconciliation_attempt", "backing_table": "evidence_reconciliations", "write_class": "append_only" }, + { "kind": "evidence_manifest", "backing_table": "evidence_manifests", "write_class": "immutable" }, + { "kind": "projection", "backing_table": "trade_projections", "write_class": "immutable" }, + { "kind": "report", "backing_table": "attestation_reports", "write_class": "immutable" }, + { "kind": "supersession", "backing_table": "attestation_reports", "write_class": "append_only" }, + { "kind": "signed_attestation_event", "backing_table": "signed_attestation_events", "write_class": "immutable" }, + { "kind": "publication_outbox", "backing_table": "publication_outbox", "write_class": "compare_and_swap" }, + { "kind": "publication_target", "backing_table": "publication_targets", "write_class": "compare_and_swap" }, + { "kind": "publication_attempt", "backing_table": "publication_attempts", "write_class": "append_only" }, + { "kind": "desired_presence", "backing_table": "presence_desired_state", "write_class": "compare_and_swap" } + ], + "deferred_behavior": [ + "schema_migration_and_verification", + "repository_crud", + "backup_restore_and_recovery", + "network_io", + "task_supervision" + ] +} diff --git a/src/lib.rs b/src/lib.rs @@ -10,6 +10,7 @@ mod runtime_context; mod state_catalog; mod state_host; mod state_metadata; +mod state_repository; pub use cli_v1::{ RhiBootstrapProfileV1, RhiCliInvocationV1, RhiCliOutputModeV1, RhiCliV1Error, @@ -61,3 +62,15 @@ pub use state_metadata::{ RhiNormalizedConfigDigest, RhiStateMetadata, RhiStateMetadataError, RhiStateMetadataErrorKind, RhiStatePolicyVersions, }; +pub use state_repository::{ + RHI_STATE_REPOSITORY_CONTRACT_VERSION, RHI_STATE_REPOSITORY_COUNT, + RhiDesiredPresenceRepository, RhiDirtyTradeRepository, RhiEvidenceManifestRepository, + RhiMutationRepository, RhiProjectionRepository, RhiProvenanceRepository, + RhiPublicationAttemptRepository, RhiPublicationOutboxRepository, + RhiPublicationTargetRepository, RhiReconciliationAttemptRepository, + RhiReconciliationJobRepository, RhiReportRepository, RhiSignedAttestationEventRepository, + RhiSignedEventRepository, RhiSourceCompletionRepository, RhiSourceCursorRepository, + RhiSourceRepository, RhiStateRepositories, RhiStateRepositoryDescriptor, + RhiStateRepositoryKind, RhiStateRepositoryWriteClass, RhiSupersessionRepository, + rhi_state_repository_descriptors, +}; diff --git a/src/state_host.rs b/src/state_host.rs @@ -10,8 +10,8 @@ use radroots_service_sqlite::{ use sqlx::{ConnectOptions, Connection, SqliteConnection, sqlite::SqliteConnectOptions}; use crate::{ - RHI_STATE_SCHEMA_VERSION, RhiRuntimeContext, RhiStateMetadata, rhi_migration_catalog, - rhi_schema_catalog, validate_rhi_state_catalogs, + RHI_STATE_SCHEMA_VERSION, RhiRuntimeContext, RhiStateMetadata, RhiStateRepositories, + rhi_migration_catalog, rhi_schema_catalog, validate_rhi_state_catalogs, }; /// Stable lifecycle mode of one opened RHI state host. @@ -139,6 +139,12 @@ impl RhiStateHost { &self.metadata } + /// Returns the sealed family of typed repository capabilities. + #[must_use] + pub const fn repositories(&self) -> RhiStateRepositories<'_> { + RhiStateRepositories::new(self) + } + /// Drains the shared host and explicitly releases retained authority. pub async fn close(&self) -> Result<(), RhiStateHostError> { self.host diff --git a/src/state_repository.rs b/src/state_repository.rs @@ -0,0 +1,491 @@ +//! Sealed typed capability topology for RHI-owned state repositories. + +use core::fmt; + +use crate::RhiStateHost; + +/// Exact version of the RHI state-repository topology contract. +pub const RHI_STATE_REPOSITORY_CONTRACT_VERSION: u32 = 1; + +/// Number of distinct typed repository capabilities in the v1 topology. +pub const RHI_STATE_REPOSITORY_COUNT: usize = 18; + +/// Closed mutation class for one governed repository. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum RhiStateRepositoryWriteClass { + AppendOnly, + CompareAndSwap, + Immutable, +} + +impl RhiStateRepositoryWriteClass { + /// Returns the exact machine-contract spelling. + #[must_use] + pub const fn code(self) -> &'static str { + match self { + Self::AppendOnly => "append_only", + Self::CompareAndSwap => "compare_and_swap", + Self::Immutable => "immutable", + } + } +} + +/// Closed inventory of RHI repository responsibilities. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum RhiStateRepositoryKind { + Source, + SourceCursor, + SourceCompletion, + SignedEvent, + Mutation, + Provenance, + DirtyTrade, + ReconciliationJob, + ReconciliationAttempt, + EvidenceManifest, + Projection, + Report, + Supersession, + SignedAttestationEvent, + PublicationOutbox, + PublicationTarget, + PublicationAttempt, + DesiredPresence, +} + +impl RhiStateRepositoryKind { + /// Returns the exact machine-contract spelling. + #[must_use] + pub const fn code(self) -> &'static str { + descriptor(self).code + } + + /// Returns the sole governed backing-table identity. + #[must_use] + pub const fn backing_table(self) -> &'static str { + descriptor(self).backing_table + } + + /// Returns the closed mutation class. + #[must_use] + pub const fn write_class(self) -> RhiStateRepositoryWriteClass { + descriptor(self).write_class + } +} + +/// Immutable description of one repository capability. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct RhiStateRepositoryDescriptor { + kind: RhiStateRepositoryKind, + code: &'static str, + backing_table: &'static str, + write_class: RhiStateRepositoryWriteClass, +} + +impl RhiStateRepositoryDescriptor { + const fn new( + kind: RhiStateRepositoryKind, + code: &'static str, + backing_table: &'static str, + write_class: RhiStateRepositoryWriteClass, + ) -> Self { + Self { + kind, + code, + backing_table, + write_class, + } + } + + /// Returns the closed repository kind. + #[must_use] + pub const fn kind(self) -> RhiStateRepositoryKind { + self.kind + } + + /// Returns the exact machine-contract spelling. + #[must_use] + pub const fn code(self) -> &'static str { + self.code + } + + /// Returns the exact governed backing-table identity. + #[must_use] + pub const fn backing_table(self) -> &'static str { + self.backing_table + } + + /// Returns the repository mutation class. + #[must_use] + pub const fn write_class(self) -> RhiStateRepositoryWriteClass { + self.write_class + } +} + +impl fmt::Debug for RhiStateRepositoryDescriptor { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiStateRepositoryDescriptor") + .field("kind", &self.kind) + .field("write_class", &self.write_class) + .finish() + } +} + +use RhiStateRepositoryKind as Kind; +use RhiStateRepositoryWriteClass as Write; + +const DESCRIPTORS: [RhiStateRepositoryDescriptor; RHI_STATE_REPOSITORY_COUNT] = [ + RhiStateRepositoryDescriptor::new( + Kind::Source, + "source", + "evidence_reconciliation_sources", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::SourceCursor, + "source_cursor", + "relay_checkpoints", + Write::CompareAndSwap, + ), + RhiStateRepositoryDescriptor::new( + Kind::SourceCompletion, + "source_completion", + "evidence_reconciliation_sources", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::SignedEvent, + "signed_event", + "nostr_events", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::Mutation, + "mutation", + "trade_mutations", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::Provenance, + "provenance", + "relay_observations", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::DirtyTrade, + "dirty_trade", + "trade_dirty_generations", + Write::CompareAndSwap, + ), + RhiStateRepositoryDescriptor::new( + Kind::ReconciliationJob, + "reconciliation_job", + "reconciliation_jobs", + Write::CompareAndSwap, + ), + RhiStateRepositoryDescriptor::new( + Kind::ReconciliationAttempt, + "reconciliation_attempt", + "evidence_reconciliations", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::EvidenceManifest, + "evidence_manifest", + "evidence_manifests", + Write::Immutable, + ), + RhiStateRepositoryDescriptor::new( + Kind::Projection, + "projection", + "trade_projections", + Write::Immutable, + ), + RhiStateRepositoryDescriptor::new( + Kind::Report, + "report", + "attestation_reports", + Write::Immutable, + ), + RhiStateRepositoryDescriptor::new( + Kind::Supersession, + "supersession", + "attestation_reports", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::SignedAttestationEvent, + "signed_attestation_event", + "signed_attestation_events", + Write::Immutable, + ), + RhiStateRepositoryDescriptor::new( + Kind::PublicationOutbox, + "publication_outbox", + "publication_outbox", + Write::CompareAndSwap, + ), + RhiStateRepositoryDescriptor::new( + Kind::PublicationTarget, + "publication_target", + "publication_targets", + Write::CompareAndSwap, + ), + RhiStateRepositoryDescriptor::new( + Kind::PublicationAttempt, + "publication_attempt", + "publication_attempts", + Write::AppendOnly, + ), + RhiStateRepositoryDescriptor::new( + Kind::DesiredPresence, + "desired_presence", + "presence_desired_state", + Write::CompareAndSwap, + ), +]; + +const fn descriptor(kind: RhiStateRepositoryKind) -> RhiStateRepositoryDescriptor { + DESCRIPTORS[kind as usize] +} + +/// Returns the exact ordered v1 repository topology. +#[must_use] +pub const fn rhi_state_repository_descriptors() +-> &'static [RhiStateRepositoryDescriptor; RHI_STATE_REPOSITORY_COUNT] { + &DESCRIPTORS +} + +/// Sealed repository family bound to one already-opened RHI state host. +/// +/// Construction is available only through [`RhiStateHost::repositories`]: +/// +/// ```compile_fail +/// use rhi::RhiStateRepositories; +/// +/// let _ = RhiStateRepositories { host: todo!() }; +/// ``` +/// +/// Distinct repository responsibilities cannot be interchanged: +/// +/// ```compile_fail +/// use rhi::{RhiMutationRepository, RhiStateRepositories}; +/// +/// fn wrong(repository: &RhiStateRepositories<'_>) { +/// let _: RhiMutationRepository<'_> = repository.sources(); +/// } +/// ``` +pub struct RhiStateRepositories<'host> { + host: &'host RhiStateHost, +} + +impl<'host> RhiStateRepositories<'host> { + pub(crate) const fn new(host: &'host RhiStateHost) -> Self { + Self { host } + } + + /// Returns typed source-result access. + #[must_use] + pub const fn sources(&self) -> RhiSourceRepository<'host> { + RhiSourceRepository { host: self.host } + } + + /// Returns typed source-cursor access. + #[must_use] + pub const fn source_cursors(&self) -> RhiSourceCursorRepository<'host> { + RhiSourceCursorRepository { host: self.host } + } + + /// Returns typed source-completion access. + #[must_use] + pub const fn source_completions(&self) -> RhiSourceCompletionRepository<'host> { + RhiSourceCompletionRepository { host: self.host } + } + + /// Returns typed admitted-event access. + #[must_use] + pub const fn signed_events(&self) -> RhiSignedEventRepository<'host> { + RhiSignedEventRepository { host: self.host } + } + + /// Returns typed canonical-mutation access. + #[must_use] + pub const fn mutations(&self) -> RhiMutationRepository<'host> { + RhiMutationRepository { host: self.host } + } + + /// Returns typed provenance-observation access. + #[must_use] + pub const fn provenance(&self) -> RhiProvenanceRepository<'host> { + RhiProvenanceRepository { host: self.host } + } + + /// Returns typed dirty-trade generation access. + #[must_use] + pub const fn dirty_trades(&self) -> RhiDirtyTradeRepository<'host> { + RhiDirtyTradeRepository { host: self.host } + } + + /// Returns typed reconciliation-job access. + #[must_use] + pub const fn reconciliation_jobs(&self) -> RhiReconciliationJobRepository<'host> { + RhiReconciliationJobRepository { host: self.host } + } + + /// Returns typed reconciliation-attempt access. + #[must_use] + pub const fn reconciliation_attempts(&self) -> RhiReconciliationAttemptRepository<'host> { + RhiReconciliationAttemptRepository { host: self.host } + } + + /// Returns typed immutable evidence-manifest access. + #[must_use] + pub const fn evidence_manifests(&self) -> RhiEvidenceManifestRepository<'host> { + RhiEvidenceManifestRepository { host: self.host } + } + + /// Returns typed immutable projection access. + #[must_use] + pub const fn projections(&self) -> RhiProjectionRepository<'host> { + RhiProjectionRepository { host: self.host } + } + + /// Returns typed immutable report access. + #[must_use] + pub const fn reports(&self) -> RhiReportRepository<'host> { + RhiReportRepository { host: self.host } + } + + /// Returns typed append-only supersession access. + #[must_use] + pub const fn supersessions(&self) -> RhiSupersessionRepository<'host> { + RhiSupersessionRepository { host: self.host } + } + + /// Returns typed immutable signed-attestation-event access. + #[must_use] + pub const fn signed_attestation_events(&self) -> RhiSignedAttestationEventRepository<'host> { + RhiSignedAttestationEventRepository { host: self.host } + } + + /// Returns typed publication-outbox access. + #[must_use] + pub const fn publication_outbox(&self) -> RhiPublicationOutboxRepository<'host> { + RhiPublicationOutboxRepository { host: self.host } + } + + /// Returns typed immutable-target workflow access. + #[must_use] + pub const fn publication_targets(&self) -> RhiPublicationTargetRepository<'host> { + RhiPublicationTargetRepository { host: self.host } + } + + /// Returns typed append-only publication-attempt access. + #[must_use] + pub const fn publication_attempts(&self) -> RhiPublicationAttemptRepository<'host> { + RhiPublicationAttemptRepository { host: self.host } + } + + /// Returns typed desired-presence access. + #[must_use] + pub const fn desired_presence(&self) -> RhiDesiredPresenceRepository<'host> { + RhiDesiredPresenceRepository { host: self.host } + } +} + +impl fmt::Debug for RhiStateRepositories<'_> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiStateRepositories") + .field("mode", &self.host.mode()) + .field("state", &"[sealed]") + .finish() + } +} + +macro_rules! repository_handle { + ($name:ident, $kind:ident) => { + #[doc = "One non-forgeable typed view of an opened RHI state host."] + pub struct $name<'host> { + host: &'host RhiStateHost, + } + + impl $name<'_> { + /// Returns this repository's closed kind. + #[must_use] + pub const fn kind(&self) -> RhiStateRepositoryKind { + Kind::$kind + } + + /// Returns this repository's immutable descriptor. + #[must_use] + pub const fn descriptor(&self) -> RhiStateRepositoryDescriptor { + descriptor(self.kind()) + } + } + + impl fmt::Debug for $name<'_> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct(stringify!($name)) + .field("mode", &self.host.mode()) + .field("state", &"[sealed]") + .finish() + } + } + }; +} + +repository_handle!(RhiSourceRepository, Source); +repository_handle!(RhiSourceCursorRepository, SourceCursor); +repository_handle!(RhiSourceCompletionRepository, SourceCompletion); +repository_handle!(RhiSignedEventRepository, SignedEvent); +repository_handle!(RhiMutationRepository, Mutation); +repository_handle!(RhiProvenanceRepository, Provenance); +repository_handle!(RhiDirtyTradeRepository, DirtyTrade); +repository_handle!(RhiReconciliationJobRepository, ReconciliationJob); +repository_handle!(RhiReconciliationAttemptRepository, ReconciliationAttempt); +repository_handle!(RhiEvidenceManifestRepository, EvidenceManifest); +repository_handle!(RhiProjectionRepository, Projection); +repository_handle!(RhiReportRepository, Report); +repository_handle!(RhiSupersessionRepository, Supersession); +repository_handle!(RhiSignedAttestationEventRepository, SignedAttestationEvent); +repository_handle!(RhiPublicationOutboxRepository, PublicationOutbox); +repository_handle!(RhiPublicationTargetRepository, PublicationTarget); +repository_handle!(RhiPublicationAttemptRepository, PublicationAttempt); +repository_handle!(RhiDesiredPresenceRepository, DesiredPresence); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn descriptors_are_closed_unique_and_nonempty() { + assert_eq!(DESCRIPTORS.len(), RHI_STATE_REPOSITORY_COUNT); + for (index, descriptor) in DESCRIPTORS.iter().enumerate() { + assert_eq!(descriptor.kind() as usize, index); + assert!(!descriptor.code().is_empty()); + assert!(!descriptor.backing_table().is_empty()); + assert_eq!(descriptor.kind().code(), descriptor.code()); + assert_eq!( + descriptor.kind().backing_table(), + descriptor.backing_table() + ); + assert_eq!(descriptor.kind().write_class(), descriptor.write_class()); + assert_eq!( + descriptor.write_class().code(), + match descriptor.write_class() { + Write::AppendOnly => "append_only", + Write::CompareAndSwap => "compare_and_swap", + Write::Immutable => "immutable", + } + ); + for other in &DESCRIPTORS[index + 1..] { + assert_ne!(descriptor.kind(), other.kind()); + assert_ne!(descriptor.code(), other.code()); + } + } + } +} diff --git a/tests/services_hardening_state_host.rs b/tests/services_hardening_state_host.rs @@ -7,9 +7,9 @@ use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdent use radroots_storage::event::SourceGeneration; use rhi::{ RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiConfigProfile, - RhiStateHostErrorKind, RhiStateHostMode, RhiStateMetadata, initialize_rhi_state, - open_rhi_state_inspection, open_rhi_state_read_write, parse_rhi_cli_v1_from, - parse_rhi_config_v1, resolve_rhi_runtime_context, + RhiStateHostErrorKind, RhiStateHostMode, RhiStateMetadata, RhiStateRepositoryKind, + initialize_rhi_state, open_rhi_state_inspection, open_rhi_state_read_write, + parse_rhi_cli_v1_from, parse_rhi_config_v1, resolve_rhi_runtime_context, }; const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); @@ -124,6 +124,83 @@ async fn initialize_is_create_new_and_both_existing_open_modes_close_explicitly( .await .expect("existing inspection state"); assert_eq!(inspection.mode(), RhiStateHostMode::ReadOnlyInspection); + let repositories = inspection.repositories(); + assert_eq!( + format!("{repositories:?}"), + "RhiStateRepositories { mode: ReadOnlyInspection, state: \"[sealed]\" }" + ); + assert_eq!( + repositories.sources().kind(), + RhiStateRepositoryKind::Source + ); + assert_eq!( + repositories.source_cursors().kind(), + RhiStateRepositoryKind::SourceCursor + ); + assert_eq!( + repositories.source_completions().kind(), + RhiStateRepositoryKind::SourceCompletion + ); + assert_eq!( + repositories.signed_events().kind(), + RhiStateRepositoryKind::SignedEvent + ); + assert_eq!( + repositories.mutations().kind(), + RhiStateRepositoryKind::Mutation + ); + assert_eq!( + repositories.provenance().kind(), + RhiStateRepositoryKind::Provenance + ); + assert_eq!( + repositories.dirty_trades().kind(), + RhiStateRepositoryKind::DirtyTrade + ); + assert_eq!( + repositories.reconciliation_jobs().kind(), + RhiStateRepositoryKind::ReconciliationJob + ); + assert_eq!( + repositories.reconciliation_attempts().kind(), + RhiStateRepositoryKind::ReconciliationAttempt + ); + assert_eq!( + repositories.evidence_manifests().kind(), + RhiStateRepositoryKind::EvidenceManifest + ); + assert_eq!( + repositories.projections().kind(), + RhiStateRepositoryKind::Projection + ); + assert_eq!( + repositories.reports().kind(), + RhiStateRepositoryKind::Report + ); + assert_eq!( + repositories.supersessions().kind(), + RhiStateRepositoryKind::Supersession + ); + assert_eq!( + repositories.signed_attestation_events().kind(), + RhiStateRepositoryKind::SignedAttestationEvent + ); + assert_eq!( + repositories.publication_outbox().kind(), + RhiStateRepositoryKind::PublicationOutbox + ); + assert_eq!( + repositories.publication_targets().kind(), + RhiStateRepositoryKind::PublicationTarget + ); + assert_eq!( + repositories.publication_attempts().kind(), + RhiStateRepositoryKind::PublicationAttempt + ); + assert_eq!( + repositories.desired_presence().kind(), + RhiStateRepositoryKind::DesiredPresence + ); inspection.close().await.expect("inspection close"); let writer = open_rhi_state_read_write(&runtime, &metadata, applied_at, &build) diff --git a/tests/services_hardening_state_repository_topology.rs b/tests/services_hardening_state_repository_topology.rs @@ -0,0 +1,269 @@ +#![forbid(unsafe_code)] + +use rhi::{ + RHI_STATE_REPOSITORY_CONTRACT_VERSION, RHI_STATE_REPOSITORY_COUNT, RhiStateRepositoryKind, + RhiStateRepositoryWriteClass, rhi_state_repository_descriptors, +}; +use serde_json::json; + +const CONTRACT: &str = + include_str!("../contracts/services_hardening/state_repository_topology.v1.json"); +const LIB_SOURCE: &str = include_str!("../src/lib.rs"); +const HOST_SOURCE: &str = include_str!("../src/state_host.rs"); +const REPOSITORY_SOURCE: &str = include_str!("../src/state_repository.rs"); + +#[test] +fn machine_contract_and_typed_descriptor_inventory_are_exact() { + let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("repository contract"); + assert_eq!(contract["schema"], "radroots.rhi.state-repository-topology"); + assert_eq!(contract["schema_version"], 1); + assert_eq!( + contract["contract_version"], + RHI_STATE_REPOSITORY_CONTRACT_VERSION + ); + assert_eq!(contract["repository_count"], RHI_STATE_REPOSITORY_COUNT); + assert_eq!(contract["construction"], "sealed_to_open_rhi_state_host"); + assert_eq!(contract["raw_sqlite_authority_exposed"], false); + assert_eq!( + contract["deferred_behavior"], + json!([ + "schema_migration_and_verification", + "repository_crud", + "backup_restore_and_recovery", + "network_io", + "task_supervision" + ]) + ); + assert_eq!( + contract + .as_object() + .expect("contract object") + .keys() + .map(String::as_str) + .collect::<std::collections::BTreeSet<_>>(), + [ + "schema", + "schema_version", + "contract_version", + "repository_count", + "construction", + "raw_sqlite_authority_exposed", + "repositories", + "deferred_behavior", + ] + .into_iter() + .collect() + ); + + let descriptors = rhi_state_repository_descriptors(); + let actual = descriptors + .iter() + .map(|descriptor| { + json!({ + "kind": descriptor.code(), + "backing_table": descriptor.backing_table(), + "write_class": descriptor.write_class().code(), + }) + }) + .collect::<Vec<_>>(); + assert_eq!( + contract["repositories"] + .as_array() + .expect("repository array"), + &actual + ); + assert_eq!(descriptors.len(), RHI_STATE_REPOSITORY_COUNT); + for repository in contract["repositories"] + .as_array() + .expect("repository array") + { + assert_eq!( + repository + .as_object() + .expect("repository object") + .keys() + .map(String::as_str) + .collect::<std::collections::BTreeSet<_>>(), + ["kind", "backing_table", "write_class"] + .into_iter() + .collect() + ); + } +} + +#[test] +fn repository_kinds_are_closed_ordered_and_cross_bound() { + use RhiStateRepositoryKind as Kind; + use RhiStateRepositoryWriteClass as Write; + + let expected = [ + ( + Kind::Source, + "source", + "evidence_reconciliation_sources", + Write::AppendOnly, + ), + ( + Kind::SourceCursor, + "source_cursor", + "relay_checkpoints", + Write::CompareAndSwap, + ), + ( + Kind::SourceCompletion, + "source_completion", + "evidence_reconciliation_sources", + Write::AppendOnly, + ), + ( + Kind::SignedEvent, + "signed_event", + "nostr_events", + Write::AppendOnly, + ), + ( + Kind::Mutation, + "mutation", + "trade_mutations", + Write::AppendOnly, + ), + ( + Kind::Provenance, + "provenance", + "relay_observations", + Write::AppendOnly, + ), + ( + Kind::DirtyTrade, + "dirty_trade", + "trade_dirty_generations", + Write::CompareAndSwap, + ), + ( + Kind::ReconciliationJob, + "reconciliation_job", + "reconciliation_jobs", + Write::CompareAndSwap, + ), + ( + Kind::ReconciliationAttempt, + "reconciliation_attempt", + "evidence_reconciliations", + Write::AppendOnly, + ), + ( + Kind::EvidenceManifest, + "evidence_manifest", + "evidence_manifests", + Write::Immutable, + ), + ( + Kind::Projection, + "projection", + "trade_projections", + Write::Immutable, + ), + ( + Kind::Report, + "report", + "attestation_reports", + Write::Immutable, + ), + ( + Kind::Supersession, + "supersession", + "attestation_reports", + Write::AppendOnly, + ), + ( + Kind::SignedAttestationEvent, + "signed_attestation_event", + "signed_attestation_events", + Write::Immutable, + ), + ( + Kind::PublicationOutbox, + "publication_outbox", + "publication_outbox", + Write::CompareAndSwap, + ), + ( + Kind::PublicationTarget, + "publication_target", + "publication_targets", + Write::CompareAndSwap, + ), + ( + Kind::PublicationAttempt, + "publication_attempt", + "publication_attempts", + Write::AppendOnly, + ), + ( + Kind::DesiredPresence, + "desired_presence", + "presence_desired_state", + Write::CompareAndSwap, + ), + ]; + for (descriptor, (kind, code, table, write_class)) in + rhi_state_repository_descriptors().iter().zip(expected) + { + assert_eq!(descriptor.kind(), kind); + assert_eq!(descriptor.code(), code); + assert_eq!(descriptor.backing_table(), table); + assert_eq!(descriptor.write_class(), write_class); + } +} + +#[test] +fn capabilities_are_private_sealed_and_defer_unowned_behavior() { + assert!(LIB_SOURCE.contains("mod state_repository;")); + assert!(!LIB_SOURCE.contains("pub mod state_repository;")); + assert!(HOST_SOURCE.contains("pub const fn repositories(&self) -> RhiStateRepositories<'_>")); + for required in [ + "pub const fn sources(&self) -> RhiSourceRepository<'host>", + "pub const fn source_cursors(&self) -> RhiSourceCursorRepository<'host>", + "pub const fn source_completions(&self) -> RhiSourceCompletionRepository<'host>", + "pub const fn signed_events(&self) -> RhiSignedEventRepository<'host>", + "pub const fn mutations(&self) -> RhiMutationRepository<'host>", + "pub const fn provenance(&self) -> RhiProvenanceRepository<'host>", + "pub const fn dirty_trades(&self) -> RhiDirtyTradeRepository<'host>", + "pub const fn reconciliation_jobs(&self) -> RhiReconciliationJobRepository<'host>", + "pub const fn reconciliation_attempts(&self) -> RhiReconciliationAttemptRepository<'host>", + "pub const fn evidence_manifests(&self) -> RhiEvidenceManifestRepository<'host>", + "pub const fn projections(&self) -> RhiProjectionRepository<'host>", + "pub const fn reports(&self) -> RhiReportRepository<'host>", + "pub const fn supersessions(&self) -> RhiSupersessionRepository<'host>", + "pub const fn signed_attestation_events(&self) -> RhiSignedAttestationEventRepository<'host>", + "pub const fn publication_outbox(&self) -> RhiPublicationOutboxRepository<'host>", + "pub const fn publication_targets(&self) -> RhiPublicationTargetRepository<'host>", + "pub const fn publication_attempts(&self) -> RhiPublicationAttemptRepository<'host>", + "pub const fn desired_presence(&self) -> RhiDesiredPresenceRepository<'host>", + ] { + assert!(REPOSITORY_SOURCE.contains(required), "missing {required}"); + } + for forbidden in [ + "SqlitePool", + "SqliteConnection", + "ServiceSqliteTransaction", + "sqlx::", + "rusqlite::", + "CREATE TABLE", + "INSERT INTO", + "UPDATE ", + "DELETE FROM", + "std::fs", + "std::path", + "std::env", + "tokio::", + "nostr::", + "Deref", + "AsRef", + ] { + assert!( + !REPOSITORY_SOURCE.contains(forbidden), + "premature or escaping repository authority {forbidden}" + ); + } +}