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 }