commit 19f9fc20b3033fa302bf40197a14a43e003f6bc8
parent f7288bd533d9e381947a24567f02e61a8fc33290
Author: triesap <tyson@radroots.org>
Date: Mon, 24 Aug 2026 12:24:38 +0000
refactor(rhi): define publication attempt evidence
Diffstat:
9 files changed, 923 insertions(+), 4 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -292,6 +292,14 @@
the bounded stored-byte digest on every read, including after reopen. The
capability is not claim authority; later relay execution must borrow its
exact byte slice without parsing or reconstruction.
+- Model publication target and attempt evidence only with the closed pending,
+ submitted, accepted, rejected, rate-limited, auth-required, failed, and
+ unknown vocabulary. Attempt evidence must derive its identity from the
+ sealed committed outbox and exact event digest plus bounded target ordinal
+ and attempt number; never accept caller-supplied identities, arbitrary result
+ codes, raw relay diagnostics, reversed timestamps, or implicit clock reads.
+ The model is not claim, transition, or relay authority. The durable executor
+ must revalidate every live target, lease, revision, attempt, and byte binding.
- Commit one exact reconciliation-attempt replay inventory only through the
typed attempt repository. Revalidate the exact live lease, dirty generation,
evidence policy, and every scoped prior checkpoint before mutation; persist
diff --git a/README b/README
@@ -377,11 +377,30 @@ return the same committed byte string or fail closed.
The capability is not a relay claim or lease. Its only payload accessor returns
the stored bytes unchanged; this path contains no JSON or event parsing,
rebuilding, reserialization, signing, relay/network/filesystem work, task
-spawn, or ambient clock/entropy access. Target states, attempt evidence, relay
-I/O, retry scheduling, and lease recovery remain with Steps 201-202. The exact
-machine contract is
+spawn, or ambient clock/entropy access. The closed target/attempt evidence model
+is defined below; durable claims and transitions, relay I/O, retry scheduling,
+and lease recovery remain with Step 202. The exact machine contract is
[`publication_submission.v1.json`](contracts/services_hardening/publication_submission.v1.json).
+## Bounded publication attempt evidence
+
+The publication target state is one closed value: pending, submitted, accepted,
+rejected, rate-limited, auth-required, failed, or unknown. Pending is not an
+attempt outcome, Accepted is the sole terminal target state, submission alone
+does not prove relay delivery, and Unknown can be refined only by independent
+evidence. Attempt outcomes use the same closed vocabulary except Pending and
+expose no arbitrary result string or upstream diagnostic.
+
+`RhiPublicationAttemptEvidence` binds the sealed committed outbox identity and
+exact event digest to a zero-based target ordinal no greater than 31, a
+one-based attempt number no greater than 100, ordered injected integer UTC
+milliseconds, and one closed outcome under a domain-separated SHA-256 identity.
+It is pure bounded evidence rather than claim, transition, or relay authority.
+Step 202 must revalidate live target, lease, revision, attempt, and exact-byte
+bindings when it persists Submitted before I/O and later commits an observed
+outcome. The exact machine contract is
+[`publication_attempt_evidence.v1.json`](contracts/services_hardening/publication_attempt_evidence.v1.json).
+
## Existing-state runtime foundation
`open_rhi_runtime_foundation` opens only an already initialized database from
diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt
@@ -133,6 +133,23 @@ pub enum rhi::RhiPresenceCommandV1
pub rhi::RhiPresenceCommandV1::Desired
pub rhi::RhiPresenceCommandV1::Refresh
pub rhi::RhiPresenceCommandV1::Render
+pub enum rhi::RhiPublicationAttemptEvidenceErrorKind
+pub rhi::RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber
+pub rhi::RhiPublicationAttemptEvidenceErrorKind::InvalidTargetOrdinal
+pub rhi::RhiPublicationAttemptEvidenceErrorKind::InvalidTime
+impl rhi::RhiPublicationAttemptEvidenceErrorKind
+pub const fn rhi::RhiPublicationAttemptEvidenceErrorKind::code(self) -> &'static str
+pub enum rhi::RhiPublicationAttemptOutcome
+pub rhi::RhiPublicationAttemptOutcome::Accepted
+pub rhi::RhiPublicationAttemptOutcome::AuthRequired
+pub rhi::RhiPublicationAttemptOutcome::Failed
+pub rhi::RhiPublicationAttemptOutcome::RateLimited
+pub rhi::RhiPublicationAttemptOutcome::Rejected
+pub rhi::RhiPublicationAttemptOutcome::Submitted
+pub rhi::RhiPublicationAttemptOutcome::Unknown
+impl rhi::RhiPublicationAttemptOutcome
+pub const fn rhi::RhiPublicationAttemptOutcome::code(self) -> &'static str
+pub const fn rhi::RhiPublicationAttemptOutcome::target_state(self) -> rhi::RhiPublicationTargetState
pub enum rhi::RhiPublicationCommandV1
pub rhi::RhiPublicationCommandV1::Backlog
pub rhi::RhiPublicationCommandV1::Retry
@@ -153,6 +170,18 @@ pub rhi::RhiPublicationSubmissionErrorKind::NotFound
pub rhi::RhiPublicationSubmissionErrorKind::Storage
impl rhi::RhiPublicationSubmissionErrorKind
pub const fn rhi::RhiPublicationSubmissionErrorKind::code(self) -> &'static str
+pub enum rhi::RhiPublicationTargetState
+pub rhi::RhiPublicationTargetState::Accepted
+pub rhi::RhiPublicationTargetState::AuthRequired
+pub rhi::RhiPublicationTargetState::Failed
+pub rhi::RhiPublicationTargetState::Pending
+pub rhi::RhiPublicationTargetState::RateLimited
+pub rhi::RhiPublicationTargetState::Rejected
+pub rhi::RhiPublicationTargetState::Submitted
+pub rhi::RhiPublicationTargetState::Unknown
+impl rhi::RhiPublicationTargetState
+pub const fn rhi::RhiPublicationTargetState::code(self) -> &'static str
+pub const fn rhi::RhiPublicationTargetState::is_accepted_terminal(self) -> bool
pub enum rhi::RhiReconciliationAttemptErrorKind
pub rhi::RhiReconciliationAttemptErrorKind::InvalidConfiguration
pub rhi::RhiReconciliationAttemptErrorKind::InvalidInput
@@ -669,6 +698,34 @@ pub const fn rhi::RhiProvenanceRepository<'_>::descriptor(&self) -> rhi::RhiStat
pub const fn rhi::RhiProvenanceRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind
impl core::fmt::Debug for rhi::RhiProvenanceRepository<'_>
pub fn rhi::RhiProvenanceRepository<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPublicationAttemptEvidence
+impl rhi::RhiPublicationAttemptEvidence
+pub const fn rhi::RhiPublicationAttemptEvidence::attempt_number(&self) -> u16
+pub const fn rhi::RhiPublicationAttemptEvidence::event_sha256(&self) -> &[u8; 32]
+pub const fn rhi::RhiPublicationAttemptEvidence::finished_at(&self) -> rhi::RhiPublicationUnixMilliseconds
+pub const fn rhi::RhiPublicationAttemptEvidence::id(&self) -> rhi::RhiPublicationAttemptId
+pub fn rhi::RhiPublicationAttemptEvidence::new(&rhi::RhiCommittedPublication, u32, u16, rhi::RhiPublicationUnixMilliseconds, rhi::RhiPublicationUnixMilliseconds, rhi::RhiPublicationAttemptOutcome) -> core::result::Result<Self, rhi::RhiPublicationAttemptEvidenceError>
+pub const fn rhi::RhiPublicationAttemptEvidence::outbox_id(&self) -> rhi::RhiPublicationOutboxId
+pub const fn rhi::RhiPublicationAttemptEvidence::outcome(&self) -> rhi::RhiPublicationAttemptOutcome
+pub const fn rhi::RhiPublicationAttemptEvidence::result_code(&self) -> &'static str
+pub const fn rhi::RhiPublicationAttemptEvidence::started_at(&self) -> rhi::RhiPublicationUnixMilliseconds
+pub const fn rhi::RhiPublicationAttemptEvidence::target_ordinal(&self) -> u8
+impl core::fmt::Debug for rhi::RhiPublicationAttemptEvidence
+pub fn rhi::RhiPublicationAttemptEvidence::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPublicationAttemptEvidenceError
+impl rhi::RhiPublicationAttemptEvidenceError
+pub const fn rhi::RhiPublicationAttemptEvidenceError::code(self) -> &'static str
+pub const fn rhi::RhiPublicationAttemptEvidenceError::kind(self) -> rhi::RhiPublicationAttemptEvidenceErrorKind
+impl core::error::Error for rhi::RhiPublicationAttemptEvidenceError
+impl core::fmt::Debug for rhi::RhiPublicationAttemptEvidenceError
+pub fn rhi::RhiPublicationAttemptEvidenceError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for rhi::RhiPublicationAttemptEvidenceError
+pub fn rhi::RhiPublicationAttemptEvidenceError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPublicationAttemptId(_)
+impl rhi::RhiPublicationAttemptId
+pub const fn rhi::RhiPublicationAttemptId::as_bytes(&self) -> &[u8; 32]
+impl core::fmt::Debug for rhi::RhiPublicationAttemptId
+pub fn rhi::RhiPublicationAttemptId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiPublicationAttemptRepository<'host>
impl rhi::RhiPublicationAttemptRepository<'_>
pub const fn rhi::RhiPublicationAttemptRepository<'_>::descriptor(&self) -> rhi::RhiStateRepositoryDescriptor
@@ -739,6 +796,10 @@ pub const fn rhi::RhiPublicationTargetRepository<'_>::descriptor(&self) -> rhi::
pub const fn rhi::RhiPublicationTargetRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind
impl core::fmt::Debug for rhi::RhiPublicationTargetRepository<'_>
pub fn rhi::RhiPublicationTargetRepository<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiPublicationUnixMilliseconds(_)
+impl rhi::RhiPublicationUnixMilliseconds
+pub const fn rhi::RhiPublicationUnixMilliseconds::get(self) -> u64
+pub fn rhi::RhiPublicationUnixMilliseconds::new(u64) -> core::result::Result<Self, rhi::RhiPublicationAttemptEvidenceError>
pub struct rhi::RhiReconciliationAttemptError
impl rhi::RhiReconciliationAttemptError
pub const fn rhi::RhiReconciliationAttemptError::code(self) -> &'static str
@@ -1443,10 +1504,13 @@ pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_CONTRACT_VERSION: u32
pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES: usize
pub const rhi::RHI_MIGRATION_CATALOG_SHA256: [u8; 32]
pub const rhi::RHI_PROVIDER_CONTRACT_VERSION: u32
+pub const rhi::RHI_PUBLICATION_ATTEMPT_EVIDENCE_CONTRACT_VERSION: u32
+pub const rhi::RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM: u16
pub const rhi::RHI_PUBLICATION_CONTRACT_VERSION: u32
pub const rhi::RHI_PUBLICATION_MAX_ATTEMPTS: u16
pub const rhi::RHI_PUBLICATION_MAX_TARGETS: usize
pub const rhi::RHI_PUBLICATION_SUBMISSION_CONTRACT_VERSION: u32
+pub const rhi::RHI_PUBLICATION_TARGET_ORDINAL_MAXIMUM: u32
pub const rhi::RHI_RECONCILIATION_ATTEMPT_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_ATTEMPT_MAX_SOURCES: usize
pub const rhi::RHI_RECONCILIATION_ATTESTATION_CONTRACT_VERSION: u32
diff --git a/contracts/services_hardening/publication_attempt_evidence.v1.json b/contracts/services_hardening/publication_attempt_evidence.v1.json
@@ -0,0 +1,99 @@
+{
+ "schema": "radroots.rhi.publication-attempt-evidence",
+ "schema_version": 1,
+ "contract_version": 1,
+ "target_states": [
+ "pending",
+ "submitted",
+ "accepted",
+ "rejected",
+ "rate_limited",
+ "auth_required",
+ "failed",
+ "unknown"
+ ],
+ "attempt_outcomes": [
+ "submitted",
+ "accepted",
+ "rejected",
+ "rate_limited",
+ "auth_required",
+ "failed",
+ "unknown"
+ ],
+ "bounds": {
+ "target_ordinal": { "minimum": 0, "maximum": 31 },
+ "attempt_number": { "minimum": 1, "maximum": 100 },
+ "unix_milliseconds": { "minimum": 0, "maximum": 9223372036854775807 },
+ "finished_at": "greater_than_or_equal_to_started_at"
+ },
+ "attempt_identity": {
+ "algorithm": "sha256",
+ "domain": "radroots.rhi.publication_attempt.v1\u0000",
+ "framing": [
+ "outbox_id_32_bytes",
+ "exact_committed_event_sha256_32_bytes",
+ "target_ordinal_u32_be",
+ "attempt_number_u32_be"
+ ]
+ },
+ "evidence": {
+ "sealed_type": "RhiPublicationAttemptEvidence",
+ "forgeable": false,
+ "serializable": false,
+ "debug": "redacted",
+ "bound_fields": [
+ "attempt_id",
+ "outbox_id",
+ "exact_committed_event_sha256",
+ "target_ordinal",
+ "attempt_number",
+ "started_at_unix_ms",
+ "finished_at_unix_ms",
+ "outcome"
+ ],
+ "result_code": "exact_closed_outcome_code",
+ "raw_relay_result_or_error": false
+ },
+ "semantics": {
+ "pending_is_attempt_outcome": false,
+ "accepted_target_is_terminal": true,
+ "submission_alone_proves_delivery": false,
+ "unknown_may_be_refined_only_by_independent_evidence": true,
+ "constructor_is_claim_or_transition_authority": false
+ },
+ "effects": {
+ "sqlite_read_or_mutation": false,
+ "relay_or_network": false,
+ "event_parse_rebuild_serialize_or_sign": false,
+ "task_spawn": false,
+ "ambient_clock": false,
+ "ambient_entropy": false
+ },
+ "deferred_to_step_202": [
+ "publication_claim_and_lease",
+ "durable_submitted_before_io",
+ "relay_submission",
+ "compare_and_swap_outcome_commit",
+ "cancellation_and_lost_acknowledgement",
+ "backoff_and_retry_schedule",
+ "lease_and_restart_recovery",
+ "unknown_outcome_reconciliation"
+ ],
+ "forbidden": [
+ "arbitrary_target_state",
+ "arbitrary_attempt_outcome",
+ "arbitrary_result_code",
+ "raw_relay_error_text",
+ "unbounded_target_ordinal",
+ "unbounded_attempt_number",
+ "unbounded_or_reversed_time",
+ "caller_supplied_outbox_or_event_digest",
+ "sqlite_authority",
+ "relay_io",
+ "event_deserialization",
+ "event_rebuild",
+ "event_reserialization",
+ "event_resigning"
+ ]
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -9,6 +9,7 @@ mod features;
mod identity_credential;
mod identity_envelope;
mod publication;
+mod publication_attempt;
mod publication_submission;
mod reconciliation_attempt;
mod reconciliation_attestation;
@@ -72,6 +73,13 @@ pub use publication::{
RhiPublicationAuthority, RhiPublicationError, RhiPublicationErrorKind, RhiPublicationMode,
RhiPublicationRetryPolicy, RhiPublicationTarget,
};
+pub use publication_attempt::{
+ RHI_PUBLICATION_ATTEMPT_EVIDENCE_CONTRACT_VERSION, RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM,
+ RHI_PUBLICATION_TARGET_ORDINAL_MAXIMUM, RhiPublicationAttemptEvidence,
+ RhiPublicationAttemptEvidenceError, RhiPublicationAttemptEvidenceErrorKind,
+ RhiPublicationAttemptId, RhiPublicationAttemptOutcome, RhiPublicationTargetState,
+ RhiPublicationUnixMilliseconds,
+};
pub use publication_submission::{
RHI_PUBLICATION_SUBMISSION_CONTRACT_VERSION, RhiCommittedPublication, RhiPublicationOutboxId,
RhiPublicationSubmissionError, RhiPublicationSubmissionErrorKind,
diff --git a/src/publication_attempt.rs b/src/publication_attempt.rs
@@ -0,0 +1,540 @@
+//! Closed publication target states and bounded attempt evidence.
+
+use core::fmt;
+use std::error::Error;
+
+use sha2::{Digest as _, Sha256};
+
+use crate::RhiCommittedPublication;
+
+/// Exact version of the publication-attempt evidence contract.
+pub const RHI_PUBLICATION_ATTEMPT_EVIDENCE_CONTRACT_VERSION: u32 = 1;
+
+/// Absolute target ordinal ceiling inherited from the 32-target publication bound.
+pub const RHI_PUBLICATION_TARGET_ORDINAL_MAXIMUM: u32 = 31;
+
+/// Absolute durable attempt ceiling inherited from the publication contract.
+pub const RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM: u16 = 100;
+
+const ATTEMPT_ID_DOMAIN: &[u8] = b"radroots.rhi.publication_attempt.v1\0";
+const MAX_UNIX_MILLISECONDS: u64 = i64::MAX as u64;
+
+/// Stable closed target state retained by the publication workflow.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RhiPublicationTargetState {
+ Pending,
+ Submitted,
+ Accepted,
+ Rejected,
+ RateLimited,
+ AuthRequired,
+ Failed,
+ Unknown,
+}
+
+impl RhiPublicationTargetState {
+ /// Returns the exact machine-contract spelling.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::Pending => "pending",
+ Self::Submitted => "submitted",
+ Self::Accepted => "accepted",
+ Self::Rejected => "rejected",
+ Self::RateLimited => "rate_limited",
+ Self::AuthRequired => "auth_required",
+ Self::Failed => "failed",
+ Self::Unknown => "unknown",
+ }
+ }
+
+ /// Reports whether the state is the sole terminal target state.
+ #[must_use]
+ pub const fn is_accepted_terminal(self) -> bool {
+ matches!(self, Self::Accepted)
+ }
+}
+
+/// Stable closed outcome for one bounded publication attempt.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RhiPublicationAttemptOutcome {
+ Submitted,
+ Accepted,
+ Rejected,
+ RateLimited,
+ AuthRequired,
+ Failed,
+ Unknown,
+}
+
+impl RhiPublicationAttemptOutcome {
+ /// Returns the exact machine-contract spelling and safe persisted result code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::Submitted => "submitted",
+ Self::Accepted => "accepted",
+ Self::Rejected => "rejected",
+ Self::RateLimited => "rate_limited",
+ Self::AuthRequired => "auth_required",
+ Self::Failed => "failed",
+ Self::Unknown => "unknown",
+ }
+ }
+
+ /// Returns the target state represented by this exact observation.
+ #[must_use]
+ pub const fn target_state(self) -> RhiPublicationTargetState {
+ match self {
+ Self::Submitted => RhiPublicationTargetState::Submitted,
+ Self::Accepted => RhiPublicationTargetState::Accepted,
+ Self::Rejected => RhiPublicationTargetState::Rejected,
+ Self::RateLimited => RhiPublicationTargetState::RateLimited,
+ Self::AuthRequired => RhiPublicationTargetState::AuthRequired,
+ Self::Failed => RhiPublicationTargetState::Failed,
+ Self::Unknown => RhiPublicationTargetState::Unknown,
+ }
+ }
+}
+
+/// Bounded injected wall-clock value in integer UTC milliseconds.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RhiPublicationUnixMilliseconds(u64);
+
+impl RhiPublicationUnixMilliseconds {
+ /// Validates an injected timestamp against SQLite's signed representation.
+ pub fn new(value: u64) -> Result<Self, RhiPublicationAttemptEvidenceError> {
+ if value > MAX_UNIX_MILLISECONDS {
+ return Err(failure(RhiPublicationAttemptEvidenceErrorKind::InvalidTime));
+ }
+ Ok(Self(value))
+ }
+
+ /// Returns the exact integer UTC millisecond value.
+ #[must_use]
+ pub const fn get(self) -> u64 {
+ self.0
+ }
+}
+
+/// Domain-separated identity of one exact outbox-target attempt.
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+pub struct RhiPublicationAttemptId([u8; 32]);
+
+impl RhiPublicationAttemptId {
+ /// Returns the exact identity bytes.
+ #[must_use]
+ pub const fn as_bytes(&self) -> &[u8; 32] {
+ &self.0
+ }
+}
+
+impl fmt::Debug for RhiPublicationAttemptId {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("RhiPublicationAttemptId([redacted])")
+ }
+}
+
+/// Sealed bounded evidence for one exact publication attempt.
+///
+/// Construction binds the immutable committed outbox and event digest to a
+/// bounded target ordinal and attempt number. It does not claim the target,
+/// persist a Submitted transition, perform relay I/O, or authorize a later
+/// durable outcome transition; Step 202 must revalidate those live facts.
+///
+/// ```compile_fail
+/// use rhi::RhiPublicationAttemptEvidence;
+///
+/// let _forged = RhiPublicationAttemptEvidence { attempt_number: 1 };
+/// ```
+#[must_use = "publication attempt evidence must be durably reconciled or deliberately discarded"]
+pub struct RhiPublicationAttemptEvidence {
+ id: RhiPublicationAttemptId,
+ outbox_id: crate::RhiPublicationOutboxId,
+ event_sha256: [u8; 32],
+ target_ordinal: u8,
+ attempt_number: u16,
+ started_at: RhiPublicationUnixMilliseconds,
+ finished_at: RhiPublicationUnixMilliseconds,
+ outcome: RhiPublicationAttemptOutcome,
+}
+
+impl RhiPublicationAttemptEvidence {
+ /// Constructs one bounded observation from the exact committed publication.
+ pub fn new(
+ publication: &RhiCommittedPublication,
+ target_ordinal: u32,
+ attempt_number: u16,
+ started_at: RhiPublicationUnixMilliseconds,
+ finished_at: RhiPublicationUnixMilliseconds,
+ outcome: RhiPublicationAttemptOutcome,
+ ) -> Result<Self, RhiPublicationAttemptEvidenceError> {
+ let target_ordinal = u8::try_from(target_ordinal)
+ .ok()
+ .filter(|ordinal| u32::from(*ordinal) <= RHI_PUBLICATION_TARGET_ORDINAL_MAXIMUM)
+ .ok_or_else(|| failure(RhiPublicationAttemptEvidenceErrorKind::InvalidTargetOrdinal))?;
+ if !(1..=RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM).contains(&attempt_number) {
+ return Err(failure(
+ RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber,
+ ));
+ }
+ if finished_at < started_at {
+ return Err(failure(RhiPublicationAttemptEvidenceErrorKind::InvalidTime));
+ }
+ let outbox_id = publication.outbox_id();
+ let event_sha256 = *publication.event_sha256();
+ let mut digest = Sha256::new();
+ digest.update(ATTEMPT_ID_DOMAIN);
+ digest.update(outbox_id.as_bytes());
+ digest.update(event_sha256);
+ digest.update(u32::from(target_ordinal).to_be_bytes());
+ digest.update(u32::from(attempt_number).to_be_bytes());
+ Ok(Self {
+ id: RhiPublicationAttemptId(digest.finalize().into()),
+ outbox_id,
+ event_sha256,
+ target_ordinal,
+ attempt_number,
+ started_at,
+ finished_at,
+ outcome,
+ })
+ }
+
+ /// Returns the exact domain-separated attempt identity.
+ #[must_use]
+ pub const fn id(&self) -> RhiPublicationAttemptId {
+ self.id
+ }
+
+ /// Returns the immutable committed outbox identity.
+ #[must_use]
+ pub const fn outbox_id(&self) -> crate::RhiPublicationOutboxId {
+ self.outbox_id
+ }
+
+ /// Returns the digest of the exact committed signed-event bytes.
+ #[must_use]
+ pub const fn event_sha256(&self) -> &[u8; 32] {
+ &self.event_sha256
+ }
+
+ /// Returns the bounded zero-based target ordinal.
+ #[must_use]
+ pub const fn target_ordinal(&self) -> u8 {
+ self.target_ordinal
+ }
+
+ /// Returns the bounded one-based attempt number.
+ #[must_use]
+ pub const fn attempt_number(&self) -> u16 {
+ self.attempt_number
+ }
+
+ /// Returns the injected attempt start time.
+ #[must_use]
+ pub const fn started_at(&self) -> RhiPublicationUnixMilliseconds {
+ self.started_at
+ }
+
+ /// Returns the injected attempt finish time.
+ #[must_use]
+ pub const fn finished_at(&self) -> RhiPublicationUnixMilliseconds {
+ self.finished_at
+ }
+
+ /// Returns the closed observed outcome.
+ #[must_use]
+ pub const fn outcome(&self) -> RhiPublicationAttemptOutcome {
+ self.outcome
+ }
+
+ /// Returns the sole safe persisted result code for this outcome.
+ #[must_use]
+ pub const fn result_code(&self) -> &'static str {
+ self.outcome.code()
+ }
+}
+
+impl fmt::Debug for RhiPublicationAttemptEvidence {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPublicationAttemptEvidence")
+ .field("identity", &"[redacted]")
+ .field("target_ordinal", &self.target_ordinal)
+ .field("attempt_number", &self.attempt_number)
+ .field("started_at", &self.started_at)
+ .field("finished_at", &self.finished_at)
+ .field("outcome", &self.outcome)
+ .finish()
+ }
+}
+
+/// Stable source-free attempt-evidence failure class.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiPublicationAttemptEvidenceErrorKind {
+ InvalidTargetOrdinal,
+ InvalidAttemptNumber,
+ InvalidTime,
+}
+
+impl RhiPublicationAttemptEvidenceErrorKind {
+ /// Returns the stable machine-readable failure code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidTargetOrdinal => "publication_attempt_target_ordinal_invalid",
+ Self::InvalidAttemptNumber => "publication_attempt_number_invalid",
+ Self::InvalidTime => "publication_attempt_time_invalid",
+ }
+ }
+}
+
+/// Redacted source-free attempt-evidence failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiPublicationAttemptEvidenceError {
+ kind: RhiPublicationAttemptEvidenceErrorKind,
+}
+
+impl RhiPublicationAttemptEvidenceError {
+ /// Returns the stable failure class.
+ #[must_use]
+ pub const fn kind(self) -> RhiPublicationAttemptEvidenceErrorKind {
+ self.kind
+ }
+
+ /// Returns the stable machine-readable failure code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ self.kind.code()
+ }
+}
+
+impl fmt::Display for RhiPublicationAttemptEvidenceError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ RhiPublicationAttemptEvidenceErrorKind::InvalidTargetOrdinal => {
+ "RHI publication attempt target ordinal is invalid"
+ }
+ RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber => {
+ "RHI publication attempt number is invalid"
+ }
+ RhiPublicationAttemptEvidenceErrorKind::InvalidTime => {
+ "RHI publication attempt time is invalid"
+ }
+ })
+ }
+}
+
+impl fmt::Debug for RhiPublicationAttemptEvidenceError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiPublicationAttemptEvidenceError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl Error for RhiPublicationAttemptEvidenceError {}
+
+const fn failure(
+ kind: RhiPublicationAttemptEvidenceErrorKind,
+) -> RhiPublicationAttemptEvidenceError {
+ RhiPublicationAttemptEvidenceError { kind }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn publication() -> RhiCommittedPublication {
+ RhiCommittedPublication::test_fixture(
+ crate::RhiPublicationOutboxId::from_committed_bytes([0x11; 32]),
+ [0x44; 32],
+ [0x22; 32],
+ vec![0x33].into_boxed_slice(),
+ )
+ }
+
+ fn lower_hex(bytes: &[u8]) -> String {
+ const DIGITS: &[u8; 16] = b"0123456789abcdef";
+ let mut output = String::with_capacity(bytes.len() * 2);
+ for byte in bytes {
+ output.push(char::from(DIGITS[usize::from(byte >> 4)]));
+ output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
+ }
+ output
+ }
+
+ #[test]
+ fn states_and_outcomes_are_exact() {
+ let states = [
+ RhiPublicationTargetState::Pending,
+ RhiPublicationTargetState::Submitted,
+ RhiPublicationTargetState::Accepted,
+ RhiPublicationTargetState::Rejected,
+ RhiPublicationTargetState::RateLimited,
+ RhiPublicationTargetState::AuthRequired,
+ RhiPublicationTargetState::Failed,
+ RhiPublicationTargetState::Unknown,
+ ];
+ assert_eq!(
+ states.map(RhiPublicationTargetState::code),
+ [
+ "pending",
+ "submitted",
+ "accepted",
+ "rejected",
+ "rate_limited",
+ "auth_required",
+ "failed",
+ "unknown",
+ ]
+ );
+ assert!(RhiPublicationTargetState::Accepted.is_accepted_terminal());
+ for state in states {
+ assert_eq!(state.is_accepted_terminal(), state.code() == "accepted");
+ }
+ let outcomes = [
+ RhiPublicationAttemptOutcome::Submitted,
+ RhiPublicationAttemptOutcome::Accepted,
+ RhiPublicationAttemptOutcome::Rejected,
+ RhiPublicationAttemptOutcome::RateLimited,
+ RhiPublicationAttemptOutcome::AuthRequired,
+ RhiPublicationAttemptOutcome::Failed,
+ RhiPublicationAttemptOutcome::Unknown,
+ ];
+ assert_eq!(
+ outcomes.map(RhiPublicationAttemptOutcome::code),
+ [
+ "submitted",
+ "accepted",
+ "rejected",
+ "rate_limited",
+ "auth_required",
+ "failed",
+ "unknown",
+ ]
+ );
+ for outcome in outcomes {
+ assert_eq!(outcome.code(), outcome.target_state().code());
+ }
+ }
+
+ #[test]
+ fn time_and_error_boundaries_are_safe() {
+ assert_eq!(RhiPublicationUnixMilliseconds::new(0).unwrap().get(), 0);
+ assert_eq!(
+ RhiPublicationUnixMilliseconds::new(MAX_UNIX_MILLISECONDS)
+ .unwrap()
+ .get(),
+ MAX_UNIX_MILLISECONDS
+ );
+ let error = RhiPublicationUnixMilliseconds::new(MAX_UNIX_MILLISECONDS + 1)
+ .expect_err("oversized time");
+ assert_eq!(
+ error.kind(),
+ RhiPublicationAttemptEvidenceErrorKind::InvalidTime
+ );
+ for kind in [
+ RhiPublicationAttemptEvidenceErrorKind::InvalidTargetOrdinal,
+ RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber,
+ RhiPublicationAttemptEvidenceErrorKind::InvalidTime,
+ ] {
+ let error = failure(kind);
+ assert!(error.code().starts_with("publication_attempt_"));
+ assert!(Error::source(&error).is_none());
+ let rendered = format!("{error} {error:?}");
+ assert!(!rendered.contains("relay-primary"));
+ assert!(!rendered.contains("secret"));
+ }
+ }
+
+ #[test]
+ fn attempt_evidence_binds_exact_maximum_fields_and_closed_result() {
+ let started_at = RhiPublicationUnixMilliseconds::new(42).unwrap();
+ let finished_at = RhiPublicationUnixMilliseconds::new(43).unwrap();
+ let evidence = RhiPublicationAttemptEvidence::new(
+ &publication(),
+ RHI_PUBLICATION_TARGET_ORDINAL_MAXIMUM,
+ RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM,
+ started_at,
+ finished_at,
+ RhiPublicationAttemptOutcome::RateLimited,
+ )
+ .expect("maximum evidence");
+ assert_eq!(
+ lower_hex(evidence.id().as_bytes()),
+ "e1acaadff3f4d52ca7e9a8d14026039827bf8386ebb66551402dfd1d7309899c"
+ );
+ assert_eq!(
+ evidence.outbox_id().as_bytes(),
+ crate::RhiPublicationOutboxId::from_committed_bytes([0x11; 32]).as_bytes()
+ );
+ assert_eq!(evidence.event_sha256(), &[0x22; 32]);
+ assert_eq!(evidence.target_ordinal(), 31);
+ assert_eq!(evidence.attempt_number(), 100);
+ assert_eq!(evidence.started_at(), started_at);
+ assert_eq!(evidence.finished_at(), finished_at);
+ assert_eq!(
+ evidence.outcome(),
+ RhiPublicationAttemptOutcome::RateLimited
+ );
+ assert_eq!(evidence.result_code(), "rate_limited");
+ let rendered = format!("{evidence:?} {:?}", evidence.id());
+ assert!(!rendered.contains(&lower_hex(evidence.id().as_bytes())));
+ assert!(!rendered.contains("relay-primary"));
+ assert!(!rendered.contains("3333"));
+ }
+
+ #[test]
+ fn attempt_evidence_rejects_each_invalid_boundary() {
+ let started_at = RhiPublicationUnixMilliseconds::new(42).unwrap();
+ let finished_at = RhiPublicationUnixMilliseconds::new(43).unwrap();
+ let cases = [
+ (
+ 32,
+ 1,
+ started_at,
+ finished_at,
+ RhiPublicationAttemptEvidenceErrorKind::InvalidTargetOrdinal,
+ ),
+ (
+ 0,
+ 0,
+ started_at,
+ finished_at,
+ RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber,
+ ),
+ (
+ 0,
+ 101,
+ started_at,
+ finished_at,
+ RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber,
+ ),
+ (
+ 0,
+ 1,
+ finished_at,
+ started_at,
+ RhiPublicationAttemptEvidenceErrorKind::InvalidTime,
+ ),
+ ];
+ for (ordinal, attempt, start, finish, expected) in cases {
+ assert_eq!(
+ RhiPublicationAttemptEvidence::new(
+ &publication(),
+ ordinal,
+ attempt,
+ start,
+ finish,
+ RhiPublicationAttemptOutcome::Unknown,
+ )
+ .expect_err("invalid evidence")
+ .kind(),
+ expected
+ );
+ }
+ }
+}
diff --git a/src/publication_submission.rs b/src/publication_submission.rs
@@ -150,6 +150,21 @@ pub struct RhiCommittedPublication {
}
impl RhiCommittedPublication {
+ #[cfg(test)]
+ pub(crate) fn test_fixture(
+ outbox_id: RhiPublicationOutboxId,
+ event_id: [u8; 32],
+ event_sha256: [u8; 32],
+ exact_signed_event_bytes: Box<[u8]>,
+ ) -> Self {
+ Self {
+ outbox_id,
+ event_id,
+ event_sha256,
+ exact_signed_event_bytes,
+ }
+ }
+
/// Returns the immutable outbox identity.
#[must_use]
pub const fn outbox_id(&self) -> RhiPublicationOutboxId {
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -12,6 +12,9 @@ const RUNTIME_ADAPTER_CONTRACT: &str =
include_str!("../contracts/services_hardening/runtime_adapters.v1.json");
const RUNTIME_FOUNDATION: &str = include_str!("../src/runtime_foundation.rs");
const PUBLICATION: &str = include_str!("../src/publication.rs");
+const PUBLICATION_ATTEMPT: &str = include_str!("../src/publication_attempt.rs");
+const PUBLICATION_ATTEMPT_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/publication_attempt_evidence.v1.json");
const PUBLICATION_CONTRACT: &str =
include_str!("../contracts/services_hardening/publication_outbox.v1.json");
const PUBLICATION_SUBMISSION: &str = include_str!("../src/publication_submission.rs");
@@ -62,6 +65,7 @@ const SOURCES: &[&str] = &[
include_str!("../src/identity_credential.rs"),
include_str!("../src/identity_envelope.rs"),
include_str!("../src/publication.rs"),
+ include_str!("../src/publication_attempt.rs"),
include_str!("../src/publication_submission.rs"),
include_str!("../src/reconciliation_attempt.rs"),
include_str!("../src/reconciliation_attestation.rs"),
@@ -139,6 +143,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"identity_credential",
"identity_envelope",
"publication",
+ "publication_attempt",
"publication_submission",
"reconciliation_attempt",
"reconciliation_attestation",
@@ -190,6 +195,15 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"RhiPublicationRetryPolicy",
"RhiPublicationTarget",
"RHI_PUBLICATION_CONTRACT_VERSION",
+ "RhiPublicationAttemptEvidence",
+ "RhiPublicationAttemptEvidenceErrorKind",
+ "RhiPublicationAttemptId",
+ "RhiPublicationAttemptOutcome",
+ "RhiPublicationTargetState",
+ "RhiPublicationUnixMilliseconds",
+ "RHI_PUBLICATION_ATTEMPT_EVIDENCE_CONTRACT_VERSION",
+ "RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM",
+ "RHI_PUBLICATION_TARGET_ORDINAL_MAXIMUM",
"RhiCommittedPublication",
"RhiPublicationOutboxId",
"RhiPublicationSubmissionErrorKind",
@@ -280,6 +294,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
assert!(!PUBLIC_API.contains("rhi::features::"));
assert!(!PUBLIC_API.contains("rhi::runtime_adapters::"));
assert!(!PUBLIC_API.contains("rhi::publication::"));
+ assert!(!PUBLIC_API.contains("rhi::publication_attempt::"));
assert!(!PUBLIC_API.contains("rhi::publication_submission::"));
}
@@ -351,6 +366,7 @@ fn committed_publication_is_bounded_exact_and_never_reconstructed() {
"std::net",
"tokio::spawn",
"SystemTime",
+ "pub(crate) fn from_committed_parts",
] {
assert!(
!PUBLICATION_SUBMISSION.contains(forbidden),
@@ -362,6 +378,55 @@ fn committed_publication_is_bounded_exact_and_never_reconstructed() {
}
#[test]
+fn publication_attempt_evidence_is_closed_bounded_and_effect_free() {
+ let contract: serde_json::Value =
+ serde_json::from_str(PUBLICATION_ATTEMPT_CONTRACT).expect("publication-attempt contract");
+ assert_eq!(
+ contract["schema"],
+ "radroots.rhi.publication-attempt-evidence"
+ );
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(contract["bounds"]["target_ordinal"]["maximum"], 31);
+ assert_eq!(contract["bounds"]["attempt_number"]["maximum"], 100);
+ assert_eq!(
+ contract["evidence"]["result_code"],
+ "exact_closed_outcome_code"
+ );
+ assert_eq!(contract["effects"]["sqlite_read_or_mutation"], false);
+ assert_eq!(contract["effects"]["relay_or_network"], false);
+ for required in [
+ "pub enum RhiPublicationTargetState",
+ "pub enum RhiPublicationAttemptOutcome",
+ "pub struct RhiPublicationAttemptEvidence",
+ "publication.outbox_id()",
+ "publication.event_sha256()",
+ "RHI_PUBLICATION_TARGET_ORDINAL_MAXIMUM",
+ "RHI_PUBLICATION_ATTEMPT_NUMBER_MAXIMUM",
+ ] {
+ assert!(
+ PUBLICATION_ATTEMPT.contains(required),
+ "publication-attempt evidence is missing {required}"
+ );
+ }
+ for forbidden in [
+ "sqlx::",
+ "serde_json",
+ "EventSink",
+ "SystemTime",
+ "std::fs",
+ "std::net",
+ "tokio::spawn",
+ ] {
+ assert!(
+ !PUBLICATION_ATTEMPT.contains(forbidden),
+ "publication-attempt evidence gained forbidden authority {forbidden}"
+ );
+ }
+ assert!(!ROOT.contains("pub mod publication_attempt"));
+ assert!(!PUBLIC_API.contains("rhi::publication_attempt::"));
+}
+
+#[test]
fn reconciliation_attestation_is_typed_signed_verified_and_effect_free() {
let contract: serde_json::Value = serde_json::from_str(RECONCILIATION_ATTESTATION_CONTRACT)
.expect("reconciliation-attestation contract");
@@ -648,7 +713,7 @@ fn public_errors_are_crate_owned_redacted_and_source_free() {
.lines()
.filter(|line| line.starts_with("pub struct rhi::") && line.ends_with("Error"))
.count();
- assert_eq!(public_error_count, 27);
+ assert_eq!(public_error_count, 28);
}
#[test]
@@ -1083,6 +1148,8 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"[`reconciliation_attestation.v1.json`](contracts/services_hardening/reconciliation_attestation.v1.json)",
"## Explicit publication authority and durable schema",
"[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json)",
+ "## Bounded publication attempt evidence",
+ "[`publication_attempt_evidence.v1.json`](contracts/services_hardening/publication_attempt_evidence.v1.json)",
"The event body and exact kind-3441 structural tags",
"without rebuilding, reserializing, or",
"Coverage is exactly `Missing`, `Partial`, `ScopeSatisfied`, or `Unsupported`",
diff --git a/tests/services_hardening_publication_attempt_evidence_contract.rs b/tests/services_hardening_publication_attempt_evidence_contract.rs
@@ -0,0 +1,99 @@
+#![forbid(unsafe_code)]
+
+use serde_json::json;
+
+const CONTRACT: &str =
+ include_str!("../contracts/services_hardening/publication_attempt_evidence.v1.json");
+const SOURCE: &str = include_str!("../src/publication_attempt.rs");
+const ROOT: &str = include_str!("../src/lib.rs");
+
+#[test]
+fn exact_target_and_attempt_vocabularies_are_closed() {
+ let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract JSON");
+ assert_eq!(
+ contract["schema"],
+ "radroots.rhi.publication-attempt-evidence"
+ );
+ assert_eq!(contract["schema_version"], 1);
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(
+ contract["target_states"],
+ json!([
+ "pending",
+ "submitted",
+ "accepted",
+ "rejected",
+ "rate_limited",
+ "auth_required",
+ "failed",
+ "unknown"
+ ])
+ );
+ assert_eq!(
+ contract["attempt_outcomes"],
+ json!([
+ "submitted",
+ "accepted",
+ "rejected",
+ "rate_limited",
+ "auth_required",
+ "failed",
+ "unknown"
+ ])
+ );
+ assert_eq!(contract["bounds"]["target_ordinal"]["maximum"], 31);
+ assert_eq!(contract["bounds"]["attempt_number"]["maximum"], 100);
+ assert_eq!(
+ contract["bounds"]["unix_milliseconds"]["maximum"],
+ 9_223_372_036_854_775_807_u64
+ );
+ assert_eq!(contract["evidence"]["raw_relay_result_or_error"], false);
+ assert_eq!(
+ contract["semantics"]["submission_alone_proves_delivery"],
+ false
+ );
+ assert_eq!(contract["effects"]["sqlite_read_or_mutation"], false);
+ assert_eq!(contract["effects"]["relay_or_network"], false);
+}
+
+#[test]
+fn pure_model_has_no_premature_workflow_or_effect_authority() {
+ for required in [
+ "pub enum RhiPublicationTargetState",
+ "pub enum RhiPublicationAttemptOutcome",
+ "pub struct RhiPublicationAttemptEvidence",
+ "pub struct RhiPublicationAttemptId",
+ "pub struct RhiPublicationUnixMilliseconds",
+ "radroots.rhi.publication_attempt.v1\\0",
+ "publication.outbox_id()",
+ "publication.event_sha256()",
+ "digest.update(u32::from(target_ordinal).to_be_bytes())",
+ "digest.update(u32::from(attempt_number).to_be_bytes())",
+ ] {
+ assert!(
+ SOURCE.contains(required),
+ "missing evidence guard {required}"
+ );
+ }
+ for forbidden in [
+ "sqlx::",
+ "radroots_transport",
+ "EventSink",
+ "SystemTime",
+ "std::fs",
+ "std::net",
+ "tokio::spawn",
+ "spawn_blocking",
+ "serde_json",
+ "publish(",
+ "send(",
+ "from_committed_parts",
+ ] {
+ assert!(
+ !SOURCE.contains(forbidden),
+ "attempt evidence gained forbidden authority {forbidden}"
+ );
+ }
+ assert!(ROOT.contains("mod publication_attempt;"));
+ assert!(!ROOT.contains("pub mod publication_attempt;"));
+}