commit 0e436a924e8cbd708b35a708b179e01f6a1450ba
parent de88ddf56a389f1424c704d73f3322aa926cecc8
Author: triesap <tyson@radroots.org>
Date: Sun, 16 Aug 2026 21:50:56 +0000
trade: define evidence coverage and outcomes
- Intent: Add the bounded four-state evidence coverage model, three-state outcome model, and deterministic precedence evaluator.
- Why: Reconciliation consumers need fail-closed scope semantics before immutable evidence and attestation contracts can be built.
- Verification: Extbuild package tests, no-std checks, Clippy, Rustdoc, workspace check, contract guards, and API regeneration passed.
- Risk: The existing reducer evidence state remains temporarily separate until the later generated-consumer and prototype-removal checkpoints.
Diffstat:
4 files changed, 526 insertions(+), 0 deletions(-)
diff --git a/contracts/api_baselines/radroots_trade.txt b/contracts/api_baselines/radroots_trade.txt
@@ -3,6 +3,34 @@ pub mod radroots_trade::evidence
pub enum radroots_trade::evidence::RadrootsTradeAttestationResultV1
pub radroots_trade::evidence::RadrootsTradeAttestationResultV1::Invalid
pub radroots_trade::evidence::RadrootsTradeAttestationResultV1::Valid
+pub enum radroots_trade::evidence::RadrootsTradeEvidenceCoverageError
+pub radroots_trade::evidence::RadrootsTradeEvidenceCoverageError::AdmittedEventCountOutOfRange
+pub radroots_trade::evidence::RadrootsTradeEvidenceCoverageError::NoRequiredSource
+pub radroots_trade::evidence::RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange
+impl core::error::Error for radroots_trade::evidence::RadrootsTradeEvidenceCoverageError
+impl core::fmt::Display for radroots_trade::evidence::RadrootsTradeEvidenceCoverageError
+pub fn radroots_trade::evidence::RadrootsTradeEvidenceCoverageError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub enum radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1
+pub radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1::Missing
+pub radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1::Partial
+pub radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied
+pub radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1::Unsupported
+impl radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1
+pub const fn radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1::permits(self, radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1) -> bool
+pub enum radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1
+pub radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1::Indeterminate
+pub radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1::Invalid
+pub radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1::Valid
+pub enum radroots_trade::evidence::RadrootsTradeEvidenceScopePrerequisitesV1
+pub radroots_trade::evidence::RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied
+pub radroots_trade::evidence::RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied
+pub enum radroots_trade::evidence::RadrootsTradeEvidenceSourceCompletionV1
+pub radroots_trade::evidence::RadrootsTradeEvidenceSourceCompletionV1::Complete
+pub radroots_trade::evidence::RadrootsTradeEvidenceSourceCompletionV1::Incomplete
+pub radroots_trade::evidence::RadrootsTradeEvidenceSourceCompletionV1::Unsupported
+pub enum radroots_trade::evidence::RadrootsTradeEvidenceSourceRequirementV1
+pub radroots_trade::evidence::RadrootsTradeEvidenceSourceRequirementV1::Optional
+pub radroots_trade::evidence::RadrootsTradeEvidenceSourceRequirementV1::Required
pub enum radroots_trade::evidence::RadrootsTradeEvidenceStateV1
pub radroots_trade::evidence::RadrootsTradeEvidenceStateV1::Complete
pub radroots_trade::evidence::RadrootsTradeEvidenceStateV1::Missing
@@ -14,6 +42,12 @@ pub const fn radroots_trade::evidence::RadrootsTradeAttestationRecordV1::claim_m
pub const fn radroots_trade::evidence::RadrootsTradeAttestationRecordV1::event_id(&self) -> &radroots_event::id::EventId
pub const fn radroots_trade::evidence::RadrootsTradeAttestationRecordV1::new(radroots_event::id::EventId, radroots_event::id::MutationId, radroots_trade::evidence::RadrootsTradeAttestationResultV1) -> Self
pub const fn radroots_trade::evidence::RadrootsTradeAttestationRecordV1::result(&self) -> radroots_trade::evidence::RadrootsTradeAttestationResultV1
+pub struct radroots_trade::evidence::RadrootsTradeEvidenceSourceResultV1
+impl radroots_trade::evidence::RadrootsTradeEvidenceSourceResultV1
+pub const fn radroots_trade::evidence::RadrootsTradeEvidenceSourceResultV1::admitted_event_count(&self) -> u32
+pub const fn radroots_trade::evidence::RadrootsTradeEvidenceSourceResultV1::completion(&self) -> radroots_trade::evidence::RadrootsTradeEvidenceSourceCompletionV1
+pub const fn radroots_trade::evidence::RadrootsTradeEvidenceSourceResultV1::new(radroots_trade::evidence::RadrootsTradeEvidenceSourceRequirementV1, radroots_trade::evidence::RadrootsTradeEvidenceSourceCompletionV1, u32) -> core::result::Result<Self, radroots_trade::evidence::RadrootsTradeEvidenceCoverageError>
+pub const fn radroots_trade::evidence::RadrootsTradeEvidenceSourceResultV1::requirement(&self) -> radroots_trade::evidence::RadrootsTradeEvidenceSourceRequirementV1
pub struct radroots_trade::evidence::RadrootsTradeMutationRecordV1
impl radroots_trade::evidence::RadrootsTradeMutationRecordV1
pub const fn radroots_trade::evidence::RadrootsTradeMutationRecordV1::mutation(&self) -> &radroots_event::trade::TradeMutationEnvelopeV1
@@ -24,6 +58,9 @@ impl radroots_trade::evidence::RadrootsTradePrivateTermsEvidenceV1
pub const fn radroots_trade::evidence::RadrootsTradePrivateTermsEvidenceV1::candidate_id(&self) -> &radroots_event::id::CandidateId
pub const fn radroots_trade::evidence::RadrootsTradePrivateTermsEvidenceV1::new(radroots_event::id::CandidateId, radroots_trade::model::RadrootsTradePrivateTermsStateV1) -> Self
pub const fn radroots_trade::evidence::RadrootsTradePrivateTermsEvidenceV1::state(&self) -> radroots_trade::model::RadrootsTradePrivateTermsStateV1
+pub const radroots_trade::evidence::RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE: u32
+pub const radroots_trade::evidence::RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT: usize
+pub fn radroots_trade::evidence::classify_trade_evidence_coverage_v1(impl core::iter::traits::collect::IntoIterator<Item = radroots_trade::evidence::RadrootsTradeEvidenceSourceResultV1>, radroots_trade::evidence::RadrootsTradeEvidenceScopePrerequisitesV1) -> core::result::Result<radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1, radroots_trade::evidence::RadrootsTradeEvidenceCoverageError>
pub mod radroots_trade::model
pub enum radroots_trade::model::OrderIdError
pub radroots_trade::model::OrderIdError::Empty
diff --git a/crates/trade/README.md b/crates/trade/README.md
@@ -58,6 +58,22 @@ Reduction does not retrieve missing records or attestations. A projection's
evidence state and issues describe only the supplied input, not the existence
of additional records elsewhere.
+## Evidence coverage and outcome
+
+`evidence::classify_trade_evidence_coverage_v1` evaluates at most sixteen
+explicit source results using the fixed `Unsupported`, `ScopeSatisfied`,
+`Partial`, then `Missing` precedence. A required unsupported source is
+`Unsupported`; every required source plus the required scope prerequisites is
+`ScopeSatisfied`; any other completed source or admitted evidence is `Partial`;
+otherwise the result is `Missing`. At least one required source is mandatory,
+and one source can represent at most 4,096 admitted events.
+
+Coverage never defaults to completeness. `Valid` and `Invalid` outcomes are
+permitted only for `ScopeSatisfied`; `Missing`, `Partial`, and `Unsupported`
+permit only `Indeterminate`. These portable values classify caller-supplied
+facts and perform no source query, policy lookup, clock read, persistence,
+signing, or publication.
+
## Workflow planning
`WorkflowPlan::prepare` validates a canonical proposal, decision, revision,
diff --git a/crates/trade/src/evidence.rs b/crates/trade/src/evidence.rs
@@ -3,8 +3,407 @@
//! These values describe observed mutation, private-term, and attestation
//! records. They perform no retrieval, signature verification, or decryption.
+use core::fmt;
+
+/// Maximum number of evidence sources admitted by the v1 coverage evaluator.
+pub const RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT: usize = 16;
+
+/// Maximum admitted event count represented for one v1 evidence source.
+pub const RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE: u32 = 4_096;
+
+/// Evidence coverage established for one governed reconciliation scope.
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RadrootsTradeEvidenceCoverageV1 {
+ /// No source completed and no relevant evidence was admitted.
+ #[default]
+ Missing,
+ /// Some relevant completion or evidence exists, but required scope is not satisfied.
+ Partial,
+ /// Every required source completed and every required scope prerequisite is satisfied.
+ ScopeSatisfied,
+ /// At least one required source cannot evaluate the governed scope.
+ Unsupported,
+}
+
+impl RadrootsTradeEvidenceCoverageV1 {
+ /// Returns whether this coverage permits the requested outcome.
+ pub const fn permits(self, outcome: RadrootsTradeEvidenceOutcomeV1) -> bool {
+ matches!(outcome, RadrootsTradeEvidenceOutcomeV1::Indeterminate)
+ || matches!(self, Self::ScopeSatisfied)
+ }
+}
+
+/// Result of evaluating a claim against one immutable evidence scope.
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RadrootsTradeEvidenceOutcomeV1 {
+ Valid,
+ Invalid,
+ #[default]
+ Indeterminate,
+}
+
+/// Completion evidence retained for one configured source.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RadrootsTradeEvidenceSourceCompletionV1 {
+ Complete,
+ #[default]
+ Incomplete,
+ Unsupported,
+}
+
+/// Whether one configured evidence source is required by the governed scope.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RadrootsTradeEvidenceSourceRequirementV1 {
+ Required,
+ Optional,
+}
+
+/// Whether every non-source prerequisite for the governed scope is satisfied.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RadrootsTradeEvidenceScopePrerequisitesV1 {
+ Satisfied,
+ Unsatisfied,
+}
+
+/// Bounded source facts consumed by the v1 coverage evaluator.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct RadrootsTradeEvidenceSourceResultV1 {
+ requirement: RadrootsTradeEvidenceSourceRequirementV1,
+ completion: RadrootsTradeEvidenceSourceCompletionV1,
+ admitted_event_count: u32,
+}
+
+impl RadrootsTradeEvidenceSourceResultV1 {
+ pub const fn new(
+ requirement: RadrootsTradeEvidenceSourceRequirementV1,
+ completion: RadrootsTradeEvidenceSourceCompletionV1,
+ admitted_event_count: u32,
+ ) -> Result<Self, RadrootsTradeEvidenceCoverageError> {
+ if admitted_event_count > RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE {
+ return Err(RadrootsTradeEvidenceCoverageError::AdmittedEventCountOutOfRange);
+ }
+ Ok(Self {
+ requirement,
+ completion,
+ admitted_event_count,
+ })
+ }
+
+ pub const fn requirement(&self) -> RadrootsTradeEvidenceSourceRequirementV1 {
+ self.requirement
+ }
+
+ pub const fn completion(&self) -> RadrootsTradeEvidenceSourceCompletionV1 {
+ self.completion
+ }
+
+ pub const fn admitted_event_count(&self) -> u32 {
+ self.admitted_event_count
+ }
+}
+
+/// Stable failures produced before coverage can be classified.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsTradeEvidenceCoverageError {
+ SourceCountOutOfRange,
+ NoRequiredSource,
+ AdmittedEventCountOutOfRange,
+}
+
+impl fmt::Display for RadrootsTradeEvidenceCoverageError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self {
+ Self::SourceCountOutOfRange => "evidence source count is out of range",
+ Self::NoRequiredSource => "evidence scope has no required source",
+ Self::AdmittedEventCountOutOfRange => {
+ "evidence source admitted event count is out of range"
+ }
+ })
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsTradeEvidenceCoverageError {}
+
+/// Classifies bounded source results using the governed v1 coverage precedence.
+///
+/// The iterator is consumed only through the maximum source count plus one, so
+/// an excessive or infinite input terminates with `SourceCountOutOfRange`.
+pub fn classify_trade_evidence_coverage_v1(
+ sources: impl IntoIterator<Item = RadrootsTradeEvidenceSourceResultV1>,
+ scope_prerequisites: RadrootsTradeEvidenceScopePrerequisitesV1,
+) -> Result<RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceCoverageError> {
+ let mut source_count = 0_usize;
+ let mut has_required_source = false;
+ let mut required_source_unsupported = false;
+ let mut every_required_source_complete = true;
+ let mut any_source_complete = false;
+ let mut any_relevant_evidence = false;
+
+ for source in sources {
+ source_count += 1;
+ if source_count > RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT {
+ return Err(RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange);
+ }
+
+ if matches!(
+ source.requirement,
+ RadrootsTradeEvidenceSourceRequirementV1::Required
+ ) {
+ has_required_source = true;
+ required_source_unsupported |= matches!(
+ source.completion,
+ RadrootsTradeEvidenceSourceCompletionV1::Unsupported
+ );
+ every_required_source_complete &= matches!(
+ source.completion,
+ RadrootsTradeEvidenceSourceCompletionV1::Complete
+ );
+ }
+ any_source_complete |= matches!(
+ source.completion,
+ RadrootsTradeEvidenceSourceCompletionV1::Complete
+ );
+ any_relevant_evidence |= source.admitted_event_count > 0;
+ }
+
+ if source_count == 0 {
+ return Err(RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange);
+ }
+ if !has_required_source {
+ return Err(RadrootsTradeEvidenceCoverageError::NoRequiredSource);
+ }
+
+ Ok(if required_source_unsupported {
+ RadrootsTradeEvidenceCoverageV1::Unsupported
+ } else if every_required_source_complete
+ && matches!(
+ scope_prerequisites,
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied
+ )
+ {
+ RadrootsTradeEvidenceCoverageV1::ScopeSatisfied
+ } else if any_source_complete || any_relevant_evidence {
+ RadrootsTradeEvidenceCoverageV1::Partial
+ } else {
+ RadrootsTradeEvidenceCoverageV1::Missing
+ })
+}
+
pub use crate::trade_contract_v1::{
RadrootsTradeAttestationRecordV1, RadrootsTradeAttestationResultV1,
RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1,
RadrootsTradePrivateTermsEvidenceV1,
};
+
+#[cfg(test)]
+mod tests {
+ #[cfg(not(feature = "std"))]
+ use alloc::{format, string::ToString, vec};
+
+ use super::*;
+
+ fn source(
+ required: bool,
+ completion: RadrootsTradeEvidenceSourceCompletionV1,
+ admitted_event_count: u32,
+ ) -> RadrootsTradeEvidenceSourceResultV1 {
+ let requirement = if required {
+ RadrootsTradeEvidenceSourceRequirementV1::Required
+ } else {
+ RadrootsTradeEvidenceSourceRequirementV1::Optional
+ };
+ RadrootsTradeEvidenceSourceResultV1::new(requirement, completion, admitted_event_count)
+ .expect("valid source result")
+ }
+
+ #[test]
+ fn coverage_precedence_matches_the_governed_vectors() {
+ use RadrootsTradeEvidenceCoverageV1::{Missing, Partial, ScopeSatisfied, Unsupported};
+ use RadrootsTradeEvidenceSourceCompletionV1::{
+ Complete, Incomplete, Unsupported as SourceUnsupported,
+ };
+
+ let vectors = [
+ (vec![source(true, SourceUnsupported, 0)], false, Unsupported),
+ (vec![source(true, Incomplete, 0)], false, Missing),
+ (
+ vec![source(true, Incomplete, 0), source(false, Complete, 1)],
+ false,
+ Partial,
+ ),
+ (vec![source(true, Complete, 1)], false, Partial),
+ (
+ vec![source(true, Complete, 1), source(false, Incomplete, 0)],
+ true,
+ ScopeSatisfied,
+ ),
+ ];
+
+ for (sources, prerequisites, expected) in vectors {
+ let prerequisites = if prerequisites {
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied
+ } else {
+ RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied
+ };
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(sources, prerequisites),
+ Ok(expected)
+ );
+ }
+ }
+
+ #[test]
+ fn optional_sources_cannot_substitute_for_required_completion() {
+ use RadrootsTradeEvidenceCoverageV1::{Missing, Partial};
+ use RadrootsTradeEvidenceSourceCompletionV1::{Complete, Incomplete, Unsupported};
+
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(
+ [source(true, Incomplete, 0), source(false, Unsupported, 0)],
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
+ ),
+ Ok(Missing)
+ );
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(
+ [source(true, Incomplete, 0), source(false, Complete, 0)],
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
+ ),
+ Ok(Partial)
+ );
+ }
+
+ #[test]
+ fn coverage_and_outcome_matrix_fails_closed() {
+ use RadrootsTradeEvidenceCoverageV1::{Missing, Partial, ScopeSatisfied, Unsupported};
+ use RadrootsTradeEvidenceOutcomeV1::{Indeterminate, Invalid, Valid};
+
+ for coverage in [Missing, Partial, Unsupported] {
+ assert!(coverage.permits(Indeterminate));
+ assert!(!coverage.permits(Valid));
+ assert!(!coverage.permits(Invalid));
+ }
+ for outcome in [Valid, Invalid, Indeterminate] {
+ assert!(ScopeSatisfied.permits(outcome));
+ }
+ assert_eq!(RadrootsTradeEvidenceCoverageV1::default(), Missing);
+ assert_eq!(RadrootsTradeEvidenceOutcomeV1::default(), Indeterminate);
+ }
+
+ #[test]
+ fn source_and_event_bounds_are_enforced_before_unbounded_ingestion() {
+ use RadrootsTradeEvidenceCoverageError::{
+ AdmittedEventCountOutOfRange, NoRequiredSource, SourceCountOutOfRange,
+ };
+ use RadrootsTradeEvidenceSourceCompletionV1::Complete;
+
+ let maximum = source(
+ true,
+ Complete,
+ RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE,
+ );
+ assert_eq!(maximum.admitted_event_count(), 4_096);
+ assert_eq!(
+ RadrootsTradeEvidenceSourceResultV1::new(
+ RadrootsTradeEvidenceSourceRequirementV1::Required,
+ Complete,
+ 4_097,
+ ),
+ Err(AdmittedEventCountOutOfRange)
+ );
+
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(
+ core::iter::empty(),
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
+ ),
+ Err(SourceCountOutOfRange)
+ );
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(
+ [source(false, Complete, 0)],
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
+ ),
+ Err(NoRequiredSource)
+ );
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(
+ core::iter::repeat(source(true, Complete, 0)),
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
+ ),
+ Err(SourceCountOutOfRange)
+ );
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(
+ core::iter::repeat_n(
+ source(true, Complete, 0),
+ RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT,
+ ),
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
+ ),
+ Ok(RadrootsTradeEvidenceCoverageV1::ScopeSatisfied)
+ );
+ }
+
+ #[cfg(feature = "json")]
+ #[test]
+ fn coverage_and_outcome_wire_values_are_exact() {
+ use RadrootsTradeEvidenceCoverageV1::{Missing, Partial, ScopeSatisfied, Unsupported};
+ use RadrootsTradeEvidenceOutcomeV1::{Indeterminate, Invalid, Valid};
+
+ for (value, wire) in [
+ (Missing, "\"missing\""),
+ (Partial, "\"partial\""),
+ (ScopeSatisfied, "\"scope_satisfied\""),
+ (Unsupported, "\"unsupported\""),
+ ] {
+ assert_eq!(serde_json::to_string(&value).unwrap(), wire);
+ assert_eq!(
+ serde_json::from_str::<RadrootsTradeEvidenceCoverageV1>(wire).unwrap(),
+ value
+ );
+ }
+ for (value, wire) in [
+ (Valid, "\"valid\""),
+ (Invalid, "\"invalid\""),
+ (Indeterminate, "\"indeterminate\""),
+ ] {
+ assert_eq!(serde_json::to_string(&value).unwrap(), wire);
+ assert_eq!(
+ serde_json::from_str::<RadrootsTradeEvidenceOutcomeV1>(wire).unwrap(),
+ value
+ );
+ }
+ for rejected in ["\"complete\"", "\"query_partial\"", "null", "{}"] {
+ assert!(serde_json::from_str::<RadrootsTradeEvidenceCoverageV1>(rejected).is_err());
+ }
+ }
+
+ #[test]
+ fn coverage_errors_are_stable_and_source_free() {
+ for (error, message) in [
+ (
+ RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange,
+ "evidence source count is out of range",
+ ),
+ (
+ RadrootsTradeEvidenceCoverageError::NoRequiredSource,
+ "evidence scope has no required source",
+ ),
+ (
+ RadrootsTradeEvidenceCoverageError::AdmittedEventCountOutOfRange,
+ "evidence source admitted event count is out of range",
+ ),
+ ] {
+ assert_eq!(error.to_string(), message);
+ assert!(!format!("{error:?}").contains("source_id"));
+ #[cfg(feature = "std")]
+ assert!(std::error::Error::source(&error).is_none());
+ }
+ }
+}
diff --git a/crates/trade/tests/package_boundary.rs b/crates/trade/tests/package_boundary.rs
@@ -4,6 +4,7 @@ use std::collections::BTreeSet;
use radroots_trade::{evidence as _, model as _, reducer as _, validation as _, workflow as _};
const MANIFEST: &str = include_str!("../Cargo.toml");
+const EVIDENCE: &str = include_str!("../src/evidence.rs");
const MODEL: &str = include_str!("../src/model.rs");
const OPERATIONS: &str = include_str!("../../../contracts/operations.toml");
const ROOT: &str = include_str!("../src/lib.rs");
@@ -220,6 +221,76 @@ fn trade_model_reducer_and_evidence_have_final_public_owners() {
}
#[test]
+fn approved_evidence_coverage_and_outcome_are_portable_and_bounded() {
+ use radroots_trade::evidence::{
+ RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE,
+ RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT, RadrootsTradeEvidenceCoverageError,
+ RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceOutcomeV1,
+ RadrootsTradeEvidenceScopePrerequisitesV1, RadrootsTradeEvidenceSourceCompletionV1,
+ RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultV1,
+ classify_trade_evidence_coverage_v1,
+ };
+
+ let source = RadrootsTradeEvidenceSourceResultV1::new(
+ RadrootsTradeEvidenceSourceRequirementV1::Required,
+ RadrootsTradeEvidenceSourceCompletionV1::Complete,
+ RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE,
+ )
+ .expect("maximum source result");
+ assert_eq!(RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT, 16);
+ assert_eq!(
+ classify_trade_evidence_coverage_v1(
+ [source],
+ RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
+ ),
+ Ok(RadrootsTradeEvidenceCoverageV1::ScopeSatisfied)
+ );
+ assert!(
+ RadrootsTradeEvidenceCoverageV1::ScopeSatisfied
+ .permits(RadrootsTradeEvidenceOutcomeV1::Valid)
+ );
+ assert!(
+ !RadrootsTradeEvidenceCoverageV1::Partial.permits(RadrootsTradeEvidenceOutcomeV1::Invalid)
+ );
+
+ fn assert_portable<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {}
+ assert_portable::<RadrootsTradeEvidenceCoverageV1>();
+ assert_portable::<RadrootsTradeEvidenceOutcomeV1>();
+ assert_portable::<RadrootsTradeEvidenceSourceCompletionV1>();
+ assert_portable::<RadrootsTradeEvidenceSourceRequirementV1>();
+ assert_portable::<RadrootsTradeEvidenceScopePrerequisitesV1>();
+ assert_portable::<RadrootsTradeEvidenceSourceResultV1>();
+ assert_portable::<RadrootsTradeEvidenceCoverageError>();
+
+ for required in [
+ "pub enum RadrootsTradeEvidenceCoverageV1",
+ "Missing,",
+ "Partial,",
+ "ScopeSatisfied,",
+ "Unsupported,",
+ "pub enum RadrootsTradeEvidenceOutcomeV1",
+ "pub enum RadrootsTradeEvidenceSourceRequirementV1",
+ "pub enum RadrootsTradeEvidenceScopePrerequisitesV1",
+ "Valid,",
+ "Invalid,",
+ "Indeterminate,",
+ "RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT: usize = 16",
+ "RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE: u32 = 4_096",
+ ] {
+ assert!(
+ EVIDENCE.contains(required),
+ "coverage contract is missing {required}"
+ );
+ }
+ for forbidden in ["std::fs", "std::net", "sqlx", "tokio", "reqwest"] {
+ assert!(
+ !EVIDENCE.contains(forbidden),
+ "evidence coverage acquired side-effect dependency {forbidden}"
+ );
+ }
+}
+
+#[test]
fn workflow_plan_is_root_exported_and_side_effect_free() {
let _: Option<radroots_trade::WorkflowPlan> = None;
let _: Option<radroots_trade::Error> = None;
@@ -276,6 +347,7 @@ fn package_documentation_and_reviewed_api_baseline_are_complete() {
for section in [
"## Canonical surface",
"## Deterministic reduction",
+ "## Evidence coverage and outcome",
"## Workflow planning",
"## Features",
"## Serialization and versioning",
@@ -294,6 +366,8 @@ fn package_documentation_and_reviewed_api_baseline_are_complete() {
assert!(PUBLIC_API.starts_with("pub mod radroots_trade\n"));
for item in [
"pub mod radroots_trade::evidence",
+ "pub enum radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1",
+ "pub enum radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1",
"pub mod radroots_trade::model",
"pub mod radroots_trade::reducer",
"pub mod radroots_trade::validation",