lib

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

package_boundary.rs (23898B)


      1 use std::collections::BTreeSet;
      2 
      3 #[allow(unused_imports)]
      4 use radroots_trade::{evidence as _, model as _, reducer as _, validation as _, workflow as _};
      5 
      6 const MANIFEST: &str = include_str!("../Cargo.toml");
      7 const EVIDENCE: &str = include_str!("../src/evidence.rs");
      8 const EVIDENCE_MANIFEST: &str = include_str!("../src/evidence_manifest.rs");
      9 const EVIDENCE_REPORT: &str = include_str!("../src/evidence_report.rs");
     10 const MODEL: &str = include_str!("../src/model.rs");
     11 const OPERATIONS: &str = include_str!("../../../contracts/operations.toml");
     12 const ROOT: &str = include_str!("../src/lib.rs");
     13 const REDUCER_IMPLEMENTATION: &str = include_str!("../src/reducer_impl.rs");
     14 const CANONICAL_REDUCER_VECTORS: &str =
     15     include_str!("../../../contracts/conformance/vectors/trade/reduce_records.v1.json");
     16 const PACKAGED_REDUCER_VECTORS: &str = include_str!("fixtures/reduce_records.v1.json");
     17 const CANONICAL_WORKFLOW_VECTORS: &str =
     18     include_str!("../../../contracts/conformance/vectors/trade/prepare_workflow.v1.json");
     19 const CANONICAL_RHI_REPORT_VECTORS: &str = include_str!(
     20     "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json"
     21 );
     22 const SERVICE_EVENT_DECISION: &str =
     23     include_str!("../../../contracts/architecture/decisions/services_hardening_events.v1.json");
     24 const PACKAGED_WORKFLOW_VECTORS: &str = include_str!("fixtures/prepare_workflow.v1.json");
     25 const WORKFLOW: &str = include_str!("../src/workflow.rs");
     26 const PACKAGE_TIERS: &str = include_str!("../../../contracts/releases/package_tiers.toml");
     27 const README: &str = include_str!("../README.md");
     28 const EXAMPLE: &str = include_str!("../examples/reduce_trade.rs");
     29 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_trade.txt");
     30 
     31 #[test]
     32 fn manifest_has_final_identity_and_required_radroots_dependencies() {
     33     assert!(MANIFEST.contains("name = \"radroots_trade\""));
     34     assert!(MANIFEST.contains("version = \"0.1.0-alpha\""));
     35     assert!(MANIFEST.contains("publish = [\"crates-io\"]"));
     36     assert!(MANIFEST.contains("[lib]\nname = \"radroots_trade\""));
     37 
     38     let dependencies = table_keys(MANIFEST, "[dependencies]");
     39     for dependency in ["radroots_core", "radroots_event", "radroots_identity"] {
     40         assert!(
     41             dependencies.contains(dependency),
     42             "missing required Radroots dependency {dependency}"
     43         );
     44     }
     45 }
     46 
     47 #[test]
     48 fn crate_root_declares_every_approved_module() {
     49     let declared = root_declarations("pub mod ");
     50     for module in ["evidence", "model", "reducer", "validation", "workflow"] {
     51         assert!(
     52             declared.contains(module) || ROOT.contains(&format!("pub mod {module} {{")),
     53             "missing approved module {module}"
     54         );
     55     }
     56 }
     57 
     58 #[test]
     59 fn expired_upward_development_dependencies_are_absent() {
     60     let dev_dependencies = table_keys(MANIFEST, "[dev-dependencies]");
     61     for dependency in ["radroots_nostr", "radroots_transport"] {
     62         assert!(
     63             !dev_dependencies.contains(dependency),
     64             "expired development dependency remains: {dependency}"
     65         );
     66         assert!(
     67             !PACKAGE_TIERS.contains(&format!(
     68                 "owner = \"radroots_trade\"\ndependency = \"{dependency}\""
     69             )),
     70             "expired tier exception remains: {dependency}"
     71         );
     72     }
     73 }
     74 
     75 #[test]
     76 fn protocol_trade_id_is_singular_and_business_order_id_is_distinct() {
     77     let trade_id = radroots_event::trade::TradeId::parse("11".repeat(16))
     78         .expect("canonical protocol trade id");
     79     let order_id = radroots_trade::model::OrderId::parse("order-1").expect("business order id");
     80 
     81     assert_eq!(trade_id.to_string(), "11".repeat(16));
     82     assert_eq!(order_id.as_str(), "order-1");
     83     assert!(!ROOT.contains("pub mod identity;"));
     84     assert!(MODEL.contains("pub struct OrderId(String);"));
     85     assert!(MODEL.contains("No conversion exists between them."));
     86     assert!(!MODEL.contains("From<OrderId> for TradeId"));
     87     assert!(!MODEL.contains("From<TradeId> for OrderId"));
     88 }
     89 
     90 #[test]
     91 fn trade_has_no_authority_or_signing_dependency() {
     92     let dependencies = table_keys(MANIFEST, "[dependencies]");
     93 
     94     assert!(!dependencies.contains("radroots_authority"));
     95     assert!(!MANIFEST.contains("radroots_authority/std"));
     96 }
     97 
     98 #[test]
     99 fn trade_feature_graph_has_no_persistence_or_sql_boundary() {
    100     let features = table_keys(MANIFEST, "[features]");
    101     let dependencies = table_keys(MANIFEST, "[dependencies]");
    102     let dev_dependencies = table_keys(MANIFEST, "[dev-dependencies]");
    103 
    104     assert!(!features.contains("event_store"));
    105     for forbidden in [
    106         "radroots_authority",
    107         "radroots_event_store",
    108         "radroots_outbox",
    109         "radroots_transport",
    110         "reqwest",
    111         "sqlx",
    112         "tokio",
    113     ] {
    114         assert!(
    115             !dependencies.contains(forbidden),
    116             "trade acquired forbidden production dependency {forbidden}"
    117         );
    118     }
    119     for forbidden in ["sqlx", "tokio"] {
    120         assert!(
    121             !dev_dependencies.contains(forbidden),
    122             "trade retained forbidden development dependency {forbidden}"
    123         );
    124     }
    125     assert!(!MANIFEST.contains("sqlite-bundled"));
    126     assert!(!MANIFEST.contains("runtime-tokio"));
    127 }
    128 
    129 #[test]
    130 fn portable_root_exports_and_native_traits_are_compile_checked() {
    131     use radroots_trade::{
    132         Error, Projection, ReducerIssue, ReductionInput, ValidationError, WorkflowPlan,
    133     };
    134 
    135     fn assert_portable<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {}
    136     fn assert_native_error<T: core::error::Error + Send + Sync>() {}
    137 
    138     assert_portable::<Projection>();
    139     assert_portable::<ReducerIssue>();
    140     assert_portable::<ReductionInput>();
    141     assert_portable::<WorkflowPlan>();
    142     assert_native_error::<Error>();
    143     assert_native_error::<ValidationError>();
    144 
    145     assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]"));
    146     assert!(!ROOT.contains("pub trait "));
    147     for export in [
    148         "pub use model::RadrootsTradeProjectionV1 as Projection;",
    149         "RadrootsTradeReducerIssueV1 as ReducerIssue",
    150         "RadrootsTradeReductionInputV1 as ReductionInput",
    151         "pub use validation::ValidationError;",
    152         "pub use workflow::{Error, WorkflowPlan};",
    153     ] {
    154         assert!(
    155             ROOT.contains(export),
    156             "missing canonical root export {export}"
    157         );
    158     }
    159 }
    160 
    161 #[test]
    162 fn packaged_trade_vectors_match_canonical_operation_contracts() {
    163     assert_eq!(PACKAGED_REDUCER_VECTORS, CANONICAL_REDUCER_VECTORS);
    164     assert_eq!(PACKAGED_WORKFLOW_VECTORS, CANONICAL_WORKFLOW_VECTORS);
    165     for required in [
    166         "id = \"trade.reduce_records\"",
    167         "crates/trade/src/reducer.rs",
    168         "radroots_trade::model::RadrootsTradeProjectionV1",
    169         "id = \"trade.prepare_workflow\"",
    170         "crates/trade/src/workflow.rs",
    171         "contracts/conformance/vectors/trade/prepare_workflow.v1.json",
    172     ] {
    173         assert!(
    174             OPERATIONS.contains(required),
    175             "operation contract is missing {required}"
    176         );
    177     }
    178     assert!(!OPERATIONS.contains("radroots_trade::workflow::RadrootsTradeProjectionV1"));
    179 }
    180 
    181 #[test]
    182 fn trade_model_reducer_and_evidence_have_final_public_owners() {
    183     use radroots_trade::{Projection, ReducerIssue, ReductionInput};
    184     use radroots_trade::{evidence::RadrootsTradeEvidenceStateV1, reducer::reduce_trade_records};
    185 
    186     let trade_id = radroots_event::trade::TradeId::parse("22".repeat(16))
    187         .expect("canonical protocol trade id");
    188     let input = ReductionInput::new(trade_id)
    189         .with_evidence_state(RadrootsTradeEvidenceStateV1::Complete)
    190         .with_mutations(Vec::new())
    191         .with_private_terms(Vec::new())
    192         .with_attestations(Vec::new())
    193         .with_observed_at_unix_s(Some(42));
    194 
    195     assert_eq!(input.trade_id(), &trade_id);
    196     assert_eq!(
    197         input.evidence_state(),
    198         RadrootsTradeEvidenceStateV1::Complete
    199     );
    200     assert_eq!(input.observed_at_unix_s(), Some(42));
    201     assert!(input.mutations().is_empty());
    202 
    203     let projection: Projection = reduce_trade_records(input.clone());
    204     let _: &[ReducerIssue] = projection.issues();
    205     assert_eq!(projection.trade_id(), &trade_id);
    206     assert_eq!(
    207         projection.evidence_state(),
    208         RadrootsTradeEvidenceStateV1::Missing
    209     );
    210 
    211     let serialized = serde_json::to_value(input).expect("serialize reduction input");
    212     assert_eq!(serialized["trade_id"], trade_id.to_string());
    213     assert_eq!(serialized["observed_at_unix_s"], 42);
    214     assert_eq!(serialized["mutations"], serde_json::json!([]));
    215 
    216     for declaration in [
    217         "pub trade_id:",
    218         "pub mutations:",
    219         "pub projection_digest:",
    220         "pub candidate_id:",
    221         "pub claim_mutation_id:",
    222     ] {
    223         assert!(
    224             !REDUCER_IMPLEMENTATION.contains(declaration),
    225             "native trade contract field must remain private: {declaration}"
    226         );
    227     }
    228 }
    229 
    230 #[test]
    231 fn approved_evidence_coverage_and_outcome_are_portable_and_bounded() {
    232     use radroots_trade::evidence::{
    233         RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE,
    234         RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT, RadrootsTradeEvidenceCoverageError,
    235         RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceOutcomeV1,
    236         RadrootsTradeEvidenceScopePrerequisitesV1, RadrootsTradeEvidenceSourceCompletionV1,
    237         RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultV1,
    238         classify_trade_evidence_coverage_v1,
    239     };
    240 
    241     let source = RadrootsTradeEvidenceSourceResultV1::new(
    242         RadrootsTradeEvidenceSourceRequirementV1::Required,
    243         RadrootsTradeEvidenceSourceCompletionV1::Complete,
    244         RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE,
    245     )
    246     .expect("maximum source result");
    247     assert_eq!(RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT, 16);
    248     assert_eq!(
    249         classify_trade_evidence_coverage_v1(
    250             [source],
    251             RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    252         ),
    253         Ok(RadrootsTradeEvidenceCoverageV1::ScopeSatisfied)
    254     );
    255     assert!(
    256         RadrootsTradeEvidenceCoverageV1::ScopeSatisfied
    257             .permits(RadrootsTradeEvidenceOutcomeV1::Valid)
    258     );
    259     assert!(
    260         !RadrootsTradeEvidenceCoverageV1::Partial.permits(RadrootsTradeEvidenceOutcomeV1::Invalid)
    261     );
    262 
    263     fn assert_portable<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {}
    264     assert_portable::<RadrootsTradeEvidenceCoverageV1>();
    265     assert_portable::<RadrootsTradeEvidenceOutcomeV1>();
    266     assert_portable::<RadrootsTradeEvidenceSourceCompletionV1>();
    267     assert_portable::<RadrootsTradeEvidenceSourceRequirementV1>();
    268     assert_portable::<RadrootsTradeEvidenceScopePrerequisitesV1>();
    269     assert_portable::<RadrootsTradeEvidenceSourceResultV1>();
    270     assert_portable::<RadrootsTradeEvidenceCoverageError>();
    271 
    272     for required in [
    273         "pub enum RadrootsTradeEvidenceCoverageV1",
    274         "Missing,",
    275         "Partial,",
    276         "ScopeSatisfied,",
    277         "Unsupported,",
    278         "pub enum RadrootsTradeEvidenceOutcomeV1",
    279         "pub enum RadrootsTradeEvidenceSourceRequirementV1",
    280         "pub enum RadrootsTradeEvidenceScopePrerequisitesV1",
    281         "Valid,",
    282         "Invalid,",
    283         "Indeterminate,",
    284         "RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT: usize = 16",
    285         "RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE: u32 = 4_096",
    286     ] {
    287         assert!(
    288             EVIDENCE.contains(required),
    289             "coverage contract is missing {required}"
    290         );
    291     }
    292     for forbidden in ["std::fs", "std::net", "sqlx", "tokio", "reqwest"] {
    293         assert!(
    294             !EVIDENCE.contains(forbidden),
    295             "evidence coverage acquired side-effect dependency {forbidden}"
    296         );
    297     }
    298 }
    299 
    300 #[test]
    301 fn immutable_evidence_manifest_is_sealed_bounded_and_side_effect_free() {
    302     use radroots_trade::evidence::{
    303         RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_ID,
    304         RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_VERSION,
    305         RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES,
    306         RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS,
    307         RADROOTS_TRADE_EVIDENCE_SOURCE_ID_MAXIMUM_BYTES, RadrootsTradeEvidenceManifestDigestV1,
    308         RadrootsTradeEvidenceManifestError, RadrootsTradeEvidenceManifestObservationV1,
    309         RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidenceManifestV1,
    310         RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceProvenanceDigestV1,
    311         RadrootsTradeEvidenceSourceIdV1, RadrootsTradeEvidenceSourceResultDigestV1,
    312         RadrootsTradeSignedEventDigestV1,
    313     };
    314 
    315     fn assert_portable<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {}
    316     assert_portable::<RadrootsTradeEvidenceManifestDigestV1>();
    317     assert_portable::<RadrootsTradeEvidenceManifestError>();
    318     assert_portable::<RadrootsTradeEvidenceManifestObservationV1>();
    319     assert_portable::<RadrootsTradeEvidenceManifestSourceResultV1>();
    320     assert_portable::<RadrootsTradeEvidenceManifestV1>();
    321     assert_portable::<RadrootsTradeEvidencePolicyDigestV1>();
    322     assert_portable::<RadrootsTradeEvidenceProvenanceDigestV1>();
    323     assert_portable::<RadrootsTradeEvidenceSourceIdV1>();
    324     assert_portable::<RadrootsTradeEvidenceSourceResultDigestV1>();
    325     assert_portable::<RadrootsTradeSignedEventDigestV1>();
    326 
    327     assert_eq!(
    328         RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_ID,
    329         "radroots.trade.evidence-manifest.v1"
    330     );
    331     assert_eq!(RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_VERSION, 1);
    332     assert_eq!(RADROOTS_TRADE_EVIDENCE_SOURCE_ID_MAXIMUM_BYTES, 64);
    333     assert_eq!(
    334         RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS,
    335         65_536
    336     );
    337     assert_eq!(
    338         RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES,
    339         16 * 1024 * 1024
    340     );
    341 
    342     for forbidden in [
    343         "std::fs",
    344         "std::net",
    345         "sqlx",
    346         "tokio",
    347         "reqwest",
    348         "radroots_transport",
    349         "spawn",
    350         "SystemTime",
    351     ] {
    352         assert!(
    353             !EVIDENCE_MANIFEST.contains(forbidden),
    354             "manifest acquired forbidden side effect or upward dependency: {forbidden}"
    355         );
    356     }
    357     for field in [
    358         "pub trade_id:",
    359         "pub sources:",
    360         "pub observations:",
    361         "pub canonical_bytes:",
    362         "pub digest:",
    363     ] {
    364         assert!(
    365             !EVIDENCE_MANIFEST.contains(field),
    366             "manifest field escaped: {field}"
    367         );
    368     }
    369     assert!(ROOT.contains("mod evidence_manifest;"));
    370     assert!(!ROOT.contains("pub mod evidence_manifest;"));
    371 }
    372 
    373 #[test]
    374 fn immutable_evidence_report_is_sealed_bounded_and_vector_bound() {
    375     use radroots_trade::evidence::{
    376         RADROOTS_RHI_EVIDENCE_ATTESTATION_METHOD, RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_ID,
    377         RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_VERSION,
    378         RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_CANONICAL_BYTES,
    379         RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_REASON_CODES,
    380         RADROOTS_RHI_EVIDENCE_REPORT_REASON_CODE_MAXIMUM_BYTES, RadrootsRhiEvidenceReasonCodeV1,
    381         RadrootsRhiEvidenceReportError, RadrootsRhiEvidenceReportV1,
    382         RadrootsRhiEvidenceStatementDigestV1, RadrootsRhiEvidenceSupersessionV1,
    383         RadrootsTradeEvidenceProjectionDigestV1,
    384     };
    385 
    386     fn assert_portable<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {}
    387     assert_portable::<RadrootsRhiEvidenceReasonCodeV1>();
    388     assert_portable::<RadrootsRhiEvidenceReportError>();
    389     assert_portable::<RadrootsRhiEvidenceReportV1>();
    390     assert_portable::<RadrootsRhiEvidenceStatementDigestV1>();
    391     assert_portable::<RadrootsRhiEvidenceSupersessionV1>();
    392     assert_portable::<RadrootsTradeEvidenceProjectionDigestV1>();
    393 
    394     assert_eq!(
    395         RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_ID,
    396         "radroots.rhi.evidence_attestation.v1"
    397     );
    398     assert_eq!(RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_VERSION, 1);
    399     assert_eq!(
    400         RADROOTS_RHI_EVIDENCE_ATTESTATION_METHOD,
    401         "signed_evidence_snapshot"
    402     );
    403     assert_eq!(RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_REASON_CODES, 16);
    404     assert_eq!(RADROOTS_RHI_EVIDENCE_REPORT_REASON_CODE_MAXIMUM_BYTES, 64);
    405     assert_eq!(
    406         RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_CANONICAL_BYTES,
    407         16 * 1024
    408     );
    409 
    410     for required in [
    411         "requires_both_references_or_neither",
    412         "report_id_equals_statement_digest",
    413         "reason_codes_sorted_unique",
    414     ] {
    415         assert!(
    416             SERVICE_EVENT_DECISION.contains(required),
    417             "service-event decision is missing {required}"
    418         );
    419     }
    420     assert!(EVIDENCE_REPORT.contains("radroots:rhi-evidence-attestation-statement:v1\\0"));
    421 
    422     let vectors: serde_json::Value =
    423         serde_json::from_str(CANONICAL_RHI_REPORT_VECTORS).expect("RHI report vectors");
    424     for vector in vectors["vectors"]
    425         .as_array()
    426         .expect("RHI report vector array")
    427         .iter()
    428         .filter(|vector| vector["kind"] == "rhi.evidence_attestation.valid")
    429     {
    430         let expected = &vector["expected"];
    431         let content = expected["canonical_event_content_utf8"]
    432             .as_str()
    433             .expect("canonical report content");
    434         let report = RadrootsRhiEvidenceReportV1::from_canonical_content(content.as_bytes())
    435             .expect("canonical report parses");
    436         assert_eq!(
    437             report.canonical_statement_payload(),
    438             expected["canonical_statement_payload_utf8"]
    439                 .as_str()
    440                 .expect("canonical statement payload")
    441         );
    442         assert_eq!(
    443             report.statement_digest().to_hex(),
    444             expected["statement_digest"]
    445                 .as_str()
    446                 .expect("statement digest")
    447         );
    448         assert_eq!(
    449             report.statement_digest().to_hex(),
    450             expected["report_id"].as_str().expect("report identity")
    451         );
    452     }
    453     for forbidden in [
    454         "std::fs",
    455         "std::net",
    456         "sqlx",
    457         "tokio",
    458         "reqwest",
    459         "radroots_transport",
    460         ".sign(",
    461         ".publish(",
    462     ] {
    463         assert!(
    464             !EVIDENCE_REPORT.contains(forbidden),
    465             "report acquired forbidden side effect or upward dependency: {forbidden}"
    466         );
    467     }
    468     for field in [
    469         "pub issuer_public_key:",
    470         "pub reason_codes:",
    471         "pub canonical_statement_payload:",
    472         "pub canonical_content:",
    473     ] {
    474         assert!(
    475             !EVIDENCE_REPORT.contains(field),
    476             "report field escaped: {field}"
    477         );
    478     }
    479     assert!(ROOT.contains("mod evidence_report;"));
    480     assert!(!ROOT.contains("pub mod evidence_report;"));
    481     assert!(!PUBLIC_API.contains("RadrootsRhiEvidenceStatementDigestV1::sha256"));
    482 }
    483 
    484 #[test]
    485 fn workflow_plan_is_root_exported_and_side_effect_free() {
    486     let _: Option<radroots_trade::WorkflowPlan> = None;
    487     let _: Option<radroots_trade::Error> = None;
    488 
    489     for forbidden in [
    490         "std::fs",
    491         "std::net",
    492         "sqlx",
    493         "tokio",
    494         "reqwest",
    495         "event_store",
    496         "outbox",
    497         ".sign(",
    498         ".deliver(",
    499         ".execute(",
    500     ] {
    501         assert!(
    502             !WORKFLOW.contains(forbidden),
    503             "workflow plan acquired host side-effect authority: {forbidden}"
    504         );
    505     }
    506     for action in ["Sign", "Persist", "Deliver", "VerifyPrivateTerms"] {
    507         assert!(
    508             WORKFLOW.contains(action),
    509             "missing workflow action {action}"
    510         );
    511     }
    512 }
    513 
    514 #[test]
    515 fn superseded_trade_surfaces_are_removed() {
    516     assert!(!ROOT.contains("pub mod identity;"));
    517     assert!(!ROOT.contains("pub mod prelude;"));
    518     assert!(!WORKFLOW.contains("Temporary migration reexports"));
    519     assert!(!WORKFLOW.contains("pub use crate::trade_contract_v1"));
    520 
    521     for declaration in [
    522         "pub mod dto;",
    523         "pub mod operational_listing;",
    524         "pub mod validation_receipt;",
    525     ] {
    526         assert!(
    527             !ROOT.contains(declaration),
    528             "temporary surface remains: {declaration}"
    529         );
    530     }
    531     for forbidden in ["radroots_event_codec", "dto-bindgen", "dto_bindgen"] {
    532         assert!(!MANIFEST.contains(forbidden));
    533     }
    534 }
    535 
    536 #[test]
    537 fn package_documentation_and_reviewed_api_baseline_are_complete() {
    538     for section in [
    539         "## Canonical surface",
    540         "## Deterministic reduction",
    541         "## Evidence coverage and outcome",
    542         "## Immutable evidence manifests",
    543         "## Immutable evidence reports",
    544         "## Workflow planning",
    545         "## Features",
    546         "## Serialization and versioning",
    547         "## Security and trust boundaries",
    548         "## Side effects, cancellation, and commit points",
    549         "## Intended consumers",
    550         "## Package charter",
    551     ] {
    552         assert!(README.contains(section), "README is missing {section}");
    553     }
    554     assert!(ROOT.contains("#![doc = include_str!(\"../README.md\")]"));
    555     assert!(
    556         EXAMPLE.contains("use radroots_trade::{ReductionInput, reducer::reduce_trade_records};")
    557     );
    558 
    559     assert!(PUBLIC_API.starts_with("pub mod radroots_trade\n"));
    560     for item in [
    561         "pub mod radroots_trade::evidence",
    562         "pub enum radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1",
    563         "pub enum radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1",
    564         "pub struct radroots_trade::evidence::RadrootsTradeEvidenceManifestV1",
    565         "pub struct radroots_trade::evidence::RadrootsTradeEvidenceManifestDigestV1",
    566         "pub struct radroots_trade::evidence::RadrootsTradeEvidencePolicyDigestV1",
    567         "pub struct radroots_trade::evidence::RadrootsTradeEvidenceProvenanceDigestV1",
    568         "pub struct radroots_trade::evidence::RadrootsTradeEvidenceSourceResultDigestV1",
    569         "pub struct radroots_trade::evidence::RadrootsTradeSignedEventDigestV1",
    570         "pub struct radroots_trade::evidence::RadrootsRhiEvidenceReasonCodeV1",
    571         "pub struct radroots_trade::evidence::RadrootsRhiEvidenceReportV1",
    572         "pub struct radroots_trade::evidence::RadrootsRhiEvidenceStatementDigestV1",
    573         "pub struct radroots_trade::evidence::RadrootsRhiEvidenceSupersessionV1",
    574         "pub struct radroots_trade::evidence::RadrootsTradeEvidenceProjectionDigestV1",
    575         "pub mod radroots_trade::model",
    576         "pub mod radroots_trade::reducer",
    577         "pub mod radroots_trade::validation",
    578         "pub mod radroots_trade::workflow",
    579         "pub struct radroots_trade::Projection",
    580         "pub enum radroots_trade::ReducerIssue",
    581         "pub struct radroots_trade::ReductionInput",
    582         "pub struct radroots_trade::ValidationError",
    583         "pub struct radroots_trade::WorkflowPlan",
    584     ] {
    585         assert!(PUBLIC_API.contains(item), "API baseline is missing {item}");
    586     }
    587     for forbidden in [
    588         "radroots_authority::",
    589         "radroots_event_store::",
    590         "radroots_outbox::",
    591         "radroots_transport::",
    592         "reqwest::",
    593         "sqlx::",
    594         "tokio::",
    595     ] {
    596         assert!(
    597             !PUBLIC_API.contains(forbidden),
    598             "API baseline exposes forbidden host path {forbidden}"
    599         );
    600     }
    601 }
    602 
    603 fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> {
    604     let Some((_, table)) = manifest.split_once(heading) else {
    605         return BTreeSet::new();
    606     };
    607     table
    608         .lines()
    609         .skip(1)
    610         .take_while(|line| !line.trim_start().starts_with('['))
    611         .filter_map(|line| {
    612             let line = line.trim();
    613             (line
    614                 .bytes()
    615                 .next()
    616                 .is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_')
    617                 && !line.starts_with('#'))
    618             .then(|| line.split_once('=').map(|(key, _)| key.trim()))
    619             .flatten()
    620         })
    621         .collect()
    622 }
    623 
    624 fn root_declarations(prefix: &str) -> BTreeSet<&str> {
    625     ROOT.lines()
    626         .map(str::trim)
    627         .filter_map(|line| line.strip_prefix(prefix))
    628         .filter_map(|name| name.strip_suffix(';'))
    629         .collect()
    630 }