commit c4bc8f4ab35ae2c066c6e439f68ee42ad1493193
parent bc1a4ac7a8c4e5e4d368ef0e8111c9f5039cd5fb
Author: triesap <tyson@radroots.org>
Date: Mon, 24 Aug 2026 08:01:42 +0000
reconciliation: freeze immutable evidence manifest
- Consume confirmed reconciliation commits into sealed canonical manifests.
- Bind policy, selectors, source results, signed events, and provenance.
- Freeze machine, package, documentation, and public API contracts.
- Verify all-target tests, Clippy, Rustdoc, and API freshness.
Diffstat:
10 files changed, 840 insertions(+), 7 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -135,6 +135,14 @@
canonically ordered immutable set with explicit policy digest, reducer
version, coverage, and observed-time input; they have no database insertion,
relay, scheduling, wall-clock, entropy, or network dependency.
+- Mint the immutable reconciliation manifest only by consuming a sealed,
+ durably confirmed source-commit outcome. Bind each source result to its exact
+ selector, completion, cursor, timing, and persisted-inventory digest, and
+ bind each observation to the exact canonical signed-event and first-source
+ provenance. Do not expose a raw manifest constructor or parser from RHI.
+- Keep Step 191 manifest materialization pure and in memory. Step 199 alone
+ owns durable manifest persistence; reducers, final coverage/outcome,
+ attestation, publication, and job finalization retain their ordered owners.
- Coverage is exactly `Missing`, `Partial`, `ScopeSatisfied`, or `Unsupported`.
ScopeSatisfied means only that the configured policy was satisfied; optional
evidence never substitutes for required-source completion.
diff --git a/README b/README
@@ -213,6 +213,32 @@ selector cursor evidence is minted only after durable commit confirmation. The
exact machine contract is
[`reconciliation_commit.v1.json`](contracts/services_hardening/reconciliation_commit.v1.json).
+## Immutable reconciliation manifest
+
+`RhiReconciliationSourceCommitOutcome::into_evidence_manifest` consumes one
+sealed, durably confirmed Step 190 outcome and freezes its exact canonical
+mutation, signed-event, first-provenance, and per-source completion inventory
+into the shared `radroots.trade.evidence-manifest.v1` encoding. The manifest
+binds the claimed trade generation and evidence-policy digest. Each source
+result additionally binds its exact selector digest, detailed completion,
+timing, cursor inputs, checkpoint eligibility, and Step 190 persisted-inventory
+digest. Each observation binds the verified mutation and event identities, the
+SHA-256 of the exact canonical signed-event JSON, and a source/selector/policy/
+signature/first-observation provenance digest.
+
+Sources and observations use the shared canonical ordering, so arrival and
+insertion order cannot select truth. The explicit whole-second observation
+time cannot precede any committed source finish, and the explicit closed scope-
+prerequisite input is included in the immutable bytes. Callers can inspect only
+the redacted sealed result's identity, counts, canonical bytes, and digest; they
+cannot construct a manifest from uncommitted replay material or supply its
+result/provenance digests. Materialization is pure and performs no SQLite,
+filesystem, source, relay, network, task, clock, or entropy operation. Durable
+manifest persistence remains deferred to Step 199, and reduction, final
+coverage/outcome, attestation, and publication retain their later checkpoint
+owners. The exact machine contract is
+[`reconciliation_manifest.v1.json`](contracts/services_hardening/reconciliation_manifest.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
@@ -178,6 +178,11 @@ pub rhi::RhiReconciliationJobState::Ready
pub rhi::RhiReconciliationJobState::Superseded
impl rhi::RhiReconciliationJobState
pub const fn rhi::RhiReconciliationJobState::code(self) -> &'static str
+pub enum rhi::RhiReconciliationManifestErrorKind
+pub rhi::RhiReconciliationManifestErrorKind::InvalidCommittedInventory
+pub rhi::RhiReconciliationManifestErrorKind::InvalidObservationTime
+impl rhi::RhiReconciliationManifestErrorKind
+pub const fn rhi::RhiReconciliationManifestErrorKind::code(self) -> &'static str
pub enum rhi::RhiReconciliationReplayErrorKind
pub rhi::RhiReconciliationReplayErrorKind::InvalidConfiguration
pub rhi::RhiReconciliationReplayErrorKind::InvalidInput
@@ -187,6 +192,9 @@ pub rhi::RhiReconciliationReplayErrorKind::ResourceLimit
pub rhi::RhiReconciliationReplayErrorKind::SignedEventConflict
impl rhi::RhiReconciliationReplayErrorKind
pub const fn rhi::RhiReconciliationReplayErrorKind::code(self) -> &'static str
+pub enum rhi::RhiReconciliationScopePrerequisites
+pub rhi::RhiReconciliationScopePrerequisites::Satisfied
+pub rhi::RhiReconciliationScopePrerequisites::Unsatisfied
pub enum rhi::RhiRuntimeAdapterErrorKind
pub rhi::RhiRuntimeAdapterErrorKind::CredentialAccess
pub rhi::RhiRuntimeAdapterErrorKind::EntropyUnavailable
@@ -711,6 +719,29 @@ impl rhi::RhiReconciliationLeaseOwner
pub fn rhi::RhiReconciliationLeaseOwner::from_bytes([u8; 16]) -> core::result::Result<Self, rhi::RhiReconciliationJobError>
impl core::fmt::Debug for rhi::RhiReconciliationLeaseOwner
pub fn rhi::RhiReconciliationLeaseOwner::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiReconciliationManifest
+impl rhi::RhiReconciliationManifest
+pub fn rhi::RhiReconciliationManifest::canonical_bytes(&self) -> &[u8]
+pub const fn rhi::RhiReconciliationManifest::contract_version(&self) -> u32
+pub fn rhi::RhiReconciliationManifest::digest(&self) -> [u8; 32]
+pub fn rhi::RhiReconciliationManifest::observation_count(&self) -> usize
+pub const fn rhi::RhiReconciliationManifest::observed_at_unix_seconds(&self) -> u64
+pub const fn rhi::RhiReconciliationManifest::shared_manifest_contract_id(&self) -> &'static str
+pub const fn rhi::RhiReconciliationManifest::shared_manifest_contract_version(&self) -> u16
+pub fn rhi::RhiReconciliationManifest::source_count(&self) -> usize
+pub const fn rhi::RhiReconciliationManifest::trade_generation(&self) -> u64
+pub const fn rhi::RhiReconciliationManifest::trade_id(&self) -> &radroots_event::id::TradeId
+impl core::fmt::Debug for rhi::RhiReconciliationManifest
+pub fn rhi::RhiReconciliationManifest::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiReconciliationManifestError
+impl rhi::RhiReconciliationManifestError
+pub const fn rhi::RhiReconciliationManifestError::code(self) -> &'static str
+pub const fn rhi::RhiReconciliationManifestError::kind(self) -> rhi::RhiReconciliationManifestErrorKind
+impl core::error::Error for rhi::RhiReconciliationManifestError
+impl core::fmt::Debug for rhi::RhiReconciliationManifestError
+pub fn rhi::RhiReconciliationManifestError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for rhi::RhiReconciliationManifestError
+pub fn rhi::RhiReconciliationManifestError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiReconciliationReplayError
impl rhi::RhiReconciliationReplayError
pub const fn rhi::RhiReconciliationReplayError::code(self) -> &'static str
@@ -735,6 +766,8 @@ pub fn rhi::RhiReconciliationSourceCommitOutcome::committed_cursors(&self) -> &[
pub const fn rhi::RhiReconciliationSourceCommitOutcome::created(&self) -> bool
pub const fn rhi::RhiReconciliationSourceCommitOutcome::dirty_generation_advanced(&self) -> bool
pub const fn rhi::RhiReconciliationSourceCommitOutcome::source_result_count(&self) -> u32
+impl rhi::RhiReconciliationSourceCommitOutcome
+pub fn rhi::RhiReconciliationSourceCommitOutcome::into_evidence_manifest(self, radroots_service_host::time::UnixTimeSeconds, rhi::RhiReconciliationScopePrerequisites) -> core::result::Result<rhi::RhiReconciliationManifest, rhi::RhiReconciliationManifestError>
impl core::fmt::Debug for rhi::RhiReconciliationSourceCommitOutcome
pub fn rhi::RhiReconciliationSourceCommitOutcome::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiReconciliationSourceCursorEvidence
@@ -1191,6 +1224,7 @@ pub const rhi::RHI_RECONCILIATION_ATTEMPT_MAX_SOURCES: usize
pub const rhi::RHI_RECONCILIATION_COMMIT_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_JOB_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_JOB_MAX_ACTIVE: u32
+pub const rhi::RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION: u32
pub const rhi::RHI_RECONCILIATION_REPLAY_CONTRACT_VERSION: u32
pub const rhi::RHI_RUNTIME_ADAPTER_CONTRACT_VERSION: u32
pub const rhi::RHI_RUNTIME_FOUNDATION_CONTRACT_VERSION: u32
diff --git a/contracts/services_hardening/reconciliation_manifest.v1.json b/contracts/services_hardening/reconciliation_manifest.v1.json
@@ -0,0 +1,104 @@
+{
+ "schema": "radroots.rhi.reconciliation-manifest",
+ "schema_version": 1,
+ "contract_version": 1,
+ "construction_authority": "confirmed_step_190_commit_outcome_only",
+ "shared_manifest": {
+ "contract_id": "radroots.trade.evidence-manifest.v1",
+ "contract_version": 1,
+ "canonical_ordering": "shared_manifest_source_and_observation_order",
+ "manifest_digest": "shared_domain_separated_sha256_over_exact_canonical_bytes",
+ "source_count_maximum": 16,
+ "observation_count_maximum": 65536,
+ "canonical_bytes_maximum": 16777216
+ },
+ "identity": {
+ "trade": "exact_claimed_job_trade",
+ "generation": "exact_claimed_job_input_generation_nonzero",
+ "evidence_policy_digest": "exact_claimed_job_policy_digest",
+ "source_selector_digest": "bound_inside_each_source_result_digest",
+ "scope_prerequisites": "explicit_closed_satisfied_or_unsatisfied_input",
+ "observation_time": "explicit_integer_utc_seconds_not_before_any_source_finish"
+ },
+ "source_result_digest": {
+ "algorithm": "sha256",
+ "domain_utf8_then_nul": "radroots.rhi.reconciliation_manifest_source_result.v1",
+ "ordered_fields": [
+ "attempt_id",
+ "canonical_source_ordinal_u32_be",
+ "request_id",
+ "framed_source_id_utf8",
+ "trade_id",
+ "required_flag",
+ "evidence_policy_digest",
+ "source_selector_digest",
+ "replay_id",
+ "framed_detailed_completion_code_utf8",
+ "started_at_unix_ms_u64_be",
+ "finished_at_unix_ms_u64_be",
+ "accepted_event_count_u32_be",
+ "accepted_event_bytes_u64_be",
+ "step_190_accepted_inventory_digest",
+ "duplicate_observation_count_u32_be",
+ "optional_first_observed_at_unix_s",
+ "optional_prior_cursor",
+ "overlap_seconds_u64_be",
+ "inclusive_since_unix_s_u64_be",
+ "optional_cursor_candidate",
+ "checkpoint_eligible_flag"
+ ],
+ "step_190_inventory_digest_domain_utf8_then_nul": "radroots.rhi.reconciliation_source_inventory.v1"
+ },
+ "observation": {
+ "mutation_id": "signature_verified_canonical_mutation_id",
+ "event_id": "signature_verified_signed_event_id",
+ "signed_event_digest": "sha256_over_exact_canonical_signed_event_json",
+ "provenance_digest": {
+ "algorithm": "sha256",
+ "domain_utf8_then_nul": "radroots.rhi.evidence_provenance.v1",
+ "ordered_fields": [
+ "framed_source_id_utf8",
+ "framed_selector_literal_trade_mutation_lineage_v1",
+ "evidence_policy_digest",
+ "event_id",
+ "event_signature",
+ "first_observed_at_unix_s_u64_be"
+ ]
+ }
+ },
+ "result": {
+ "sealed": true,
+ "caller_forgeable": false,
+ "canonical_bytes_available": true,
+ "manifest_digest_available": true,
+ "debug": "counts_only_redacted",
+ "errors": "crate_owned_source_free_redacted"
+ },
+ "effects": {
+ "sqlite": false,
+ "filesystem": false,
+ "source_or_relay": false,
+ "network": false,
+ "task_spawn": false,
+ "ambient_clock": false,
+ "ambient_entropy": false
+ },
+ "forbidden": [
+ "manifest_from_uncommitted_replay",
+ "caller_supplied_source_result_digest",
+ "caller_supplied_provenance_digest",
+ "arrival_order_selected_truth",
+ "event_authored_time_relabelled_as_observation_time",
+ "manifest_persistence",
+ "reducer_or_final_outcome_authority"
+ ],
+ "deferred": [
+ "wave_qualification",
+ "lineage_reducer",
+ "coverage_and_outcome_projection",
+ "manifest_persistence",
+ "attestation",
+ "publication",
+ "job_finalization"
+ ]
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -11,6 +11,7 @@ mod identity_envelope;
mod reconciliation_attempt;
mod reconciliation_commit;
mod reconciliation_job;
+mod reconciliation_manifest;
mod reconciliation_replay;
mod runtime_adapters;
mod runtime_context;
@@ -88,6 +89,11 @@ pub use reconciliation_job::{
RhiReconciliationLease, RhiReconciliationLeaseOwner, RhiReconciliationRetryDelayMilliseconds,
RhiReconciliationScheduleOutcome, RhiReconciliationUnixMilliseconds,
};
+pub use reconciliation_manifest::{
+ RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION, RhiReconciliationManifest,
+ RhiReconciliationManifestError, RhiReconciliationManifestErrorKind,
+ RhiReconciliationScopePrerequisites,
+};
pub use reconciliation_replay::{
RHI_RECONCILIATION_REPLAY_CONTRACT_VERSION, RhiReconciliationReplayError,
RhiReconciliationReplayErrorKind, RhiReconciliationSourceCursorEvidence,
diff --git a/src/reconciliation_commit.rs b/src/reconciliation_commit.rs
@@ -15,6 +15,7 @@ use crate::{
RhiReconciliationSourceCursorEvidence, RhiReconciliationSourceReplay, RhiStateHostMode,
RhiTradeSourceCursor,
reconciliation_job::{LeaseValidationError, validate_exact_lease},
+ reconciliation_manifest::{RhiCommittedManifestMaterial, committed_manifest_material},
reconciliation_replay::{
RhiReconciliationReplayCommitFact, RhiReconciliationReplayCommitParts,
committed_cursor_evidence,
@@ -160,6 +161,7 @@ pub struct RhiReconciliationSourceCommitOutcome {
checkpoint_advance_count: u32,
dirty_generation_advanced: bool,
committed_cursors: Box<[RhiReconciliationSourceCursorEvidence]>,
+ pub(crate) manifest_material: RhiCommittedManifestMaterial,
}
impl RhiReconciliationSourceCommitOutcome {
@@ -203,6 +205,7 @@ impl fmt::Debug for RhiReconciliationSourceCommitOutcome {
.field("checkpoint_advance_count", &self.checkpoint_advance_count)
.field("dirty_generation_advanced", &self.dirty_generation_advanced)
.field("committed_cursors", &self.committed_cursors.len())
+ .field("manifest_material", &"[sealed]")
.finish()
}
}
@@ -254,6 +257,8 @@ impl RhiReconciliationAttemptRepository<'_> {
}) {
return Err(failure(RhiReconciliationCommitErrorKind::InvalidInput));
}
+ let manifest_material = committed_manifest_material(&plan, &parts)
+ .map_err(|()| failure(RhiReconciliationCommitErrorKind::InvalidInput))?;
let raw = self
.host()
@@ -284,6 +289,7 @@ impl RhiReconciliationAttemptRepository<'_> {
checkpoint_advance_count: raw.checkpoint_advance_count,
dirty_generation_advanced: raw.dirty_generation_advanced,
committed_cursors,
+ manifest_material,
})
}
}
@@ -646,6 +652,12 @@ fn accepted_inventory_digest(
accepted_inventory_digest_for_facts(&part.facts)
}
+pub(crate) fn committed_inventory_digest(
+ part: &RhiReconciliationReplayCommitParts,
+) -> Option<[u8; 32]> {
+ accepted_inventory_digest(part).ok()
+}
+
fn accepted_inventory_digest_for_facts(
facts: &[RhiReconciliationReplayCommitFact],
) -> Result<[u8; 32], CommitOperationError> {
diff --git a/src/reconciliation_manifest.rs b/src/reconciliation_manifest.rs
@@ -0,0 +1,417 @@
+//! Immutable manifest materialization from one confirmed reconciliation commit.
+
+use core::{fmt, num::NonZeroU64};
+use std::error::Error;
+
+use radroots_event::id::{EventId, MutationId, TradeId};
+use radroots_service_host::UnixTimeSeconds;
+use radroots_trade::evidence::{
+ RadrootsTradeEvidenceManifestObservationV1, RadrootsTradeEvidenceManifestSourceResultV1,
+ RadrootsTradeEvidenceManifestV1, RadrootsTradeEvidencePolicyDigestV1,
+ RadrootsTradeEvidenceProvenanceDigestV1, RadrootsTradeEvidenceScopePrerequisitesV1,
+ RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceIdV1,
+ RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultDigestV1,
+ RadrootsTradeEvidenceSourceResultV1, RadrootsTradeSignedEventDigestV1,
+};
+use sha2::{Digest, Sha256};
+
+use crate::{
+ RhiReconciliationAttemptPlan, RhiReconciliationSourceCommitOutcome, RhiTradeSourceCompletion,
+ reconciliation_commit::committed_inventory_digest,
+ reconciliation_replay::{
+ RhiReconciliationReplayCommitFact, RhiReconciliationReplayCommitParts,
+ },
+};
+
+/// Exact version of the RHI reconciliation-manifest materialization contract.
+pub const RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION: u32 = 1;
+
+const SOURCE_RESULT_DIGEST_DOMAIN: &[u8] =
+ b"radroots.rhi.reconciliation_manifest_source_result.v1\0";
+const PROVENANCE_DIGEST_DOMAIN: &[u8] = b"radroots.rhi.evidence_provenance.v1\0";
+const SOURCE_SELECTOR: &[u8] = b"trade_mutation_lineage_v1";
+
+/// Exact non-source prerequisite state bound into one manifest.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiReconciliationScopePrerequisites {
+ Satisfied,
+ Unsatisfied,
+}
+
+/// Stable source-free manifest materialization failure class.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiReconciliationManifestErrorKind {
+ InvalidObservationTime,
+ InvalidCommittedInventory,
+}
+
+impl RhiReconciliationManifestErrorKind {
+ /// Returns the stable machine-readable failure code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidObservationTime => "reconciliation_manifest_observation_time_invalid",
+ Self::InvalidCommittedInventory => "reconciliation_manifest_inventory_invalid",
+ }
+ }
+}
+
+/// Redacted source-free reconciliation-manifest failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiReconciliationManifestError {
+ kind: RhiReconciliationManifestErrorKind,
+}
+
+impl RhiReconciliationManifestError {
+ /// Returns the stable failure class.
+ #[must_use]
+ pub const fn kind(self) -> RhiReconciliationManifestErrorKind {
+ 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 RhiReconciliationManifestError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ RhiReconciliationManifestErrorKind::InvalidObservationTime => {
+ "RHI reconciliation manifest observation time is invalid"
+ }
+ RhiReconciliationManifestErrorKind::InvalidCommittedInventory => {
+ "RHI committed reconciliation inventory is invalid"
+ }
+ })
+ }
+}
+
+impl fmt::Debug for RhiReconciliationManifestError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiReconciliationManifestError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl Error for RhiReconciliationManifestError {}
+
+/// Sealed immutable evidence manifest derived from one confirmed Step190 commit.
+///
+/// Callers cannot forge a manifest by constructing its representation.
+///
+/// ```compile_fail
+/// use rhi::RhiReconciliationManifest;
+///
+/// let _forged = RhiReconciliationManifest { inner: todo!() };
+/// ```
+pub struct RhiReconciliationManifest {
+ inner: RadrootsTradeEvidenceManifestV1,
+}
+
+impl RhiReconciliationManifest {
+ /// Returns the exact RHI manifest-materialization contract version.
+ #[must_use]
+ pub const fn contract_version(&self) -> u32 {
+ RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION
+ }
+
+ /// Returns the exact shared manifest encoding contract ID.
+ #[must_use]
+ pub const fn shared_manifest_contract_id(&self) -> &'static str {
+ self.inner.contract_id()
+ }
+
+ /// Returns the exact shared manifest encoding contract version.
+ #[must_use]
+ pub const fn shared_manifest_contract_version(&self) -> u16 {
+ self.inner.contract_version()
+ }
+
+ /// Returns the exact trade selected by the committed attempt.
+ #[must_use]
+ pub const fn trade_id(&self) -> &TradeId {
+ self.inner.trade_id()
+ }
+
+ /// Returns the exact nonzero dirty generation frozen by the manifest.
+ #[must_use]
+ pub const fn trade_generation(&self) -> u64 {
+ self.inner.trade_generation().get()
+ }
+
+ /// Returns the explicit observation time in UTC seconds.
+ #[must_use]
+ pub const fn observed_at_unix_seconds(&self) -> u64 {
+ self.inner.observed_at_unix_s()
+ }
+
+ /// Returns the exact canonical manifest bytes.
+ #[must_use]
+ pub fn canonical_bytes(&self) -> &[u8] {
+ self.inner.canonical_bytes()
+ }
+
+ /// Returns the domain-separated shared manifest digest bytes.
+ #[must_use]
+ pub fn digest(&self) -> [u8; 32] {
+ *self.inner.digest().as_bytes()
+ }
+
+ /// Returns the exact configured source count.
+ #[must_use]
+ pub fn source_count(&self) -> usize {
+ self.inner.sources().len()
+ }
+
+ /// Returns the exact accepted source-observation count.
+ #[must_use]
+ pub fn observation_count(&self) -> usize {
+ self.inner.observations().len()
+ }
+}
+
+impl fmt::Debug for RhiReconciliationManifest {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiReconciliationManifest")
+ .field("source_count", &self.source_count())
+ .field("observation_count", &self.observation_count())
+ .finish_non_exhaustive()
+ }
+}
+
+impl RhiReconciliationSourceCommitOutcome {
+ /// Freezes the exact committed inventory into the governed shared manifest.
+ ///
+ /// This consumes the sealed commit outcome so uncommitted replay material
+ /// cannot be relabelled as durable evidence. The observation time must be
+ /// at or after every committed source-result completion.
+ pub fn into_evidence_manifest(
+ self,
+ observed_at: UnixTimeSeconds,
+ prerequisites: RhiReconciliationScopePrerequisites,
+ ) -> Result<RhiReconciliationManifest, RhiReconciliationManifestError> {
+ freeze_manifest(self.manifest_material, observed_at, prerequisites)
+ }
+}
+
+pub(crate) struct RhiCommittedManifestMaterial {
+ trade_id: TradeId,
+ generation: NonZeroU64,
+ policy_digest: RadrootsTradeEvidencePolicyDigestV1,
+ latest_finished_unix_ms: u64,
+ sources: Box<[RadrootsTradeEvidenceManifestSourceResultV1]>,
+ observations: Box<[RadrootsTradeEvidenceManifestObservationV1]>,
+}
+
+pub(crate) fn committed_manifest_material(
+ plan: &RhiReconciliationAttemptPlan,
+ parts: &[RhiReconciliationReplayCommitParts],
+) -> Result<RhiCommittedManifestMaterial, ()> {
+ let trade_id = parts.first().ok_or(())?.trade_id;
+ let generation = NonZeroU64::new(plan.input_generation()).ok_or(())?;
+ let policy_digest =
+ RadrootsTradeEvidencePolicyDigestV1::from_bytes(*plan.evidence_policy_digest().as_bytes());
+ let mut latest_finished_unix_ms = 0_u64;
+ let mut sources = Vec::with_capacity(parts.len());
+ let observation_capacity = parts.iter().try_fold(0_usize, |total, part| {
+ total.checked_add(part.facts.len()).ok_or(())
+ })?;
+ let mut observations = Vec::with_capacity(observation_capacity);
+
+ for (ordinal, part) in parts.iter().enumerate() {
+ if part.trade_id != trade_id
+ || part.policy_digest != *plan.evidence_policy_digest().as_bytes()
+ {
+ return Err(());
+ }
+ latest_finished_unix_ms = latest_finished_unix_ms.max(part.result.finished_at().get());
+ let source_id =
+ RadrootsTradeEvidenceSourceIdV1::parse(part.source_id.as_ref()).map_err(|_| ())?;
+ let result = RadrootsTradeEvidenceSourceResultV1::new(
+ if part.required {
+ RadrootsTradeEvidenceSourceRequirementV1::Required
+ } else {
+ RadrootsTradeEvidenceSourceRequirementV1::Optional
+ },
+ map_completion(part.result.outcome()),
+ part.result.accepted_event_count(),
+ )
+ .map_err(|_| ())?;
+ let inventory_digest = committed_inventory_digest(part).ok_or(())?;
+ let result_digest = source_result_digest(plan, ordinal, part, inventory_digest)?;
+ sources.push(RadrootsTradeEvidenceManifestSourceResultV1::new(
+ source_id.clone(),
+ result,
+ RadrootsTradeEvidenceSourceResultDigestV1::from_bytes(result_digest),
+ ));
+ for fact in &part.facts {
+ observations.push(manifest_observation(source_id.clone(), part, fact)?);
+ }
+ }
+
+ Ok(RhiCommittedManifestMaterial {
+ trade_id,
+ generation,
+ policy_digest,
+ latest_finished_unix_ms,
+ sources: sources.into_boxed_slice(),
+ observations: observations.into_boxed_slice(),
+ })
+}
+
+fn freeze_manifest(
+ material: RhiCommittedManifestMaterial,
+ observed_at: UnixTimeSeconds,
+ prerequisites: RhiReconciliationScopePrerequisites,
+) -> Result<RhiReconciliationManifest, RhiReconciliationManifestError> {
+ let earliest_observation = material.latest_finished_unix_ms.div_ceil(1_000);
+ if observed_at.get() < earliest_observation || i64::try_from(observed_at.get()).is_err() {
+ return Err(error(
+ RhiReconciliationManifestErrorKind::InvalidObservationTime,
+ ));
+ }
+ let inner = RadrootsTradeEvidenceManifestV1::new(
+ material.trade_id,
+ material.generation,
+ material.policy_digest,
+ observed_at.get(),
+ match prerequisites {
+ RhiReconciliationScopePrerequisites::Satisfied => {
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied
+ }
+ RhiReconciliationScopePrerequisites::Unsatisfied => {
+ RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied
+ }
+ },
+ material.sources.into_vec(),
+ material.observations.into_vec(),
+ )
+ .map_err(|_| error(RhiReconciliationManifestErrorKind::InvalidCommittedInventory))?;
+ Ok(RhiReconciliationManifest { inner })
+}
+
+fn map_completion(value: RhiTradeSourceCompletion) -> RadrootsTradeEvidenceSourceCompletionV1 {
+ match value {
+ RhiTradeSourceCompletion::Complete => RadrootsTradeEvidenceSourceCompletionV1::Complete,
+ RhiTradeSourceCompletion::Unsupported => {
+ RadrootsTradeEvidenceSourceCompletionV1::Unsupported
+ }
+ RhiTradeSourceCompletion::IncompleteTimeout
+ | RhiTradeSourceCompletion::IncompleteUnavailable
+ | RhiTradeSourceCompletion::IncompleteResourceLimit
+ | RhiTradeSourceCompletion::IncompleteUnknown => {
+ RadrootsTradeEvidenceSourceCompletionV1::Incomplete
+ }
+ }
+}
+
+fn source_result_digest(
+ plan: &RhiReconciliationAttemptPlan,
+ ordinal: usize,
+ part: &RhiReconciliationReplayCommitParts,
+ inventory_digest: [u8; 32],
+) -> Result<[u8; 32], ()> {
+ let mut digest = Sha256::new();
+ digest.update(SOURCE_RESULT_DIGEST_DOMAIN);
+ digest.update(plan.id().as_bytes());
+ digest.update(u32::try_from(ordinal).map_err(|_| ())?.to_be_bytes());
+ digest.update(part.request_id.as_bytes());
+ update_framed(&mut digest, part.source_id.as_bytes())?;
+ digest.update(part.trade_id.as_bytes());
+ digest.update([u8::from(part.required)]);
+ digest.update(part.policy_digest);
+ digest.update(part.selector_digest);
+ digest.update(part.replay_id.as_bytes());
+ update_framed(&mut digest, part.result.outcome().code().as_bytes())?;
+ digest.update(part.result.started_at().get().to_be_bytes());
+ digest.update(part.result.finished_at().get().to_be_bytes());
+ digest.update(part.result.accepted_event_count().to_be_bytes());
+ digest.update(part.result.accepted_event_bytes().to_be_bytes());
+ digest.update(inventory_digest);
+ digest.update(part.duplicate_observations.to_be_bytes());
+ update_optional_u64(&mut digest, part.first_observed_at.map(|value| value.get()));
+ update_optional_cursor(&mut digest, part.prior_cursor);
+ digest.update(part.overlap_seconds.to_be_bytes());
+ digest.update(part.since_unix_seconds.to_be_bytes());
+ update_optional_cursor(&mut digest, part.cursor_candidate);
+ digest.update([u8::from(part.eligible_cursor.is_some())]);
+ Ok(digest.finalize().into())
+}
+
+fn manifest_observation(
+ source_id: RadrootsTradeEvidenceSourceIdV1,
+ part: &RhiReconciliationReplayCommitParts,
+ fact: &RhiReconciliationReplayCommitFact,
+) -> Result<RadrootsTradeEvidenceManifestObservationV1, ()> {
+ let record = &fact.record;
+ let mut provenance = Sha256::new();
+ provenance.update(PROVENANCE_DIGEST_DOMAIN);
+ update_framed(&mut provenance, part.source_id.as_bytes())?;
+ update_framed(&mut provenance, SOURCE_SELECTOR)?;
+ provenance.update(part.policy_digest);
+ provenance.update(record.event_id);
+ provenance.update(record.event_signature);
+ provenance.update(fact.observed_at.get().to_be_bytes());
+ Ok(RadrootsTradeEvidenceManifestObservationV1::new(
+ source_id,
+ MutationId::from_bytes(record.mutation_id),
+ EventId::from_bytes(record.event_id),
+ RadrootsTradeSignedEventDigestV1::sha256(&record.canonical_event_json),
+ RadrootsTradeEvidenceProvenanceDigestV1::from_bytes(provenance.finalize().into()),
+ ))
+}
+
+fn update_framed(digest: &mut Sha256, bytes: &[u8]) -> Result<(), ()> {
+ digest.update(u64::try_from(bytes.len()).map_err(|_| ())?.to_be_bytes());
+ digest.update(bytes);
+ Ok(())
+}
+
+fn update_optional_u64(digest: &mut Sha256, value: Option<u64>) {
+ match value {
+ Some(value) => {
+ digest.update([1]);
+ digest.update(value.to_be_bytes());
+ }
+ None => digest.update([0]),
+ }
+}
+
+fn update_optional_cursor(digest: &mut Sha256, value: Option<crate::RhiTradeSourceCursor>) {
+ match value {
+ Some(value) => {
+ digest.update([1]);
+ digest.update(value.created_at_unix_seconds().to_be_bytes());
+ digest.update(value.event_id());
+ }
+ None => digest.update([0]),
+ }
+}
+
+const fn error(kind: RhiReconciliationManifestErrorKind) -> RhiReconciliationManifestError {
+ RhiReconciliationManifestError { kind }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn diagnostics_are_closed_redacted_and_source_free() {
+ for kind in [
+ RhiReconciliationManifestErrorKind::InvalidObservationTime,
+ RhiReconciliationManifestErrorKind::InvalidCommittedInventory,
+ ] {
+ let error = super::error(kind);
+ assert_eq!(error.kind(), kind);
+ assert!(Error::source(&error).is_none());
+ assert!(!format!("{error} {error:?}").contains("trade-primary"));
+ }
+ }
+}
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -13,6 +13,7 @@ const RUNTIME_ADAPTER_CONTRACT: &str =
const RUNTIME_FOUNDATION: &str = include_str!("../src/runtime_foundation.rs");
const RECONCILIATION_ATTEMPTS: &str = include_str!("../src/reconciliation_attempt.rs");
const RECONCILIATION_COMMIT: &str = include_str!("../src/reconciliation_commit.rs");
+const RECONCILIATION_MANIFEST: &str = include_str!("../src/reconciliation_manifest.rs");
const RECONCILIATION_JOBS: &str = include_str!("../src/reconciliation_job.rs");
const RECONCILIATION_REPLAY: &str = include_str!("../src/reconciliation_replay.rs");
const RECONCILIATION_ATTEMPT_CONTRACT: &str =
@@ -21,6 +22,8 @@ const RECONCILIATION_REPLAY_CONTRACT: &str =
include_str!("../contracts/services_hardening/reconciliation_replay.v1.json");
const RECONCILIATION_COMMIT_CONTRACT: &str =
include_str!("../contracts/services_hardening/reconciliation_commit.v1.json");
+const RECONCILIATION_MANIFEST_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/reconciliation_manifest.v1.json");
const RUNTIME_FOUNDATION_CONTRACT: &str =
include_str!("../contracts/services_hardening/runtime_foundation.v1.json");
const TRADE_INGEST_CONTRACT: &str =
@@ -40,6 +43,7 @@ const SOURCES: &[&str] = &[
include_str!("../src/reconciliation_attempt.rs"),
include_str!("../src/reconciliation_commit.rs"),
include_str!("../src/reconciliation_job.rs"),
+ include_str!("../src/reconciliation_manifest.rs"),
include_str!("../src/reconciliation_replay.rs"),
include_str!("../src/runtime_context.rs"),
include_str!("../src/runtime_adapters.rs"),
@@ -110,6 +114,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"reconciliation_attempt",
"reconciliation_commit",
"reconciliation_job",
+ "reconciliation_manifest",
"reconciliation_replay",
"runtime_context",
"runtime_adapters",
@@ -152,6 +157,9 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"RhiReconciliationAttemptResults",
"RhiReconciliationSourceCommitOutcome",
"RhiReconciliationCommitErrorKind",
+ "RhiReconciliationManifest",
+ "RhiReconciliationManifestErrorKind",
+ "RhiReconciliationScopePrerequisites",
"RhiReconciliationSourceReplayPlan",
"RhiReconciliationSourceReplay",
"RhiReconciliationJobPolicy",
@@ -237,6 +245,39 @@ fn reconciliation_commit_is_atomic_bounded_and_sealed() {
}
#[test]
+fn reconciliation_manifest_is_canonical_sealed_and_effect_free() {
+ let contract: serde_json::Value = serde_json::from_str(RECONCILIATION_MANIFEST_CONTRACT)
+ .expect("reconciliation-manifest contract");
+ assert_eq!(contract["schema"], "radroots.rhi.reconciliation-manifest");
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(
+ contract["construction_authority"],
+ "confirmed_step_190_commit_outcome_only"
+ );
+ assert_eq!(contract["effects"]["sqlite"], false);
+ for required in [
+ "RadrootsTradeEvidenceManifestV1::new(",
+ "SOURCE_RESULT_DIGEST_DOMAIN",
+ "PROVENANCE_DIGEST_DOMAIN",
+ "committed_inventory_digest(part)",
+ "pub fn into_evidence_manifest(",
+ ] {
+ assert!(
+ RECONCILIATION_MANIFEST.contains(required),
+ "reconciliation manifest is missing {required}"
+ );
+ }
+ for forbidden in ["sqlx::", "std::fs", "std::net", "tokio::", "SystemTime"] {
+ assert!(
+ !RECONCILIATION_MANIFEST.contains(forbidden),
+ "reconciliation manifest gained forbidden authority {forbidden}"
+ );
+ }
+ assert!(!ROOT.contains("pub mod reconciliation_manifest"));
+ assert!(!PUBLIC_API.contains("rhi::reconciliation_manifest::"));
+}
+
+#[test]
fn public_errors_are_crate_owned_redacted_and_source_free() {
let production = SOURCES.join("\n");
assert!(!production.contains("fn source("));
@@ -268,7 +309,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, 20);
+ assert_eq!(public_error_count, 21);
}
#[test]
@@ -686,6 +727,8 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"[`reconciliation_replay.v1.json`](contracts/services_hardening/reconciliation_replay.v1.json)",
"## Atomic reconciliation result commit",
"[`reconciliation_commit.v1.json`](contracts/services_hardening/reconciliation_commit.v1.json)",
+ "## Immutable reconciliation manifest",
+ "[`reconciliation_manifest.v1.json`](contracts/services_hardening/reconciliation_manifest.v1.json)",
"configured queue capacity is enforced beneath a fixed 65,536-job",
"from an unexpired claimed job lease",
"can be omitted, duplicated, reordered, or appended beyond",
diff --git a/tests/services_hardening_reconciliation_jobs.rs b/tests/services_hardening_reconciliation_jobs.rs
@@ -11,10 +11,10 @@ use rhi::{
RhiReconciliationAttemptResults, RhiReconciliationCommitErrorKind,
RhiReconciliationJobErrorKind, RhiReconciliationJobPolicy, RhiReconciliationJobState,
RhiReconciliationLease, RhiReconciliationLeaseOwner, RhiReconciliationRetryDelayMilliseconds,
- RhiReconciliationSourceReplayPlan, RhiReconciliationSourceResult,
- RhiReconciliationUnixMilliseconds, RhiRuntimeContext, RhiStateMetadata,
- RhiTradeMutationAdmissionLimits, RhiTradeMutationAuthoredTimePolicy,
- RhiTradeMutationObservedAtUnixSeconds, RhiTradeSourceCompletion, TradeId,
+ RhiReconciliationScopePrerequisites, RhiReconciliationSourceReplayPlan,
+ RhiReconciliationSourceResult, RhiReconciliationUnixMilliseconds, RhiRuntimeContext,
+ RhiStateMetadata, RhiTradeMutationAdmissionLimits, RhiTradeMutationAuthoredTimePolicy,
+ RhiTradeMutationObservedAtUnixSeconds, RhiTradeSourceCompletion, TradeId, UnixTimeSeconds,
admit_rhi_trade_mutation_event, 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,
@@ -997,6 +997,7 @@ async fn source_replay_commit_is_atomic_idempotent_and_mints_durable_cursor_evid
};
let first_replay = make_replay();
let retry_replay = make_replay();
+ let early_replay = make_replay();
let resume_plan = plan.clone();
let attempts = host.repositories().reconciliation_attempts();
let committed = attempts
@@ -1014,23 +1015,72 @@ async fn source_replay_commit_is_atomic_idempotent_and_mints_durable_cursor_evid
.created_at_unix_seconds(),
1_784_347_200
);
+ let committed_cursor = committed.committed_cursors()[0].clone();
let resumed = RhiReconciliationSourceReplayPlan::from_request(
&resume_plan,
&resume_plan.requests()[0],
&configuration,
- Some(committed.committed_cursors()[0].clone()),
+ Some(committed_cursor),
)
.expect("committed cursor resumes exact scope");
assert_eq!(resumed.since_unix_seconds(), 1_784_346_900);
+ let manifest = committed
+ .into_evidence_manifest(
+ UnixTimeSeconds::new(1_784_347_203),
+ RhiReconciliationScopePrerequisites::Satisfied,
+ )
+ .expect("manifest");
+ assert_eq!(manifest.contract_version(), 1);
+ assert_eq!(
+ manifest.shared_manifest_contract_id(),
+ "radroots.trade.evidence-manifest.v1"
+ );
+ assert_eq!(manifest.shared_manifest_contract_version(), 1);
+ assert_eq!(manifest.trade_id(), &TradeId::from_bytes([0x11; 16]));
+ assert_eq!(manifest.trade_generation(), 1);
+ assert_eq!(manifest.observed_at_unix_seconds(), 1_784_347_203);
+ assert_eq!(
+ (manifest.source_count(), manifest.observation_count()),
+ (1, 1)
+ );
+ assert_eq!(
+ manifest.digest(),
+ [
+ 0x0b, 0x19, 0x3e, 0xd2, 0x93, 0x56, 0xd6, 0x3d, 0x37, 0x31, 0x63, 0x4b, 0x37, 0xfe,
+ 0x1f, 0x5d, 0x53, 0x21, 0x48, 0x74, 0x79, 0x3d, 0xc2, 0x3f, 0xe4, 0xa3, 0xb9, 0x84,
+ 0xf8, 0xa4, 0x51, 0xc2,
+ ]
+ );
+ let canonical_manifest = manifest.canonical_bytes().to_vec();
let reconciled = attempts
- .commit_source_replays(lease, plan, [retry_replay])
+ .commit_source_replays(lease, plan.clone(), [retry_replay])
.await
.expect("idempotent reconcile");
assert!(!reconciled.created());
assert_eq!(reconciled.source_result_count(), 1);
assert_eq!(reconciled.checkpoint_advance_count(), 1);
assert!(!reconciled.dirty_generation_advanced());
+ let reconciled_manifest = reconciled
+ .into_evidence_manifest(
+ UnixTimeSeconds::new(1_784_347_203),
+ RhiReconciliationScopePrerequisites::Satisfied,
+ )
+ .expect("idempotent manifest");
+ assert_eq!(reconciled_manifest.canonical_bytes(), canonical_manifest);
+ let too_early = attempts
+ .commit_source_replays(lease, plan, [early_replay])
+ .await
+ .expect("second idempotent reconcile")
+ .into_evidence_manifest(
+ UnixTimeSeconds::new(1_784_347_201),
+ RhiReconciliationScopePrerequisites::Unsatisfied,
+ )
+ .expect_err("observation precedes source completion");
+ assert_eq!(
+ too_early.kind(),
+ rhi::RhiReconciliationManifestErrorKind::InvalidObservationTime
+ );
host.close().await.expect("close");
let options = SqliteConnectOptions::new()
diff --git a/tests/services_hardening_reconciliation_manifest_contract.rs b/tests/services_hardening_reconciliation_manifest_contract.rs
@@ -0,0 +1,133 @@
+#![forbid(unsafe_code)]
+
+use rhi::RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION;
+use serde_json::json;
+
+const CONTRACT: &str =
+ include_str!("../contracts/services_hardening/reconciliation_manifest.v1.json");
+const ROOT: &str = include_str!("../src/lib.rs");
+const SOURCE: &str = include_str!("../src/reconciliation_manifest.rs");
+const COMMIT: &str = include_str!("../src/reconciliation_commit.rs");
+const README: &str = include_str!("../README");
+
+#[test]
+fn machine_contract_freezes_the_complete_step_191_boundary() {
+ let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract");
+ assert_eq!(contract["schema"], "radroots.rhi.reconciliation-manifest");
+ assert_eq!(contract["schema_version"], 1);
+ assert_eq!(
+ contract["contract_version"],
+ RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION
+ );
+ assert_eq!(
+ contract["construction_authority"],
+ "confirmed_step_190_commit_outcome_only"
+ );
+ assert_eq!(
+ contract["shared_manifest"]["contract_id"],
+ "radroots.trade.evidence-manifest.v1"
+ );
+ assert_eq!(contract["shared_manifest"]["source_count_maximum"], 16);
+ assert_eq!(
+ contract["shared_manifest"]["observation_count_maximum"],
+ 65_536
+ );
+ assert_eq!(
+ contract["identity"]["source_selector_digest"],
+ "bound_inside_each_source_result_digest"
+ );
+ assert_eq!(
+ contract["source_result_digest"]["ordered_fields"],
+ json!([
+ "attempt_id",
+ "canonical_source_ordinal_u32_be",
+ "request_id",
+ "framed_source_id_utf8",
+ "trade_id",
+ "required_flag",
+ "evidence_policy_digest",
+ "source_selector_digest",
+ "replay_id",
+ "framed_detailed_completion_code_utf8",
+ "started_at_unix_ms_u64_be",
+ "finished_at_unix_ms_u64_be",
+ "accepted_event_count_u32_be",
+ "accepted_event_bytes_u64_be",
+ "step_190_accepted_inventory_digest",
+ "duplicate_observation_count_u32_be",
+ "optional_first_observed_at_unix_s",
+ "optional_prior_cursor",
+ "overlap_seconds_u64_be",
+ "inclusive_since_unix_s_u64_be",
+ "optional_cursor_candidate",
+ "checkpoint_eligible_flag"
+ ])
+ );
+ assert_eq!(contract["effects"]["sqlite"], false);
+ assert_eq!(contract["effects"]["ambient_clock"], false);
+ assert_eq!(contract["effects"]["ambient_entropy"], false);
+}
+
+#[test]
+fn manifest_boundary_is_sealed_canonical_redacted_and_effect_free() {
+ assert!(ROOT.contains("mod reconciliation_manifest;"));
+ assert!(!ROOT.contains("pub mod reconciliation_manifest;"));
+ for required in [
+ "RhiReconciliationManifest",
+ "RhiReconciliationManifestError",
+ "RhiReconciliationManifestErrorKind",
+ "RhiReconciliationScopePrerequisites",
+ "RHI_RECONCILIATION_MANIFEST_CONTRACT_VERSION",
+ ] {
+ assert!(ROOT.contains(required), "root API is missing {required}");
+ }
+ for required in [
+ "RadrootsTradeEvidenceManifestV1::new(",
+ "SOURCE_RESULT_DIGEST_DOMAIN",
+ "PROVENANCE_DIGEST_DOMAIN",
+ "SOURCE_SELECTOR",
+ "committed_inventory_digest(part)",
+ "RadrootsTradeSignedEventDigestV1::sha256(&record.canonical_event_json)",
+ "self.manifest_material",
+ "i64::try_from(observed_at.get())",
+ "pub const fn shared_manifest_contract_id(&self)",
+ "pub const fn shared_manifest_contract_version(&self)",
+ ] {
+ assert!(
+ SOURCE.contains(required),
+ "manifest boundary is missing {required}"
+ );
+ }
+ assert!(COMMIT.contains("let manifest_material = committed_manifest_material(&plan, &parts)"));
+ assert!(COMMIT.contains("manifest_material,"));
+ assert!(
+ COMMIT
+ .find("let manifest_material = committed_manifest_material(&plan, &parts)")
+ .expect("pure manifest derivation")
+ < COMMIT
+ .find(".transaction(move |transaction|")
+ .expect("transaction boundary")
+ );
+ for forbidden in [
+ "sqlx::",
+ "std::fs",
+ "std::net",
+ "tokio::",
+ "SystemTime",
+ "thread_rng",
+ "OsRng",
+ "from_canonical_bytes",
+ "pub fn new(",
+ "pub const fn new(",
+ "pub fn persist",
+ ] {
+ assert!(
+ !SOURCE.contains(forbidden),
+ "manifest boundary gained forbidden authority {forbidden}"
+ );
+ }
+ assert!(README.contains("## Immutable reconciliation manifest"));
+ assert!(README.contains(
+ "[`reconciliation_manifest.v1.json`](contracts/services_hardening/reconciliation_manifest.v1.json)"
+ ));
+}