lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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:
Mcontracts/api_baselines/radroots_trade.txt | 37+++++++++++++++++++++++++++++++++++++
Mcrates/trade/README.md | 16++++++++++++++++
Mcrates/trade/src/evidence.rs | 399+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/trade/tests/package_boundary.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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",