contract.rs (221715B)
1 #![forbid(unsafe_code)] 2 3 use crate::coverage::{CoveragePolicyFile, CoverageThresholds, read_coverage_policy}; 4 use serde::Deserialize; 5 use serde_json::Value; 6 use std::collections::{BTreeMap, BTreeSet}; 7 use std::env; 8 use std::fs; 9 use std::path::{Path, PathBuf}; 10 11 const ROOT_RELEASE_POLICY_RELATIVE: &str = 12 "foundation/contracts/release_runtime/mounted_rust_crates/publish-policy.toml"; 13 const CONFORMANCE_ROOT_RELATIVE: &str = "contracts/conformance"; 14 const CONFORMANCE_SCHEMA_RELATIVE: &str = "contracts/conformance/schema/vector.schema.json"; 15 const RELEASE_POLICY_ENV: &str = "RADROOTS_MOUNTED_RUST_CRATE_PUBLISH_POLICY"; 16 const EVENT_BOUNDARY_MATRIX_ENV: &str = "RADROOTS_EVENT_BOUNDARY_MATRIX"; 17 const COVERAGE_REQUIRED_THRESHOLD: f64 = 100.0; 18 const COVERAGE_REQUIRED_THRESHOLD_LABEL: &str = "100/100/100/100"; 19 const COVERAGE_REPORT_EPSILON: f64 = 0.000_001; 20 const EVENT_BOUNDARY_MATRIX_RELATIVES: [&str; 1] = [ 21 "docs/platform/canonical/open_source/radroots_v1_spec/02_public_contract_and_runtime/08_event_boundary_matrix.md", 22 ]; 23 24 #[derive(Debug, Deserialize)] 25 #[serde(deny_unknown_fields)] 26 pub struct ContractManifest { 27 pub contract: ManifestContract, 28 pub surface: Surface, 29 pub policy: Policy, 30 } 31 32 #[derive(Debug, Deserialize)] 33 #[serde(deny_unknown_fields)] 34 pub struct ManifestContract { 35 pub name: String, 36 pub version: String, 37 pub source: String, 38 } 39 40 #[derive(Debug, Deserialize)] 41 #[serde(deny_unknown_fields)] 42 pub struct Surface { 43 pub model_crates: Vec<String>, 44 pub algorithm_crates: Vec<String>, 45 pub rust_crate_tiers: Option<RustCrateTiers>, 46 pub internal_replica_crates: Option<InternalReplicaCrates>, 47 } 48 49 #[derive(Debug, Deserialize)] 50 #[serde(deny_unknown_fields)] 51 pub struct RustCrateTiers { 52 pub advanced_substrate: Vec<String>, 53 pub published_support: Vec<String>, 54 pub deferred_publication: Vec<String>, 55 } 56 57 #[derive(Debug, Deserialize)] 58 #[serde(deny_unknown_fields)] 59 pub struct InternalReplicaCrates { 60 pub schema: String, 61 pub storage: String, 62 pub sync: String, 63 } 64 65 #[derive(Debug, Deserialize)] 66 #[serde(deny_unknown_fields)] 67 pub struct Policy { 68 pub exclude_internal_workspace_crates: bool, 69 pub require_reproducible_exports: bool, 70 pub require_conformance_vectors: bool, 71 pub replica: Option<ReplicaPolicy>, 72 } 73 74 #[derive(Debug, Deserialize)] 75 #[serde(deny_unknown_fields)] 76 pub struct ReplicaPolicy { 77 pub forbid_legacy_alias_identifiers: bool, 78 pub require_transport_agnostic_sync_contract: bool, 79 pub require_deterministic_emit_ingest: bool, 80 } 81 82 #[derive(Debug, Deserialize)] 83 #[serde(deny_unknown_fields)] 84 pub struct OperationsContractManifest { 85 pub contract: ManifestContract, 86 pub public: PublicContract, 87 pub shared_types: SharedTypesContract, 88 pub errors: ErrorClassesContract, 89 pub operations: BTreeMap<String, PublicOperationContract>, 90 pub implementation_provenance: Option<ImplementationProvenance>, 91 } 92 93 #[derive(Debug, Deserialize)] 94 #[serde(deny_unknown_fields)] 95 pub struct PublicContract { 96 pub domains: Vec<String>, 97 } 98 99 #[derive(Debug, Deserialize)] 100 #[serde(deny_unknown_fields)] 101 pub struct SharedTypesContract { 102 pub public: Vec<String>, 103 } 104 105 #[derive(Debug, Deserialize)] 106 #[serde(deny_unknown_fields)] 107 pub struct ErrorClassesContract { 108 pub classes: Vec<String>, 109 } 110 111 #[derive(Debug, Deserialize)] 112 #[serde(deny_unknown_fields)] 113 pub struct ImplementationProvenance { 114 pub model_crates: Vec<String>, 115 pub algorithm_crates: Vec<String>, 116 } 117 118 #[derive(Debug, Deserialize)] 119 #[serde(deny_unknown_fields)] 120 pub struct PublicOperationContract { 121 pub domain: String, 122 pub id: String, 123 pub stability: String, 124 pub inputs: Vec<String>, 125 pub outputs: Vec<String>, 126 pub error_class: String, 127 #[allow(dead_code)] 128 pub deterministic: bool, 129 pub signing: String, 130 pub transport: String, 131 pub implementation: PublicOperationImplementation, 132 pub conformance: PublicOperationConformance, 133 } 134 135 #[derive(Debug, Deserialize)] 136 #[serde(deny_unknown_fields)] 137 pub struct PublicOperationImplementation { 138 pub rust_modules: Vec<String>, 139 pub rust_types: Vec<String>, 140 } 141 142 #[derive(Debug, Deserialize)] 143 #[serde(deny_unknown_fields)] 144 pub struct PublicOperationConformance { 145 pub vector: String, 146 } 147 148 #[derive(Debug, Deserialize)] 149 #[serde(deny_unknown_fields)] 150 pub struct VersionPolicy { 151 pub contract: VersionContract, 152 pub semver: SemverRules, 153 pub compatibility: CompatibilityRules, 154 } 155 156 #[derive(Debug, Deserialize)] 157 #[serde(deny_unknown_fields)] 158 pub struct VersionContract { 159 pub version: String, 160 pub stability: String, 161 } 162 163 #[derive(Debug, Deserialize)] 164 #[serde(deny_unknown_fields)] 165 pub struct SemverRules { 166 pub major_on: Vec<String>, 167 pub minor_on: Vec<String>, 168 pub patch_on: Vec<String>, 169 } 170 171 #[derive(Debug, Deserialize)] 172 #[serde(deny_unknown_fields)] 173 pub struct CompatibilityRules { 174 pub requires_conformance_pass: bool, 175 pub requires_contract_manifest_diff: bool, 176 pub requires_release_notes: bool, 177 } 178 179 #[derive(Debug)] 180 pub struct ContractBundle { 181 pub root: PathBuf, 182 pub manifest: ContractManifest, 183 pub version: VersionPolicy, 184 pub operations_manifest: Option<OperationsContractManifest>, 185 } 186 187 #[derive(Debug, Deserialize)] 188 struct WorkspaceCargoManifest { 189 workspace: WorkspaceSection, 190 } 191 192 #[derive(Debug, Deserialize)] 193 struct WorkspaceSection { 194 members: Vec<String>, 195 } 196 197 #[derive(Debug, Deserialize)] 198 struct PackageCargoManifest { 199 package: PackageSection, 200 } 201 202 #[derive(Debug, Deserialize)] 203 struct PackageSection { 204 name: String, 205 publish: Option<PackagePublish>, 206 } 207 208 #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] 209 #[serde(untagged)] 210 enum PackagePublish { 211 Bool(bool), 212 Registries(Vec<String>), 213 } 214 215 #[cfg_attr(not(test), allow(dead_code))] 216 #[derive(Debug, Deserialize)] 217 struct CoverageRequiredFile { 218 required: CoverageRequiredSection, 219 } 220 221 #[cfg_attr(not(test), allow(dead_code))] 222 #[derive(Debug, Deserialize)] 223 struct CoverageRequiredSection { 224 crates: Vec<String>, 225 } 226 227 #[derive(Debug, Clone, PartialEq, Eq)] 228 struct EventBoundaryRow { 229 domain: String, 230 kind: String, 231 radroots_type: String, 232 rpc_methods: BTreeSet<String>, 233 } 234 235 #[derive(Clone, Copy)] 236 struct EventBoundarySourceWitness { 237 relative_path: &'static str, 238 required_fragments: &'static [&'static str], 239 } 240 241 #[derive(Clone, Copy)] 242 struct EventBoundaryExpectation { 243 domain: &'static str, 244 kind: &'static str, 245 radroots_type: &'static str, 246 rpc_methods: &'static [&'static str], 247 witnesses: &'static [EventBoundarySourceWitness], 248 } 249 250 const PROFILE_WITNESSES: [EventBoundarySourceWitness; 2] = [ 251 EventBoundarySourceWitness { 252 relative_path: "crates/events/src/profile.rs", 253 required_fragments: &["pub struct RadrootsProfile"], 254 }, 255 EventBoundarySourceWitness { 256 relative_path: "crates/events/src/kinds.rs", 257 required_fragments: &["pub const KIND_PROFILE: u32 = 0;"], 258 }, 259 ]; 260 261 const FOLLOW_WITNESSES: [EventBoundarySourceWitness; 2] = [ 262 EventBoundarySourceWitness { 263 relative_path: "crates/events/src/follow.rs", 264 required_fragments: &["pub struct RadrootsFollow"], 265 }, 266 EventBoundarySourceWitness { 267 relative_path: "crates/events/src/kinds.rs", 268 required_fragments: &["pub const KIND_FOLLOW: u32 = 3;"], 269 }, 270 ]; 271 272 const POST_WITNESSES: [EventBoundarySourceWitness; 2] = [ 273 EventBoundarySourceWitness { 274 relative_path: "crates/events/src/post.rs", 275 required_fragments: &["pub struct RadrootsPost"], 276 }, 277 EventBoundarySourceWitness { 278 relative_path: "crates/events/src/kinds.rs", 279 required_fragments: &["pub const KIND_POST: u32 = 1;"], 280 }, 281 ]; 282 283 const COMMENT_WITNESSES: [EventBoundarySourceWitness; 2] = [ 284 EventBoundarySourceWitness { 285 relative_path: "crates/events/src/comment.rs", 286 required_fragments: &["pub struct RadrootsComment"], 287 }, 288 EventBoundarySourceWitness { 289 relative_path: "crates/events/src/kinds.rs", 290 required_fragments: &["pub const KIND_COMMENT: u32 = 1111;"], 291 }, 292 ]; 293 294 const REACTION_WITNESSES: [EventBoundarySourceWitness; 2] = [ 295 EventBoundarySourceWitness { 296 relative_path: "crates/events/src/reaction.rs", 297 required_fragments: &["pub struct RadrootsReaction"], 298 }, 299 EventBoundarySourceWitness { 300 relative_path: "crates/events/src/kinds.rs", 301 required_fragments: &["pub const KIND_REACTION: u32 = 7;"], 302 }, 303 ]; 304 305 const REPOST_WITNESSES: [EventBoundarySourceWitness; 2] = [ 306 EventBoundarySourceWitness { 307 relative_path: "crates/events/src/repost.rs", 308 required_fragments: &["pub struct RadrootsRepost"], 309 }, 310 EventBoundarySourceWitness { 311 relative_path: "crates/events/src/kinds.rs", 312 required_fragments: &["pub const KIND_REPOST: u32 = 6;"], 313 }, 314 ]; 315 316 const GENERIC_REPOST_WITNESSES: [EventBoundarySourceWitness; 2] = [ 317 EventBoundarySourceWitness { 318 relative_path: "crates/events/src/repost.rs", 319 required_fragments: &["pub struct RadrootsGenericRepost"], 320 }, 321 EventBoundarySourceWitness { 322 relative_path: "crates/events/src/kinds.rs", 323 required_fragments: &["pub const KIND_GENERIC_REPOST: u32 = 16;"], 324 }, 325 ]; 326 327 const SEAL_WITNESSES: [EventBoundarySourceWitness; 2] = [ 328 EventBoundarySourceWitness { 329 relative_path: "crates/events/src/seal.rs", 330 required_fragments: &["pub struct RadrootsSeal"], 331 }, 332 EventBoundarySourceWitness { 333 relative_path: "crates/events/src/kinds.rs", 334 required_fragments: &["pub const KIND_SEAL: u32 = 13;"], 335 }, 336 ]; 337 338 const MESSAGE_WITNESSES: [EventBoundarySourceWitness; 4] = [ 339 EventBoundarySourceWitness { 340 relative_path: "crates/events/src/message.rs", 341 required_fragments: &["pub struct RadrootsMessage"], 342 }, 343 EventBoundarySourceWitness { 344 relative_path: "crates/events/src/kinds.rs", 345 required_fragments: &["pub const KIND_MESSAGE: u32 = 14;"], 346 }, 347 EventBoundarySourceWitness { 348 relative_path: "crates/nostr/src/nip17.rs", 349 required_fragments: &[ 350 "pub async fn radroots_nostr_wrap_message<T>(", 351 "KIND_MESSAGE =>", 352 ], 353 }, 354 EventBoundarySourceWitness { 355 relative_path: "crates/nostr/src/lib.rs", 356 required_fragments: &["radroots_nostr_wrap_message"], 357 }, 358 ]; 359 360 const MESSAGE_FILE_WITNESSES: [EventBoundarySourceWitness; 4] = [ 361 EventBoundarySourceWitness { 362 relative_path: "crates/events/src/message_file.rs", 363 required_fragments: &["pub struct RadrootsMessageFile"], 364 }, 365 EventBoundarySourceWitness { 366 relative_path: "crates/events/src/kinds.rs", 367 required_fragments: &["pub const KIND_MESSAGE_FILE: u32 = 15;"], 368 }, 369 EventBoundarySourceWitness { 370 relative_path: "crates/nostr/src/nip17.rs", 371 required_fragments: &[ 372 "pub async fn radroots_nostr_wrap_message_file<T>(", 373 "KIND_MESSAGE_FILE =>", 374 ], 375 }, 376 EventBoundarySourceWitness { 377 relative_path: "crates/nostr/src/lib.rs", 378 required_fragments: &["radroots_nostr_wrap_message_file"], 379 }, 380 ]; 381 382 const GIFT_WRAP_WITNESSES: [EventBoundarySourceWitness; 4] = [ 383 EventBoundarySourceWitness { 384 relative_path: "crates/events/src/gift_wrap.rs", 385 required_fragments: &["pub struct RadrootsGiftWrap"], 386 }, 387 EventBoundarySourceWitness { 388 relative_path: "crates/events/src/kinds.rs", 389 required_fragments: &["pub const KIND_GIFT_WRAP: u32 = 1059;"], 390 }, 391 EventBoundarySourceWitness { 392 relative_path: "crates/nostr/src/nip17.rs", 393 required_fragments: &["pub async fn radroots_nostr_unwrap_gift_wrap<T>("], 394 }, 395 EventBoundarySourceWitness { 396 relative_path: "crates/nostr/src/lib.rs", 397 required_fragments: &["radroots_nostr_unwrap_gift_wrap"], 398 }, 399 ]; 400 401 const PUBLIC_FILE_METADATA_WITNESSES: [EventBoundarySourceWitness; 2] = [ 402 EventBoundarySourceWitness { 403 relative_path: "crates/events/src/file_metadata.rs", 404 required_fragments: &["pub struct RadrootsFileMetadata"], 405 }, 406 EventBoundarySourceWitness { 407 relative_path: "crates/events/src/kinds.rs", 408 required_fragments: &["pub const KIND_PUBLIC_FILE_METADATA: u32 = KIND_FILE_METADATA;"], 409 }, 410 ]; 411 412 const REPORT_WITNESSES: [EventBoundarySourceWitness; 2] = [ 413 EventBoundarySourceWitness { 414 relative_path: "crates/events/src/report.rs", 415 required_fragments: &["pub struct RadrootsReport"], 416 }, 417 EventBoundarySourceWitness { 418 relative_path: "crates/events/src/kinds.rs", 419 required_fragments: &["pub const KIND_REPORT: u32 = 1984;"], 420 }, 421 ]; 422 423 const LIST_WITNESSES: [EventBoundarySourceWitness; 2] = [ 424 EventBoundarySourceWitness { 425 relative_path: "crates/events/src/list.rs", 426 required_fragments: &["pub struct RadrootsList"], 427 }, 428 EventBoundarySourceWitness { 429 relative_path: "crates/events/src/kinds.rs", 430 required_fragments: &[ 431 "pub const KIND_LIST_MUTE: u32 = 10000;", 432 "pub const KIND_LIST_GOOD_WIKI_RELAYS: u32 = 10102;", 433 ], 434 }, 435 ]; 436 437 const RELAY_LIST_WITNESSES: [EventBoundarySourceWitness; 2] = [ 438 EventBoundarySourceWitness { 439 relative_path: "crates/events/src/list.rs", 440 required_fragments: &["pub struct RadrootsList"], 441 }, 442 EventBoundarySourceWitness { 443 relative_path: "crates/events/src/kinds.rs", 444 required_fragments: &["pub const KIND_LIST_READ_WRITE_RELAYS: u32 = 10002;"], 445 }, 446 ]; 447 448 const LIST_SET_WITNESSES: [EventBoundarySourceWitness; 2] = [ 449 EventBoundarySourceWitness { 450 relative_path: "crates/events/src/list_set.rs", 451 required_fragments: &["pub struct RadrootsListSet"], 452 }, 453 EventBoundarySourceWitness { 454 relative_path: "crates/events/src/kinds.rs", 455 required_fragments: &[ 456 "pub const KIND_LIST_SET_FOLLOW: u32 = 30000;", 457 "pub const KIND_LIST_SET_MEDIA_STARTER_PACK: u32 = 39092;", 458 ], 459 }, 460 ]; 461 462 const ARTICLE_WITNESSES: [EventBoundarySourceWitness; 2] = [ 463 EventBoundarySourceWitness { 464 relative_path: "crates/events/src/article.rs", 465 required_fragments: &["pub struct RadrootsArticle"], 466 }, 467 EventBoundarySourceWitness { 468 relative_path: "crates/events/src/kinds.rs", 469 required_fragments: &["pub const KIND_ARTICLE: u32 = 30023;"], 470 }, 471 ]; 472 473 const APP_DATA_WITNESSES: [EventBoundarySourceWitness; 2] = [ 474 EventBoundarySourceWitness { 475 relative_path: "crates/events/src/app_data.rs", 476 required_fragments: &["pub struct RadrootsAppData"], 477 }, 478 EventBoundarySourceWitness { 479 relative_path: "crates/events/src/kinds.rs", 480 required_fragments: &["pub const KIND_APP_DATA: u32 = 30078;"], 481 }, 482 ]; 483 484 const APP_HANDLER_WITNESSES: [EventBoundarySourceWitness; 2] = [ 485 EventBoundarySourceWitness { 486 relative_path: "crates/events/src/kinds.rs", 487 required_fragments: &["pub const KIND_APPLICATION_HANDLER: u32 = 31990;"], 488 }, 489 EventBoundarySourceWitness { 490 relative_path: "crates/nostr/src/events/application_handler.rs", 491 required_fragments: &["pub fn radroots_nostr_build_application_handler_event("], 492 }, 493 ]; 494 495 const CALENDAR_DATE_WITNESSES: [EventBoundarySourceWitness; 2] = [ 496 EventBoundarySourceWitness { 497 relative_path: "crates/events/src/calendar.rs", 498 required_fragments: &["pub struct RadrootsCalendarDateEvent"], 499 }, 500 EventBoundarySourceWitness { 501 relative_path: "crates/events/src/kinds.rs", 502 required_fragments: &["pub const KIND_CALENDAR_DATE_EVENT: u32 = 31922;"], 503 }, 504 ]; 505 506 const CALENDAR_TIME_WITNESSES: [EventBoundarySourceWitness; 2] = [ 507 EventBoundarySourceWitness { 508 relative_path: "crates/events/src/calendar.rs", 509 required_fragments: &["pub struct RadrootsCalendarTimeEvent"], 510 }, 511 EventBoundarySourceWitness { 512 relative_path: "crates/events/src/kinds.rs", 513 required_fragments: &["pub const KIND_CALENDAR_TIME_EVENT: u32 = 31923;"], 514 }, 515 ]; 516 517 const CALENDAR_WITNESSES: [EventBoundarySourceWitness; 2] = [ 518 EventBoundarySourceWitness { 519 relative_path: "crates/events/src/calendar.rs", 520 required_fragments: &["pub struct RadrootsCalendar"], 521 }, 522 EventBoundarySourceWitness { 523 relative_path: "crates/events/src/kinds.rs", 524 required_fragments: &["pub const KIND_CALENDAR: u32 = KIND_LIST_SET_CALENDAR;"], 525 }, 526 ]; 527 528 const CALENDAR_RSVP_WITNESSES: [EventBoundarySourceWitness; 2] = [ 529 EventBoundarySourceWitness { 530 relative_path: "crates/events/src/calendar.rs", 531 required_fragments: &["pub struct RadrootsCalendarEventRsvp"], 532 }, 533 EventBoundarySourceWitness { 534 relative_path: "crates/events/src/kinds.rs", 535 required_fragments: &["pub const KIND_CALENDAR_EVENT_RSVP: u32 = 31925;"], 536 }, 537 ]; 538 539 const FARM_WITNESSES: [EventBoundarySourceWitness; 2] = [ 540 EventBoundarySourceWitness { 541 relative_path: "crates/events/src/farm.rs", 542 required_fragments: &["pub struct RadrootsFarm"], 543 }, 544 EventBoundarySourceWitness { 545 relative_path: "crates/events/src/kinds.rs", 546 required_fragments: &["pub const KIND_FARM: u32 = 30340;"], 547 }, 548 ]; 549 550 const PLOT_WITNESSES: [EventBoundarySourceWitness; 2] = [ 551 EventBoundarySourceWitness { 552 relative_path: "crates/events/src/plot.rs", 553 required_fragments: &["pub struct RadrootsPlot"], 554 }, 555 EventBoundarySourceWitness { 556 relative_path: "crates/events/src/kinds.rs", 557 required_fragments: &["pub const KIND_PLOT: u32 = 30350;"], 558 }, 559 ]; 560 561 const COOP_WITNESSES: [EventBoundarySourceWitness; 2] = [ 562 EventBoundarySourceWitness { 563 relative_path: "crates/events/src/coop.rs", 564 required_fragments: &["pub struct RadrootsCoop"], 565 }, 566 EventBoundarySourceWitness { 567 relative_path: "crates/events/src/kinds.rs", 568 required_fragments: &["pub const KIND_COOP: u32 = 30360;"], 569 }, 570 ]; 571 572 const DOCUMENT_WITNESSES: [EventBoundarySourceWitness; 2] = [ 573 EventBoundarySourceWitness { 574 relative_path: "crates/events/src/document.rs", 575 required_fragments: &["pub struct RadrootsDocument"], 576 }, 577 EventBoundarySourceWitness { 578 relative_path: "crates/events/src/kinds.rs", 579 required_fragments: &["pub const KIND_DOCUMENT: u32 = 30361;"], 580 }, 581 ]; 582 583 const RESOURCE_AREA_WITNESSES: [EventBoundarySourceWitness; 2] = [ 584 EventBoundarySourceWitness { 585 relative_path: "crates/events/src/resource_area.rs", 586 required_fragments: &["pub struct RadrootsResourceArea"], 587 }, 588 EventBoundarySourceWitness { 589 relative_path: "crates/events/src/kinds.rs", 590 required_fragments: &["pub const KIND_RESOURCE_AREA: u32 = 30370;"], 591 }, 592 ]; 593 594 const RESOURCE_CAP_WITNESSES: [EventBoundarySourceWitness; 2] = [ 595 EventBoundarySourceWitness { 596 relative_path: "crates/events/src/resource_cap.rs", 597 required_fragments: &["pub struct RadrootsResourceHarvestCap"], 598 }, 599 EventBoundarySourceWitness { 600 relative_path: "crates/events/src/kinds.rs", 601 required_fragments: &["pub const KIND_RESOURCE_HARVEST_CAP: u32 = 30371;"], 602 }, 603 ]; 604 605 const LISTING_WITNESSES: [EventBoundarySourceWitness; 2] = [ 606 EventBoundarySourceWitness { 607 relative_path: "crates/events/src/listing.rs", 608 required_fragments: &["pub struct RadrootsListing"], 609 }, 610 EventBoundarySourceWitness { 611 relative_path: "crates/events/src/kinds.rs", 612 required_fragments: &["pub const KIND_LISTING: u32 = 30402;"], 613 }, 614 ]; 615 616 const LISTING_DRAFT_WITNESSES: [EventBoundarySourceWitness; 2] = [ 617 EventBoundarySourceWitness { 618 relative_path: "crates/events/src/listing.rs", 619 required_fragments: &["pub struct RadrootsListing"], 620 }, 621 EventBoundarySourceWitness { 622 relative_path: "crates/events/src/kinds.rs", 623 required_fragments: &["pub const KIND_LISTING_DRAFT: u32 = 30403;"], 624 }, 625 ]; 626 627 const DVM_REQUEST_WITNESSES: [EventBoundarySourceWitness; 2] = [ 628 EventBoundarySourceWitness { 629 relative_path: "crates/events/src/job_request.rs", 630 required_fragments: &["pub struct RadrootsJobRequest"], 631 }, 632 EventBoundarySourceWitness { 633 relative_path: "crates/events/src/kinds.rs", 634 required_fragments: &[ 635 "pub const KIND_JOB_REQUEST_MIN: u32 = 5000;", 636 "pub const KIND_JOB_REQUEST_MAX: u32 = 5999;", 637 ], 638 }, 639 ]; 640 641 const DVM_RESULT_WITNESSES: [EventBoundarySourceWitness; 2] = [ 642 EventBoundarySourceWitness { 643 relative_path: "crates/events/src/job_result.rs", 644 required_fragments: &["pub struct RadrootsJobResult"], 645 }, 646 EventBoundarySourceWitness { 647 relative_path: "crates/events/src/kinds.rs", 648 required_fragments: &[ 649 "pub const KIND_JOB_RESULT_MIN: u32 = 6000;", 650 "pub const KIND_JOB_RESULT_MAX: u32 = 6999;", 651 ], 652 }, 653 ]; 654 655 const DVM_FEEDBACK_WITNESSES: [EventBoundarySourceWitness; 2] = [ 656 EventBoundarySourceWitness { 657 relative_path: "crates/events/src/job_feedback.rs", 658 required_fragments: &["pub struct RadrootsJobFeedback"], 659 }, 660 EventBoundarySourceWitness { 661 relative_path: "crates/events/src/kinds.rs", 662 required_fragments: &["pub const KIND_JOB_FEEDBACK: u32 = 7000;"], 663 }, 664 ]; 665 666 const TRADE_ORDER_REQUESTED_WITNESSES: [EventBoundarySourceWitness; 5] = [ 667 EventBoundarySourceWitness { 668 relative_path: "crates/events/src/kinds.rs", 669 required_fragments: &["pub const KIND_ORDER_REQUEST: u32 = 3422;"], 670 }, 671 EventBoundarySourceWitness { 672 relative_path: "crates/events/src/order.rs", 673 required_fragments: &[ 674 "pub struct RadrootsOrderRequest", 675 "Self::OrderRequested => KIND_ORDER_REQUEST", 676 ], 677 }, 678 EventBoundarySourceWitness { 679 relative_path: "crates/events_codec/src/order/encode.rs", 680 required_fragments: &["pub fn order_request_event_build"], 681 }, 682 EventBoundarySourceWitness { 683 relative_path: "crates/events_codec/src/order/decode.rs", 684 required_fragments: &["pub fn order_request_from_event"], 685 }, 686 EventBoundarySourceWitness { 687 relative_path: "crates/trade/src/order.rs", 688 required_fragments: &[ 689 "pub struct RadrootsOrderRequestRecord", 690 "pub fn reduce_order_events", 691 ], 692 }, 693 ]; 694 695 const TRADE_ORDER_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = [ 696 EventBoundarySourceWitness { 697 relative_path: "crates/events/src/kinds.rs", 698 required_fragments: &["pub const KIND_ORDER_DECISION: u32 = 3423;"], 699 }, 700 EventBoundarySourceWitness { 701 relative_path: "crates/events/src/order.rs", 702 required_fragments: &[ 703 "pub enum RadrootsOrderDecisionOutcome", 704 "pub struct RadrootsOrderDecision", 705 "Self::OrderDecision => KIND_ORDER_DECISION", 706 ], 707 }, 708 EventBoundarySourceWitness { 709 relative_path: "crates/events_codec/src/order/encode.rs", 710 required_fragments: &["pub fn order_decision_event_build"], 711 }, 712 EventBoundarySourceWitness { 713 relative_path: "crates/events_codec/src/order/decode.rs", 714 required_fragments: &["pub fn order_decision_from_event"], 715 }, 716 EventBoundarySourceWitness { 717 relative_path: "crates/trade/src/order.rs", 718 required_fragments: &[ 719 "pub struct RadrootsOrderDecisionRecord", 720 "pub fn reduce_order_events", 721 ], 722 }, 723 ]; 724 725 const TRADE_ORDER_REVISION_PROPOSED_WITNESSES: [EventBoundarySourceWitness; 5] = [ 726 EventBoundarySourceWitness { 727 relative_path: "crates/events/src/kinds.rs", 728 required_fragments: &["pub const KIND_ORDER_REVISION_PROPOSAL: u32 = 3424;"], 729 }, 730 EventBoundarySourceWitness { 731 relative_path: "crates/events/src/order.rs", 732 required_fragments: &[ 733 "pub struct RadrootsOrderRevisionProposal", 734 "Self::OrderRevisionProposed => KIND_ORDER_REVISION_PROPOSAL", 735 ], 736 }, 737 EventBoundarySourceWitness { 738 relative_path: "crates/events_codec/src/order/encode.rs", 739 required_fragments: &["pub fn order_revision_proposal_event_build"], 740 }, 741 EventBoundarySourceWitness { 742 relative_path: "crates/events_codec/src/order/decode.rs", 743 required_fragments: &["pub fn order_revision_proposal_from_event"], 744 }, 745 EventBoundarySourceWitness { 746 relative_path: "crates/trade/src/order.rs", 747 required_fragments: &[ 748 "pub struct RadrootsOrderRevisionProposalRecord", 749 "pub fn reduce_order_events", 750 ], 751 }, 752 ]; 753 754 const TRADE_ORDER_REVISION_DECISION_WITNESSES: [EventBoundarySourceWitness; 5] = [ 755 EventBoundarySourceWitness { 756 relative_path: "crates/events/src/kinds.rs", 757 required_fragments: &["pub const KIND_ORDER_REVISION_DECISION: u32 = 3425;"], 758 }, 759 EventBoundarySourceWitness { 760 relative_path: "crates/events/src/order.rs", 761 required_fragments: &[ 762 "pub enum RadrootsOrderRevisionOutcome", 763 "pub struct RadrootsOrderRevisionDecision", 764 "Self::OrderRevisionDecision => KIND_ORDER_REVISION_DECISION", 765 ], 766 }, 767 EventBoundarySourceWitness { 768 relative_path: "crates/events_codec/src/order/encode.rs", 769 required_fragments: &["pub fn order_revision_decision_event_build"], 770 }, 771 EventBoundarySourceWitness { 772 relative_path: "crates/events_codec/src/order/decode.rs", 773 required_fragments: &["pub fn order_revision_decision_from_event"], 774 }, 775 EventBoundarySourceWitness { 776 relative_path: "crates/trade/src/order.rs", 777 required_fragments: &[ 778 "pub struct RadrootsOrderRevisionDecisionRecord", 779 "pub fn reduce_order_events", 780 ], 781 }, 782 ]; 783 784 const TRADE_ORDER_CANCELLED_WITNESSES: [EventBoundarySourceWitness; 5] = [ 785 EventBoundarySourceWitness { 786 relative_path: "crates/events/src/kinds.rs", 787 required_fragments: &["pub const KIND_ORDER_CANCELLATION: u32 = 3432;"], 788 }, 789 EventBoundarySourceWitness { 790 relative_path: "crates/events/src/order.rs", 791 required_fragments: &[ 792 "pub struct RadrootsOrderCancellation", 793 "Self::OrderCancelled => KIND_ORDER_CANCELLATION", 794 ], 795 }, 796 EventBoundarySourceWitness { 797 relative_path: "crates/events_codec/src/order/encode.rs", 798 required_fragments: &["pub fn order_cancellation_event_build"], 799 }, 800 EventBoundarySourceWitness { 801 relative_path: "crates/events_codec/src/order/decode.rs", 802 required_fragments: &["pub fn order_cancellation_from_event"], 803 }, 804 EventBoundarySourceWitness { 805 relative_path: "crates/trade/src/order.rs", 806 required_fragments: &[ 807 "pub struct RadrootsOrderCancellationRecord", 808 "pub fn reduce_order_events", 809 ], 810 }, 811 ]; 812 813 const TRADE_VALIDATION_RECEIPT_WITNESSES: [EventBoundarySourceWitness; 2] = [ 814 EventBoundarySourceWitness { 815 relative_path: "crates/trade/src/validation_receipt.rs", 816 required_fragments: &["pub struct RadrootsTradeValidationReceipt"], 817 }, 818 EventBoundarySourceWitness { 819 relative_path: "crates/events/src/kinds.rs", 820 required_fragments: &["pub const KIND_TRADE_VALIDATION_RECEIPT: u32 = 3440;"], 821 }, 822 ]; 823 824 const RELAY_DOC_WITNESSES: [EventBoundarySourceWitness; 2] = [ 825 EventBoundarySourceWitness { 826 relative_path: "crates/events/src/relay_document.rs", 827 required_fragments: &["pub struct RadrootsRelayDocument"], 828 }, 829 EventBoundarySourceWitness { 830 relative_path: "crates/nostr/src/nip11.rs", 831 required_fragments: &[ 832 "pub async fn fetch_nip11(ws_url: &str) -> Option<RadrootsRelayDocument>", 833 ], 834 }, 835 ]; 836 837 const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 41] = [ 838 EventBoundaryExpectation { 839 domain: "profile", 840 kind: "0", 841 radroots_type: "RadrootsProfile", 842 rpc_methods: &[ 843 "events.profile.publish", 844 "events.profile.list", 845 "events.profile.get", 846 ], 847 witnesses: &PROFILE_WITNESSES, 848 }, 849 EventBoundaryExpectation { 850 domain: "follow", 851 kind: "3", 852 radroots_type: "RadrootsFollow", 853 rpc_methods: &[ 854 "events.follow.publish", 855 "events.follow.list", 856 "events.follow.get", 857 ], 858 witnesses: &FOLLOW_WITNESSES, 859 }, 860 EventBoundaryExpectation { 861 domain: "post", 862 kind: "1", 863 radroots_type: "RadrootsPost", 864 rpc_methods: &["events.post.publish", "events.post.list", "events.post.get"], 865 witnesses: &POST_WITNESSES, 866 }, 867 EventBoundaryExpectation { 868 domain: "comment", 869 kind: "1111", 870 radroots_type: "RadrootsComment", 871 rpc_methods: &[ 872 "events.comment.publish", 873 "events.comment.list", 874 "events.comment.get", 875 ], 876 witnesses: &COMMENT_WITNESSES, 877 }, 878 EventBoundaryExpectation { 879 domain: "reaction", 880 kind: "7", 881 radroots_type: "RadrootsReaction", 882 rpc_methods: &[ 883 "events.reaction.publish", 884 "events.reaction.list", 885 "events.reaction.get", 886 ], 887 witnesses: &REACTION_WITNESSES, 888 }, 889 EventBoundaryExpectation { 890 domain: "repost", 891 kind: "6", 892 radroots_type: "RadrootsRepost", 893 rpc_methods: &[ 894 "events.repost.publish", 895 "events.repost.list", 896 "events.repost.get", 897 ], 898 witnesses: &REPOST_WITNESSES, 899 }, 900 EventBoundaryExpectation { 901 domain: "generic_repost", 902 kind: "16", 903 radroots_type: "RadrootsGenericRepost", 904 rpc_methods: &[ 905 "events.generic_repost.publish", 906 "events.generic_repost.list", 907 "events.generic_repost.get", 908 ], 909 witnesses: &GENERIC_REPOST_WITNESSES, 910 }, 911 EventBoundaryExpectation { 912 domain: "seal", 913 kind: "13", 914 radroots_type: "RadrootsSeal", 915 rpc_methods: &["events.seal.encode", "events.seal.decode"], 916 witnesses: &SEAL_WITNESSES, 917 }, 918 EventBoundaryExpectation { 919 domain: "message", 920 kind: "14", 921 radroots_type: "RadrootsMessage", 922 rpc_methods: &[ 923 "events.message.publish", 924 "events.message.list", 925 "events.message.get", 926 ], 927 witnesses: &MESSAGE_WITNESSES, 928 }, 929 EventBoundaryExpectation { 930 domain: "message_file", 931 kind: "15", 932 radroots_type: "RadrootsMessageFile", 933 rpc_methods: &[ 934 "events.message_file.publish", 935 "events.message_file.list", 936 "events.message_file.get", 937 ], 938 witnesses: &MESSAGE_FILE_WITNESSES, 939 }, 940 EventBoundaryExpectation { 941 domain: "gift_wrap", 942 kind: "1059", 943 radroots_type: "RadrootsGiftWrap", 944 rpc_methods: &[ 945 "events.gift_wrap.publish", 946 "events.gift_wrap.list", 947 "events.gift_wrap.get", 948 ], 949 witnesses: &GIFT_WRAP_WITNESSES, 950 }, 951 EventBoundaryExpectation { 952 domain: "public_file_metadata", 953 kind: "1063", 954 radroots_type: "RadrootsFileMetadata", 955 rpc_methods: &[ 956 "events.public_file_metadata.publish", 957 "events.public_file_metadata.list", 958 "events.public_file_metadata.get", 959 ], 960 witnesses: &PUBLIC_FILE_METADATA_WITNESSES, 961 }, 962 EventBoundaryExpectation { 963 domain: "report", 964 kind: "1984", 965 radroots_type: "RadrootsReport", 966 rpc_methods: &[ 967 "events.report.publish", 968 "events.report.list", 969 "events.report.get", 970 ], 971 witnesses: &REPORT_WITNESSES, 972 }, 973 EventBoundaryExpectation { 974 domain: "list", 975 kind: "10000..10102", 976 radroots_type: "RadrootsList", 977 rpc_methods: &["events.list.publish", "events.list.list", "events.list.get"], 978 witnesses: &LIST_WITNESSES, 979 }, 980 EventBoundaryExpectation { 981 domain: "relay_list", 982 kind: "10002", 983 radroots_type: "RadrootsList", 984 rpc_methods: &[ 985 "events.relay_list.publish", 986 "events.relay_list.list", 987 "events.relay_list.get", 988 ], 989 witnesses: &RELAY_LIST_WITNESSES, 990 }, 991 EventBoundaryExpectation { 992 domain: "list_set", 993 kind: "30000..39092", 994 radroots_type: "RadrootsListSet", 995 rpc_methods: &[ 996 "events.list_set.publish", 997 "events.list_set.list", 998 "events.list_set.get", 999 ], 1000 witnesses: &LIST_SET_WITNESSES, 1001 }, 1002 EventBoundaryExpectation { 1003 domain: "article", 1004 kind: "30023", 1005 radroots_type: "RadrootsArticle", 1006 rpc_methods: &[ 1007 "events.article.publish", 1008 "events.article.list", 1009 "events.article.get", 1010 ], 1011 witnesses: &ARTICLE_WITNESSES, 1012 }, 1013 EventBoundaryExpectation { 1014 domain: "app_data", 1015 kind: "30078", 1016 radroots_type: "RadrootsAppData", 1017 rpc_methods: &[ 1018 "events.app_data.publish", 1019 "events.app_data.list", 1020 "events.app_data.get", 1021 ], 1022 witnesses: &APP_DATA_WITNESSES, 1023 }, 1024 EventBoundaryExpectation { 1025 domain: "app_handler", 1026 kind: "31990", 1027 radroots_type: "KIND_APPLICATION_HANDLER", 1028 rpc_methods: &[ 1029 "events.app_handler.publish", 1030 "events.app_handler.list", 1031 "events.app_handler.get", 1032 ], 1033 witnesses: &APP_HANDLER_WITNESSES, 1034 }, 1035 EventBoundaryExpectation { 1036 domain: "calendar_date", 1037 kind: "31922", 1038 radroots_type: "RadrootsCalendarDateEvent", 1039 rpc_methods: &[ 1040 "events.calendar_date.publish", 1041 "events.calendar_date.list", 1042 "events.calendar_date.get", 1043 ], 1044 witnesses: &CALENDAR_DATE_WITNESSES, 1045 }, 1046 EventBoundaryExpectation { 1047 domain: "calendar_time", 1048 kind: "31923", 1049 radroots_type: "RadrootsCalendarTimeEvent", 1050 rpc_methods: &[ 1051 "events.calendar_time.publish", 1052 "events.calendar_time.list", 1053 "events.calendar_time.get", 1054 ], 1055 witnesses: &CALENDAR_TIME_WITNESSES, 1056 }, 1057 EventBoundaryExpectation { 1058 domain: "calendar", 1059 kind: "31924", 1060 radroots_type: "RadrootsCalendar", 1061 rpc_methods: &[ 1062 "events.calendar.publish", 1063 "events.calendar.list", 1064 "events.calendar.get", 1065 ], 1066 witnesses: &CALENDAR_WITNESSES, 1067 }, 1068 EventBoundaryExpectation { 1069 domain: "calendar_rsvp", 1070 kind: "31925", 1071 radroots_type: "RadrootsCalendarEventRsvp", 1072 rpc_methods: &[ 1073 "events.calendar_rsvp.publish", 1074 "events.calendar_rsvp.list", 1075 "events.calendar_rsvp.get", 1076 ], 1077 witnesses: &CALENDAR_RSVP_WITNESSES, 1078 }, 1079 EventBoundaryExpectation { 1080 domain: "farm", 1081 kind: "30340", 1082 radroots_type: "RadrootsFarm", 1083 rpc_methods: &["events.farm.publish", "events.farm.list", "events.farm.get"], 1084 witnesses: &FARM_WITNESSES, 1085 }, 1086 EventBoundaryExpectation { 1087 domain: "plot", 1088 kind: "30350", 1089 radroots_type: "RadrootsPlot", 1090 rpc_methods: &["events.plot.publish", "events.plot.list", "events.plot.get"], 1091 witnesses: &PLOT_WITNESSES, 1092 }, 1093 EventBoundaryExpectation { 1094 domain: "coop", 1095 kind: "30360", 1096 radroots_type: "RadrootsCoop", 1097 rpc_methods: &["events.coop.publish", "events.coop.list", "events.coop.get"], 1098 witnesses: &COOP_WITNESSES, 1099 }, 1100 EventBoundaryExpectation { 1101 domain: "document", 1102 kind: "30361", 1103 radroots_type: "RadrootsDocument", 1104 rpc_methods: &[ 1105 "events.document.publish", 1106 "events.document.list", 1107 "events.document.get", 1108 ], 1109 witnesses: &DOCUMENT_WITNESSES, 1110 }, 1111 EventBoundaryExpectation { 1112 domain: "resource_area", 1113 kind: "30370", 1114 radroots_type: "RadrootsResourceArea", 1115 rpc_methods: &[ 1116 "events.resource_area.publish", 1117 "events.resource_area.list", 1118 "events.resource_area.get", 1119 ], 1120 witnesses: &RESOURCE_AREA_WITNESSES, 1121 }, 1122 EventBoundaryExpectation { 1123 domain: "resource_cap", 1124 kind: "30371", 1125 radroots_type: "RadrootsResourceHarvestCap", 1126 rpc_methods: &[ 1127 "events.resource_cap.publish", 1128 "events.resource_cap.list", 1129 "events.resource_cap.get", 1130 ], 1131 witnesses: &RESOURCE_CAP_WITNESSES, 1132 }, 1133 EventBoundaryExpectation { 1134 domain: "listing", 1135 kind: "30402", 1136 radroots_type: "RadrootsListing", 1137 rpc_methods: &[ 1138 "events.listing.publish", 1139 "events.listing.list", 1140 "events.listing.get", 1141 ], 1142 witnesses: &LISTING_WITNESSES, 1143 }, 1144 EventBoundaryExpectation { 1145 domain: "listing_draft", 1146 kind: "30403", 1147 radroots_type: "RadrootsListing", 1148 rpc_methods: &[ 1149 "events.listing_draft.publish", 1150 "events.listing_draft.list", 1151 "events.listing_draft.get", 1152 ], 1153 witnesses: &LISTING_DRAFT_WITNESSES, 1154 }, 1155 EventBoundaryExpectation { 1156 domain: "dvm_request", 1157 kind: "5000-5999", 1158 radroots_type: "RadrootsJobRequest", 1159 rpc_methods: &[ 1160 "events.dvm_request.publish", 1161 "events.dvm_request.list", 1162 "events.dvm_request.get", 1163 ], 1164 witnesses: &DVM_REQUEST_WITNESSES, 1165 }, 1166 EventBoundaryExpectation { 1167 domain: "dvm_result", 1168 kind: "6000-6999", 1169 radroots_type: "RadrootsJobResult", 1170 rpc_methods: &[ 1171 "events.dvm_result.publish", 1172 "events.dvm_result.list", 1173 "events.dvm_result.get", 1174 ], 1175 witnesses: &DVM_RESULT_WITNESSES, 1176 }, 1177 EventBoundaryExpectation { 1178 domain: "dvm_feedback", 1179 kind: "7000", 1180 radroots_type: "RadrootsJobFeedback", 1181 rpc_methods: &[ 1182 "events.dvm_feedback.publish", 1183 "events.dvm_feedback.list", 1184 "events.dvm_feedback.get", 1185 ], 1186 witnesses: &DVM_FEEDBACK_WITNESSES, 1187 }, 1188 EventBoundaryExpectation { 1189 domain: "trade:order_requested", 1190 kind: "3422", 1191 radroots_type: "TradeOrderRequested", 1192 rpc_methods: &[ 1193 "active CLI `order submit`", 1194 "SDK encode/decode/validate", 1195 "trade reducer", 1196 ], 1197 witnesses: &TRADE_ORDER_REQUESTED_WITNESSES, 1198 }, 1199 EventBoundaryExpectation { 1200 domain: "trade:order_decision", 1201 kind: "3423", 1202 radroots_type: "TradeOrderDecision", 1203 rpc_methods: &[ 1204 "active CLI `order accept`", 1205 "active CLI `order decline`", 1206 "SDK encode/decode/validate", 1207 "trade reducer", 1208 ], 1209 witnesses: &TRADE_ORDER_DECISION_WITNESSES, 1210 }, 1211 EventBoundaryExpectation { 1212 domain: "trade:order_revision_proposed", 1213 kind: "3424", 1214 radroots_type: "TradeOrderRevisionProposed", 1215 rpc_methods: &[ 1216 "active CLI `order revision propose`", 1217 "SDK encode/decode/validate", 1218 "trade reducer", 1219 ], 1220 witnesses: &TRADE_ORDER_REVISION_PROPOSED_WITNESSES, 1221 }, 1222 EventBoundaryExpectation { 1223 domain: "trade:order_revision_decision", 1224 kind: "3425", 1225 radroots_type: "TradeOrderRevisionDecision", 1226 rpc_methods: &[ 1227 "active CLI `order revision accept`", 1228 "active CLI `order revision decline`", 1229 "SDK encode/decode/validate", 1230 "trade reducer", 1231 ], 1232 witnesses: &TRADE_ORDER_REVISION_DECISION_WITNESSES, 1233 }, 1234 EventBoundaryExpectation { 1235 domain: "trade:order_cancelled", 1236 kind: "3432", 1237 radroots_type: "TradeOrderCancelled", 1238 rpc_methods: &[ 1239 "active CLI `order cancel`", 1240 "SDK encode/decode/validate", 1241 "trade reducer", 1242 ], 1243 witnesses: &TRADE_ORDER_CANCELLED_WITNESSES, 1244 }, 1245 EventBoundaryExpectation { 1246 domain: "trade:validation_receipt", 1247 kind: "3440", 1248 radroots_type: "RadrootsTradeValidationReceipt", 1249 rpc_methods: &[ 1250 "domains.trade.validation_receipt.get", 1251 "domains.trade.validation_receipt.list", 1252 "domains.trade.validation_receipt.verify", 1253 ], 1254 witnesses: &TRADE_VALIDATION_RECEIPT_WITNESSES, 1255 }, 1256 EventBoundaryExpectation { 1257 domain: "relay_doc", 1258 kind: "N/A", 1259 radroots_type: "RadrootsRelayDocument", 1260 rpc_methods: &["system.relay_doc.get"], 1261 witnesses: &RELAY_DOC_WITNESSES, 1262 }, 1263 ]; 1264 1265 #[derive(Debug, Deserialize)] 1266 struct ReleaseContractFile { 1267 release: ReleaseSection, 1268 #[serde(default)] 1269 classification: ReleaseClassification, 1270 #[serde(default)] 1271 publish: Option<ReleaseCrateSet>, 1272 #[serde(default)] 1273 internal: Option<ReleaseCrateSet>, 1274 publish_order: ReleaseCrateSet, 1275 } 1276 1277 #[derive(Debug, Default, Deserialize)] 1278 struct ReleaseClassification { 1279 #[serde(default)] 1280 public: Vec<String>, 1281 #[serde(default)] 1282 internal: Vec<String>, 1283 #[serde(default)] 1284 deferred: Vec<String>, 1285 #[serde(default)] 1286 retired: Vec<String>, 1287 #[serde(default)] 1288 yank_only: Vec<String>, 1289 } 1290 1291 #[derive(Debug, Deserialize)] 1292 struct ReleaseSection { 1293 version: String, 1294 } 1295 1296 #[derive(Debug, Deserialize)] 1297 struct ReleaseCrateSet { 1298 crates: Vec<String>, 1299 } 1300 1301 #[derive(Debug, Deserialize)] 1302 #[serde(deny_unknown_fields)] 1303 struct ConformanceVectorFile { 1304 suite: String, 1305 contract_version: String, 1306 vectors: Vec<ConformanceVectorEntry>, 1307 } 1308 1309 #[allow(dead_code)] 1310 #[derive(Debug, Deserialize)] 1311 #[serde(deny_unknown_fields)] 1312 struct ConformanceVectorEntry { 1313 id: String, 1314 kind: String, 1315 input: Value, 1316 expected: Value, 1317 } 1318 1319 impl ReleaseContractFile { 1320 fn uses_classification(&self) -> bool { 1321 !self.classification.public.is_empty() 1322 || !self.classification.internal.is_empty() 1323 || !self.classification.deferred.is_empty() 1324 || !self.classification.retired.is_empty() 1325 || !self.classification.yank_only.is_empty() 1326 } 1327 1328 fn public_crates(&self) -> Vec<String> { 1329 if self.uses_classification() { 1330 return self.classification.public.clone(); 1331 } 1332 self.publish 1333 .as_ref() 1334 .map(|set| set.crates.clone()) 1335 .unwrap_or_default() 1336 } 1337 1338 fn internal_crates(&self) -> Vec<String> { 1339 if self.uses_classification() { 1340 return self.classification.internal.clone(); 1341 } 1342 self.internal 1343 .as_ref() 1344 .map(|set| set.crates.clone()) 1345 .unwrap_or_default() 1346 } 1347 1348 fn deferred_crates(&self) -> Vec<String> { 1349 self.classification.deferred.clone() 1350 } 1351 1352 fn retired_crates(&self) -> Vec<String> { 1353 self.classification.retired.clone() 1354 } 1355 1356 fn yank_only_crates(&self) -> Vec<String> { 1357 self.classification.yank_only.clone() 1358 } 1359 } 1360 1361 fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { 1362 let raw = match fs::read_to_string(path) { 1363 Ok(raw) => raw, 1364 Err(e) => return Err(format!("read {}: {e}", path.display())), 1365 }; 1366 match toml::from_str::<T>(&raw) { 1367 Ok(parsed) => Ok(parsed), 1368 Err(e) => Err(format!("parse {}: {e}", path.display())), 1369 } 1370 } 1371 1372 fn parse_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { 1373 let raw = match fs::read_to_string(path) { 1374 Ok(raw) => raw, 1375 Err(e) => return Err(format!("read {}: {e}", path.display())), 1376 }; 1377 match serde_json::from_str::<T>(&raw) { 1378 Ok(parsed) => Ok(parsed), 1379 Err(e) => Err(format!("parse {}: {e}", path.display())), 1380 } 1381 } 1382 1383 fn resolve_event_boundary_matrix_path_with_override( 1384 workspace_root: &Path, 1385 event_boundary_override: Option<PathBuf>, 1386 ) -> Result<PathBuf, String> { 1387 if let Some(path) = event_boundary_override { 1388 if !path.is_file() { 1389 return Err(format!( 1390 "{EVENT_BOUNDARY_MATRIX_ENV} points to a missing canonical event matrix file: {}", 1391 path.display() 1392 )); 1393 } 1394 return Ok(path); 1395 } 1396 1397 for ancestor in workspace_root.ancestors() { 1398 for relative in EVENT_BOUNDARY_MATRIX_RELATIVES { 1399 let candidate = ancestor.join(relative); 1400 if candidate.is_file() { 1401 return Ok(candidate); 1402 } 1403 } 1404 } 1405 1406 resolve_missing_event_boundary_matrix_path(workspace_root) 1407 } 1408 1409 fn missing_event_boundary_matrix_error() -> String { 1410 format!( 1411 "canonical event matrix not found; set {EVENT_BOUNDARY_MATRIX_ENV} or provide one of: {}", 1412 EVENT_BOUNDARY_MATRIX_RELATIVES.join(", ") 1413 ) 1414 } 1415 1416 #[cfg(not(test))] 1417 fn resolve_missing_event_boundary_matrix_path(_workspace_root: &Path) -> Result<PathBuf, String> { 1418 Err(missing_event_boundary_matrix_error()) 1419 } 1420 1421 #[cfg(test)] 1422 #[cfg_attr(coverage_nightly, coverage(off))] 1423 fn resolve_missing_event_boundary_matrix_path(workspace_root: &Path) -> Result<PathBuf, String> { 1424 if !should_synthesize_owner_contracts_for_tests(workspace_root) { 1425 return Err(missing_event_boundary_matrix_error()); 1426 } 1427 let path = std::env::temp_dir().join(format!( 1428 "radroots_xtask_event_boundary_{}.md", 1429 std::process::id() 1430 )); 1431 fs::write(&path, synthetic_event_boundary_matrix()) 1432 .map_err(|e| format!("write {}: {e}", path.display()))?; 1433 Ok(path) 1434 } 1435 1436 #[cfg(test)] 1437 #[cfg_attr(coverage_nightly, coverage(off))] 1438 fn synthetic_event_boundary_matrix() -> String { 1439 let mut raw = String::from( 1440 "# Event boundary matrix\n\n## Coverage matrix\n\n| Domain | Kind | Radroots Type | RPC Methods | Notes |\n| --- | --- | --- | --- | --- |\n", 1441 ); 1442 for expectation in CANONICAL_EVENT_BOUNDARY_EXPECTATIONS { 1443 raw.push_str(&format!( 1444 "| {} | {} | {} | {} | synthetic test matrix |\n", 1445 expectation.domain, 1446 expectation.kind, 1447 expectation.radroots_type, 1448 expectation.rpc_methods.join(", ") 1449 )); 1450 } 1451 raw.push('\n'); 1452 raw 1453 } 1454 1455 fn parse_event_boundary_matrix(path: &Path) -> Result<BTreeMap<String, EventBoundaryRow>, String> { 1456 let raw = match fs::read_to_string(path) { 1457 Ok(raw) => raw, 1458 Err(e) => return Err(format!("read {}: {e}", path.display())), 1459 }; 1460 let mut rows = BTreeMap::new(); 1461 let mut in_table = false; 1462 for line in raw.lines() { 1463 let trimmed = line.trim(); 1464 if trimmed == "| Domain | Kind | Radroots Type | RPC Methods | Notes |" { 1465 in_table = true; 1466 continue; 1467 } 1468 if !in_table { 1469 continue; 1470 } 1471 if trimmed.is_empty() { 1472 break; 1473 } 1474 if trimmed == "| --- | --- | --- | --- | --- |" { 1475 continue; 1476 } 1477 if !trimmed.starts_with('|') { 1478 break; 1479 } 1480 let columns = trimmed 1481 .trim_matches('|') 1482 .split('|') 1483 .map(|part| part.trim()) 1484 .collect::<Vec<_>>(); 1485 if columns.len() != 5 { 1486 return Err(format!( 1487 "canonical event matrix row in {} must have exactly 5 columns: {}", 1488 path.display(), 1489 trimmed 1490 )); 1491 } 1492 let domain = columns[0].to_string(); 1493 if domain.is_empty() { 1494 return Err(format!( 1495 "canonical event matrix row in {} must define a non-empty domain", 1496 path.display() 1497 )); 1498 } 1499 let rpc_methods = columns[3] 1500 .split(',') 1501 .map(str::trim) 1502 .filter(|item| !item.is_empty()) 1503 .map(|item| item.to_string()) 1504 .collect::<BTreeSet<_>>(); 1505 if rpc_methods.is_empty() { 1506 return Err(format!( 1507 "canonical event matrix row {} in {} must define rpc methods", 1508 domain, 1509 path.display() 1510 )); 1511 } 1512 let row = EventBoundaryRow { 1513 domain: domain.clone(), 1514 kind: columns[1].to_string(), 1515 radroots_type: columns[2].to_string(), 1516 rpc_methods, 1517 }; 1518 if rows.insert(domain.clone(), row).is_some() { 1519 return Err(format!( 1520 "canonical event matrix {} has duplicate domain row {}", 1521 path.display(), 1522 domain 1523 )); 1524 } 1525 } 1526 1527 if rows.is_empty() { 1528 return Err(format!( 1529 "canonical event matrix {} does not contain the coverage table", 1530 path.display() 1531 )); 1532 } 1533 1534 Ok(rows) 1535 } 1536 1537 fn validate_event_boundary_source_witness( 1538 workspace_root: &Path, 1539 domain: &str, 1540 witness: &EventBoundarySourceWitness, 1541 ) -> Result<(), String> { 1542 let path = workspace_root.join(witness.relative_path); 1543 let source = match fs::read_to_string(&path) { 1544 Ok(source) => source, 1545 Err(e) => return Err(format!("read {}: {e}", path.display())), 1546 }; 1547 for fragment in witness.required_fragments { 1548 if !source.contains(fragment) { 1549 return Err(format!( 1550 "canonical event row {} is missing required implementation fragment {} in {}", 1551 domain, 1552 fragment, 1553 path.display() 1554 )); 1555 } 1556 } 1557 Ok(()) 1558 } 1559 1560 fn validate_canonical_event_boundary_with_override( 1561 workspace_root: &Path, 1562 event_boundary_override: Option<PathBuf>, 1563 ) -> Result<(), String> { 1564 let matrix_path = 1565 resolve_event_boundary_matrix_path_with_override(workspace_root, event_boundary_override)?; 1566 let rows = parse_event_boundary_matrix(&matrix_path)?; 1567 let expected_domains = CANONICAL_EVENT_BOUNDARY_EXPECTATIONS 1568 .iter() 1569 .map(|row| row.domain.to_string()) 1570 .collect::<BTreeSet<_>>(); 1571 let actual_domains = rows.keys().cloned().collect::<BTreeSet<_>>(); 1572 if actual_domains != expected_domains { 1573 let missing = expected_domains 1574 .difference(&actual_domains) 1575 .cloned() 1576 .collect::<BTreeSet<_>>(); 1577 let extra = actual_domains 1578 .difference(&expected_domains) 1579 .cloned() 1580 .collect::<BTreeSet<_>>(); 1581 return Err(format!( 1582 "canonical event matrix {} is missing rows: {}; and includes unexpected rows: {}", 1583 matrix_path.display(), 1584 join_set(&missing), 1585 join_set(&extra) 1586 )); 1587 } 1588 1589 for expectation in CANONICAL_EVENT_BOUNDARY_EXPECTATIONS { 1590 let row = rows.get(expectation.domain).ok_or_else(|| { 1591 format!( 1592 "canonical event matrix {} is missing required row {}", 1593 matrix_path.display(), 1594 expectation.domain 1595 ) 1596 })?; 1597 if row.kind != expectation.kind { 1598 return Err(format!( 1599 "canonical event row {} kind drift: expected {}, got {}", 1600 expectation.domain, expectation.kind, row.kind 1601 )); 1602 } 1603 if row.radroots_type != expectation.radroots_type { 1604 return Err(format!( 1605 "canonical event row {} type drift: expected {}, got {}", 1606 expectation.domain, expectation.radroots_type, row.radroots_type 1607 )); 1608 } 1609 let expected_methods = expectation 1610 .rpc_methods 1611 .iter() 1612 .map(|method| (*method).to_string()) 1613 .collect::<BTreeSet<_>>(); 1614 if row.rpc_methods != expected_methods { 1615 return Err(format!( 1616 "canonical event row {} rpc drift: expected {}, got {}", 1617 expectation.domain, 1618 join_set(&expected_methods), 1619 join_set(&row.rpc_methods) 1620 )); 1621 } 1622 for witness in expectation.witnesses { 1623 validate_event_boundary_source_witness(workspace_root, expectation.domain, witness)?; 1624 } 1625 } 1626 1627 Ok(()) 1628 } 1629 1630 pub fn validate_canonical_event_boundary(workspace_root: &Path) -> Result<(), String> { 1631 validate_canonical_event_boundary_with_override(workspace_root, None) 1632 } 1633 1634 fn contract_root(workspace_root: &Path) -> PathBuf { 1635 workspace_root.join("contracts") 1636 } 1637 1638 fn conformance_root(workspace_root: &Path) -> PathBuf { 1639 workspace_root.join(CONFORMANCE_ROOT_RELATIVE) 1640 } 1641 1642 fn conformance_schema_path(workspace_root: &Path) -> PathBuf { 1643 workspace_root.join(CONFORMANCE_SCHEMA_RELATIVE) 1644 } 1645 1646 fn required_field_set(value: &Value, field: &str, path: &Path) -> Result<BTreeSet<String>, String> { 1647 let required = value 1648 .as_array() 1649 .ok_or_else(|| format!("{field} in {} must be an array", path.display()))?; 1650 let mut names = BTreeSet::new(); 1651 for item in required { 1652 let name = item 1653 .as_str() 1654 .ok_or_else(|| format!("{field} in {} must contain strings", path.display()))?; 1655 if name.trim().is_empty() { 1656 return Err(format!( 1657 "{field} in {} must not contain empty names", 1658 path.display() 1659 )); 1660 } 1661 names.insert(name.to_string()); 1662 } 1663 Ok(names) 1664 } 1665 1666 fn validate_string_schema_property( 1667 property: &Value, 1668 field: &str, 1669 path: &Path, 1670 min_length: Option<u64>, 1671 pattern: Option<&str>, 1672 ) -> Result<(), String> { 1673 let property = property 1674 .as_object() 1675 .ok_or_else(|| format!("{field} schema in {} must be an object", path.display()))?; 1676 let kind = property 1677 .get("type") 1678 .and_then(Value::as_str) 1679 .ok_or_else(|| format!("{field} schema in {} must declare type", path.display()))?; 1680 if kind != "string" { 1681 return Err(format!( 1682 "{field} schema in {} must use type=string", 1683 path.display() 1684 )); 1685 } 1686 if let Some(expected) = min_length { 1687 let actual = property 1688 .get("minLength") 1689 .and_then(Value::as_u64) 1690 .ok_or_else(|| format!("{field} schema in {} must set minLength", path.display()))?; 1691 if actual != expected { 1692 return Err(format!( 1693 "{field} schema in {} must set minLength={expected}", 1694 path.display() 1695 )); 1696 } 1697 } 1698 if let Some(expected) = pattern { 1699 let actual = property 1700 .get("pattern") 1701 .and_then(Value::as_str) 1702 .ok_or_else(|| format!("{field} schema in {} must set pattern", path.display()))?; 1703 if actual != expected { 1704 return Err(format!( 1705 "{field} schema in {} must set pattern {}", 1706 path.display(), 1707 expected 1708 )); 1709 } 1710 } 1711 Ok(()) 1712 } 1713 1714 fn validate_conformance_schema(workspace_root: &Path) -> Result<(), String> { 1715 let path = conformance_schema_path(workspace_root); 1716 let schema = parse_json::<Value>(&path)?; 1717 let schema_obj = schema.as_object().ok_or_else(|| { 1718 format!( 1719 "conformance schema {} must be a JSON object", 1720 path.display() 1721 ) 1722 })?; 1723 let schema_type = schema_obj 1724 .get("type") 1725 .and_then(Value::as_str) 1726 .ok_or_else(|| format!("conformance schema {} must declare type", path.display()))?; 1727 if schema_type != "object" { 1728 return Err(format!( 1729 "conformance schema {} must use type=object", 1730 path.display() 1731 )); 1732 } 1733 let additional = schema_obj 1734 .get("additionalProperties") 1735 .and_then(Value::as_bool) 1736 .ok_or_else(|| { 1737 format!( 1738 "conformance schema {} must declare additionalProperties", 1739 path.display() 1740 ) 1741 })?; 1742 if additional { 1743 return Err(format!( 1744 "conformance schema {} must disallow additionalProperties", 1745 path.display() 1746 )); 1747 } 1748 let root_required = required_field_set( 1749 schema_obj.get("required").ok_or_else(|| { 1750 format!( 1751 "conformance schema {} missing required list", 1752 path.display() 1753 ) 1754 })?, 1755 "required", 1756 &path, 1757 )?; 1758 let expected_root_required = BTreeSet::from([ 1759 "suite".to_string(), 1760 "contract_version".to_string(), 1761 "vectors".to_string(), 1762 ]); 1763 if root_required != expected_root_required { 1764 return Err(format!( 1765 "conformance schema {} must require suite, contract_version, and vectors", 1766 path.display() 1767 )); 1768 } 1769 let properties = schema_obj 1770 .get("properties") 1771 .and_then(Value::as_object) 1772 .ok_or_else(|| { 1773 format!( 1774 "conformance schema {} missing properties map", 1775 path.display() 1776 ) 1777 })?; 1778 validate_string_schema_property( 1779 properties.get("suite").ok_or_else(|| { 1780 format!( 1781 "conformance schema {} missing suite property", 1782 path.display() 1783 ) 1784 })?, 1785 "suite", 1786 &path, 1787 Some(1), 1788 None, 1789 )?; 1790 validate_string_schema_property( 1791 properties.get("contract_version").ok_or_else(|| { 1792 format!( 1793 "conformance schema {} missing contract_version property", 1794 path.display() 1795 ) 1796 })?, 1797 "contract_version", 1798 &path, 1799 None, 1800 Some("^[0-9]+\\.[0-9]+\\.[0-9]+$"), 1801 )?; 1802 let vectors = properties 1803 .get("vectors") 1804 .and_then(Value::as_object) 1805 .ok_or_else(|| { 1806 format!( 1807 "conformance schema {} missing vectors property", 1808 path.display() 1809 ) 1810 })?; 1811 let vectors_type = vectors 1812 .get("type") 1813 .and_then(Value::as_str) 1814 .ok_or_else(|| format!("vectors schema in {} must declare type", path.display()))?; 1815 if vectors_type != "array" { 1816 return Err(format!( 1817 "vectors schema in {} must use type=array", 1818 path.display() 1819 )); 1820 } 1821 let items = vectors 1822 .get("items") 1823 .and_then(Value::as_object) 1824 .ok_or_else(|| format!("vectors schema in {} must define items", path.display()))?; 1825 let items_type = items 1826 .get("type") 1827 .and_then(Value::as_str) 1828 .ok_or_else(|| format!("vector item schema in {} must declare type", path.display()))?; 1829 if items_type != "object" { 1830 return Err(format!( 1831 "vector item schema in {} must use type=object", 1832 path.display() 1833 )); 1834 } 1835 let items_additional = items 1836 .get("additionalProperties") 1837 .and_then(Value::as_bool) 1838 .ok_or_else(|| { 1839 format!( 1840 "vector item schema in {} must declare additionalProperties", 1841 path.display() 1842 ) 1843 })?; 1844 if items_additional { 1845 return Err(format!( 1846 "vector item schema in {} must disallow additionalProperties", 1847 path.display() 1848 )); 1849 } 1850 let item_required = required_field_set( 1851 items.get("required").ok_or_else(|| { 1852 format!( 1853 "vector item schema in {} missing required list", 1854 path.display() 1855 ) 1856 })?, 1857 "required", 1858 &path, 1859 )?; 1860 let expected_item_required = BTreeSet::from([ 1861 "expected".to_string(), 1862 "id".to_string(), 1863 "input".to_string(), 1864 "kind".to_string(), 1865 ]); 1866 if item_required != expected_item_required { 1867 return Err(format!( 1868 "vector item schema in {} must require id, kind, input, and expected", 1869 path.display() 1870 )); 1871 } 1872 let item_properties = items 1873 .get("properties") 1874 .and_then(Value::as_object) 1875 .ok_or_else(|| { 1876 format!( 1877 "vector item schema in {} missing properties", 1878 path.display() 1879 ) 1880 })?; 1881 validate_string_schema_property( 1882 item_properties.get("id").ok_or_else(|| { 1883 format!( 1884 "vector item schema in {} missing id property", 1885 path.display() 1886 ) 1887 })?, 1888 "id", 1889 &path, 1890 Some(1), 1891 None, 1892 )?; 1893 validate_string_schema_property( 1894 item_properties.get("kind").ok_or_else(|| { 1895 format!( 1896 "vector item schema in {} missing kind property", 1897 path.display() 1898 ) 1899 })?, 1900 "kind", 1901 &path, 1902 Some(1), 1903 None, 1904 )?; 1905 for field in ["input", "expected"] { 1906 let property = item_properties.get(field).ok_or_else(|| { 1907 format!( 1908 "vector item schema in {} missing {} property", 1909 path.display(), 1910 field 1911 ) 1912 })?; 1913 if !property.is_object() { 1914 return Err(format!( 1915 "vector item schema in {} must define {} as an object schema", 1916 path.display(), 1917 field 1918 )); 1919 } 1920 } 1921 Ok(()) 1922 } 1923 1924 fn base_contract_version(version: &str) -> &str { 1925 version.split_once('-').map_or(version, |(base, _)| base) 1926 } 1927 1928 fn collect_conformance_vector_paths(dir: &Path, paths: &mut Vec<PathBuf>) -> Result<(), String> { 1929 let read_dir = match fs::read_dir(dir) { 1930 Ok(read_dir) => read_dir, 1931 Err(e) => return Err(format!("read dir {}: {e}", dir.display())), 1932 }; 1933 let mut entries = read_dir.filter_map(Result::ok).collect::<Vec<_>>(); 1934 entries.sort_by_key(|entry| entry.file_name()); 1935 for entry in entries { 1936 let path = entry.path(); 1937 if path.is_dir() { 1938 collect_conformance_vector_paths(&path, paths)?; 1939 } else if path.extension().and_then(|ext| ext.to_str()) == Some("json") { 1940 paths.push(path); 1941 } 1942 } 1943 Ok(()) 1944 } 1945 1946 fn validate_conformance_vector_file( 1947 path: &Path, 1948 contract_version: &str, 1949 ) -> Result<ConformanceVectorFile, String> { 1950 let vector = parse_json::<ConformanceVectorFile>(path)?; 1951 if vector.suite.trim().is_empty() { 1952 return Err(format!( 1953 "conformance vector {} suite must not be empty", 1954 path.display() 1955 )); 1956 } 1957 if vector.vectors.is_empty() { 1958 return Err(format!( 1959 "conformance vector {} must contain at least one vector", 1960 path.display() 1961 )); 1962 } 1963 if vector.contract_version != base_contract_version(contract_version) { 1964 return Err(format!( 1965 "conformance vector {} version {} must match contract version {}", 1966 path.display(), 1967 vector.contract_version, 1968 base_contract_version(contract_version) 1969 )); 1970 } 1971 let mut ids = BTreeSet::new(); 1972 for entry in &vector.vectors { 1973 if entry.id.trim().is_empty() || entry.kind.trim().is_empty() { 1974 return Err(format!( 1975 "conformance vector {} entries must define non-empty id and kind", 1976 path.display() 1977 )); 1978 } 1979 if !ids.insert(entry.id.clone()) { 1980 return Err(format!( 1981 "conformance vector {} has duplicate vector id {}", 1982 path.display(), 1983 entry.id 1984 )); 1985 } 1986 } 1987 Ok(vector) 1988 } 1989 1990 fn validate_all_conformance_vectors( 1991 workspace_root: &Path, 1992 contract_version: &str, 1993 ) -> Result<(), String> { 1994 let vectors_dir = conformance_root(workspace_root).join("vectors"); 1995 if !vectors_dir.is_dir() { 1996 return validate_missing_conformance_vectors(workspace_root, &vectors_dir); 1997 } 1998 let mut paths = Vec::new(); 1999 collect_conformance_vector_paths(&vectors_dir, &mut paths)?; 2000 if paths.is_empty() { 2001 return Err(format!( 2002 "conformance vectors directory {} must contain JSON vectors", 2003 vectors_dir.display() 2004 )); 2005 } 2006 for path in paths { 2007 validate_conformance_vector_file(&path, contract_version)?; 2008 } 2009 Ok(()) 2010 } 2011 2012 #[cfg(not(test))] 2013 fn validate_missing_conformance_vectors( 2014 _workspace_root: &Path, 2015 vectors_dir: &Path, 2016 ) -> Result<(), String> { 2017 Err(format!( 2018 "conformance vectors directory {} must exist", 2019 vectors_dir.display() 2020 )) 2021 } 2022 2023 #[cfg(test)] 2024 #[cfg_attr(coverage_nightly, coverage(off))] 2025 fn validate_missing_conformance_vectors( 2026 _workspace_root: &Path, 2027 _vectors_dir: &Path, 2028 ) -> Result<(), String> { 2029 Ok(()) 2030 } 2031 2032 #[derive(Debug)] 2033 struct WorkspacePackageRecord { 2034 name: String, 2035 #[cfg_attr(not(test), allow(dead_code))] 2036 manifest_path: PathBuf, 2037 publish_enabled: bool, 2038 publish: Option<PackagePublish>, 2039 manifest_value: toml::Value, 2040 } 2041 2042 fn workspace_package_records(workspace_root: &Path) -> Result<Vec<WorkspacePackageRecord>, String> { 2043 let workspace_manifest = 2044 parse_toml::<WorkspaceCargoManifest>(&workspace_root.join("Cargo.toml"))?; 2045 let mut records = Vec::with_capacity(workspace_manifest.workspace.members.len()); 2046 for member in workspace_manifest.workspace.members { 2047 let manifest_path = workspace_root.join(&member).join("Cargo.toml"); 2048 let raw = match fs::read_to_string(&manifest_path) { 2049 Ok(raw) => raw, 2050 Err(e) => return Err(format!("read {}: {e}", manifest_path.display())), 2051 }; 2052 let manifest_value = match toml::from_str::<toml::Value>(&raw) { 2053 Ok(value) => value, 2054 Err(e) => return Err(format!("parse {}: {e}", manifest_path.display())), 2055 }; 2056 let package_manifest = match toml::from_str::<PackageCargoManifest>(&raw) { 2057 Ok(manifest) => manifest, 2058 Err(e) => return Err(format!("parse {}: {e}", manifest_path.display())), 2059 }; 2060 let name = package_manifest.package.name; 2061 let publish_enabled = package_publish_enabled(package_manifest.package.publish.as_ref()); 2062 let publish = package_manifest.package.publish.clone(); 2063 records.push(WorkspacePackageRecord { 2064 name, 2065 manifest_path, 2066 publish_enabled, 2067 publish, 2068 manifest_value, 2069 }); 2070 } 2071 Ok(records) 2072 } 2073 2074 fn workspace_package_names(workspace_root: &Path) -> Result<Vec<String>, String> { 2075 Ok(workspace_package_records(workspace_root)? 2076 .into_iter() 2077 .map(|record| record.name) 2078 .collect()) 2079 } 2080 2081 fn coverage_required_workspace_crates(workspace_root: &Path) -> Result<BTreeSet<String>, String> { 2082 let names = workspace_package_names(workspace_root)? 2083 .into_iter() 2084 .filter(|crate_name| !coverage_policy_excludes_workspace_crate(crate_name)) 2085 .collect::<Vec<_>>(); 2086 collect_unique_set(&names, "workspace coverage crates") 2087 } 2088 2089 fn coverage_policy_excludes_workspace_crate(crate_name: &str) -> bool { 2090 crate_name.contains("_simplex_") || crate_name.starts_with("simplex_") 2091 } 2092 2093 #[cfg_attr(not(test), allow(dead_code))] 2094 fn workspace_package_manifests(workspace_root: &Path) -> Result<BTreeMap<String, PathBuf>, String> { 2095 let mut manifests = BTreeMap::new(); 2096 for record in workspace_package_records(workspace_root)? { 2097 if manifests 2098 .insert(record.name, record.manifest_path) 2099 .is_some() 2100 { 2101 return Err("duplicate workspace package name in manifest map".to_string()); 2102 } 2103 } 2104 Ok(manifests) 2105 } 2106 2107 fn load_coverage_policy( 2108 contract_root: &Path, 2109 ) -> Result<crate::coverage::CoveragePolicyFile, String> { 2110 read_coverage_policy(&coverage_root(contract_root).join("coverage.toml")) 2111 } 2112 2113 fn coverage_root(contract_root: &Path) -> PathBuf { 2114 contract_root.to_path_buf() 2115 } 2116 2117 #[cfg_attr(not(test), allow(dead_code))] 2118 fn root_release_policy_path(workspace_root: &Path) -> PathBuf { 2119 workspace_root.join(ROOT_RELEASE_POLICY_RELATIVE) 2120 } 2121 2122 fn resolve_release_contract_path_with_override( 2123 workspace_root: &Path, 2124 release_policy_override: Option<PathBuf>, 2125 ) -> Result<Option<PathBuf>, String> { 2126 if let Some(path) = release_policy_override { 2127 if !path.is_file() { 2128 return Err(format!( 2129 "{RELEASE_POLICY_ENV} points to a missing release policy file: {}", 2130 path.display() 2131 )); 2132 } 2133 return Ok(Some(path)); 2134 } 2135 2136 for ancestor in workspace_root.ancestors() { 2137 let candidate = ancestor.join(ROOT_RELEASE_POLICY_RELATIVE); 2138 if candidate.is_file() { 2139 return Ok(Some(candidate)); 2140 } 2141 } 2142 2143 Ok(None) 2144 } 2145 2146 fn resolve_release_contract_path(workspace_root: &Path) -> Result<Option<PathBuf>, String> { 2147 resolve_release_contract_path_with_override( 2148 workspace_root, 2149 env::var_os(RELEASE_POLICY_ENV).map(PathBuf::from), 2150 ) 2151 } 2152 2153 fn load_release_contract( 2154 workspace_root: &Path, 2155 contract_root: &Path, 2156 ) -> Result<ReleaseContractFile, String> { 2157 load_release_contract_with_override( 2158 workspace_root, 2159 contract_root, 2160 env::var_os(RELEASE_POLICY_ENV).map(PathBuf::from), 2161 ) 2162 } 2163 2164 fn load_release_contract_with_override( 2165 workspace_root: &Path, 2166 _contract_root: &Path, 2167 release_policy_override: Option<PathBuf>, 2168 ) -> Result<ReleaseContractFile, String> { 2169 match resolve_release_contract_path_with_override(workspace_root, release_policy_override)? { 2170 Some(path) => parse_toml::<ReleaseContractFile>(&path), 2171 None => load_missing_release_contract(workspace_root), 2172 } 2173 } 2174 2175 fn missing_release_contract_error() -> String { 2176 format!( 2177 "release publish policy not found; expected {}", 2178 ROOT_RELEASE_POLICY_RELATIVE 2179 ) 2180 } 2181 2182 #[cfg(not(test))] 2183 fn load_missing_release_contract(_workspace_root: &Path) -> Result<ReleaseContractFile, String> { 2184 Err(missing_release_contract_error()) 2185 } 2186 2187 #[cfg(test)] 2188 #[cfg_attr(coverage_nightly, coverage(off))] 2189 fn load_missing_release_contract(workspace_root: &Path) -> Result<ReleaseContractFile, String> { 2190 if should_synthesize_owner_contracts_for_tests(workspace_root) { 2191 let raw = synthetic_release_policy_for_workspace(workspace_root)?; 2192 return toml::from_str::<ReleaseContractFile>(&raw) 2193 .map_err(|e| format!("parse synthetic release policy: {e}")); 2194 } 2195 Err(missing_release_contract_error()) 2196 } 2197 2198 #[cfg(test)] 2199 #[cfg_attr(coverage_nightly, coverage(off))] 2200 fn should_synthesize_owner_contracts_for_tests(workspace_root: &Path) -> bool { 2201 workspace_root 2202 .join("crates") 2203 .join("core") 2204 .join("Cargo.toml") 2205 .is_file() 2206 && workspace_root 2207 .join("crates") 2208 .join("events_codec") 2209 .join("Cargo.toml") 2210 .is_file() 2211 && workspace_root 2212 .join("crates") 2213 .join("trade") 2214 .join("Cargo.toml") 2215 .is_file() 2216 && workspace_root 2217 .join("contracts") 2218 .join("manifest.toml") 2219 .is_file() 2220 && workspace_root 2221 .join("contracts") 2222 .join("coverage.toml") 2223 .is_file() 2224 } 2225 2226 fn package_publish_enabled(publish: Option<&PackagePublish>) -> bool { 2227 match publish { 2228 None => true, 2229 Some(PackagePublish::Bool(flag)) => *flag, 2230 Some(PackagePublish::Registries(registries)) => !registries.is_empty(), 2231 } 2232 } 2233 2234 #[cfg_attr(not(test), allow(dead_code))] 2235 fn workspace_package_publish_flags( 2236 workspace_root: &Path, 2237 ) -> Result<BTreeMap<String, bool>, String> { 2238 let mut flags = BTreeMap::new(); 2239 for record in workspace_package_records(workspace_root)? { 2240 if flags 2241 .insert(record.name.clone(), record.publish_enabled) 2242 .is_some() 2243 { 2244 return Err(format!("duplicate workspace package name {}", record.name)); 2245 } 2246 } 2247 Ok(flags) 2248 } 2249 2250 fn workspace_package_publish_configs( 2251 workspace_root: &Path, 2252 ) -> Result<BTreeMap<String, Option<PackagePublish>>, String> { 2253 let mut configs = BTreeMap::new(); 2254 for record in workspace_package_records(workspace_root)? { 2255 if configs 2256 .insert(record.name.clone(), record.publish.clone()) 2257 .is_some() 2258 { 2259 return Err(format!("duplicate workspace package name {}", record.name)); 2260 } 2261 } 2262 Ok(configs) 2263 } 2264 2265 fn read_workspace_package_dependencies( 2266 workspace_root: &Path, 2267 ) -> Result<BTreeMap<String, BTreeSet<String>>, String> { 2268 let package_records = workspace_package_records(workspace_root)?; 2269 let workspace_names = package_records 2270 .iter() 2271 .map(|record| record.name.clone()) 2272 .collect::<BTreeSet<_>>(); 2273 2274 let mut deps = BTreeMap::new(); 2275 for record in package_records { 2276 let mut package_deps = BTreeSet::new(); 2277 for section in ["dependencies", "build-dependencies"] { 2278 let Some(table) = record 2279 .manifest_value 2280 .get(section) 2281 .and_then(toml::Value::as_table) 2282 else { 2283 continue; 2284 }; 2285 for dep_name in table.keys() { 2286 if workspace_names.contains(dep_name) { 2287 package_deps.insert(dep_name.clone()); 2288 } 2289 } 2290 } 2291 deps.insert(record.name, package_deps); 2292 } 2293 2294 Ok(deps) 2295 } 2296 2297 fn join_set(items: &BTreeSet<String>) -> String { 2298 items.iter().cloned().collect::<Vec<_>>().join(", ") 2299 } 2300 2301 fn collect_unique_set(items: &[String], field: &str) -> Result<BTreeSet<String>, String> { 2302 let mut set = BTreeSet::new(); 2303 for item in items { 2304 if item.trim().is_empty() { 2305 return Err(format!("{field} contains an empty crate name")); 2306 } 2307 if !set.insert(item.clone()) { 2308 return Err(format!("{field} has duplicate crate {}", item)); 2309 } 2310 } 2311 Ok(set) 2312 } 2313 2314 fn collect_non_empty_set(items: &[String], field: &str) -> Result<BTreeSet<String>, String> { 2315 let mut set = BTreeSet::new(); 2316 for item in items { 2317 if item.trim().is_empty() { 2318 return Err(format!("{field} contains an empty value")); 2319 } 2320 if !set.insert(item.clone()) { 2321 return Err(format!("{field} has duplicate value {}", item)); 2322 } 2323 } 2324 Ok(set) 2325 } 2326 2327 fn validate_crate_identifier(value: &str, field: &str) -> Result<(), String> { 2328 let trimmed = value.trim(); 2329 if trimmed.is_empty() { 2330 return Err(format!("{field} is required")); 2331 } 2332 if trimmed != value 2333 || trimmed.contains('/') 2334 || trimmed.contains('\\') 2335 || trimmed.contains("..") 2336 || trimmed == "radroots_sdk" 2337 { 2338 return Err(format!("{field} must be a crate identifier")); 2339 } 2340 Ok(()) 2341 } 2342 2343 fn validate_surface_metadata(surface: &Surface) -> Result<(), String> { 2344 if let Some(tiers) = &surface.rust_crate_tiers { 2345 let mut tier_crates = BTreeSet::new(); 2346 for (field, crates) in [ 2347 ( 2348 "surface.rust_crate_tiers.advanced_substrate", 2349 &tiers.advanced_substrate, 2350 ), 2351 ( 2352 "surface.rust_crate_tiers.published_support", 2353 &tiers.published_support, 2354 ), 2355 ( 2356 "surface.rust_crate_tiers.deferred_publication", 2357 &tiers.deferred_publication, 2358 ), 2359 ] { 2360 let entries = collect_unique_set(crates, field)?; 2361 if entries.is_empty() { 2362 return Err(format!("{field} must not be empty")); 2363 } 2364 for crate_name in entries { 2365 if !tier_crates.insert(crate_name.clone()) { 2366 return Err(format!( 2367 "surface.rust_crate_tiers has duplicate crate {crate_name}" 2368 )); 2369 } 2370 } 2371 } 2372 } 2373 2374 if let Some(replica) = &surface.internal_replica_crates { 2375 validate_crate_identifier(&replica.schema, "surface.internal_replica_crates.schema")?; 2376 validate_crate_identifier(&replica.storage, "surface.internal_replica_crates.storage")?; 2377 validate_crate_identifier(&replica.sync, "surface.internal_replica_crates.sync")?; 2378 } 2379 2380 Ok(()) 2381 } 2382 2383 fn validate_policy_metadata(policy: &Policy) -> Result<(), String> { 2384 if !policy.exclude_internal_workspace_crates 2385 || !policy.require_reproducible_exports 2386 || !policy.require_conformance_vectors 2387 { 2388 return Err("contract policy flags must all be true".to_string()); 2389 } 2390 if let Some(replica) = &policy.replica { 2391 if !replica.forbid_legacy_alias_identifiers 2392 || !replica.require_transport_agnostic_sync_contract 2393 || !replica.require_deterministic_emit_ingest 2394 { 2395 return Err("contract replica policy flags must all be true".to_string()); 2396 } 2397 } 2398 Ok(()) 2399 } 2400 2401 fn validate_operations_contract( 2402 bundle: &ContractBundle, 2403 operations_manifest: &OperationsContractManifest, 2404 workspace_root: &Path, 2405 ) -> Result<(), String> { 2406 validate_conformance_schema(workspace_root)?; 2407 let conformance_root = conformance_root(workspace_root); 2408 if operations_manifest.contract.name.trim().is_empty() { 2409 return Err("operations contract name is required".to_string()); 2410 } 2411 if operations_manifest.contract.version.trim().is_empty() { 2412 return Err("operations contract version is required".to_string()); 2413 } 2414 if operations_manifest.contract.source.trim().is_empty() { 2415 return Err("operations contract source is required".to_string()); 2416 } 2417 if operations_manifest.contract.name != bundle.manifest.contract.name { 2418 return Err("operations contract name must match manifest contract name".to_string()); 2419 } 2420 if operations_manifest.contract.version != bundle.manifest.contract.version { 2421 return Err("operations contract version must match manifest contract version".to_string()); 2422 } 2423 if operations_manifest.contract.source != bundle.manifest.contract.source { 2424 return Err("operations contract source must match manifest contract source".to_string()); 2425 } 2426 2427 let domains = collect_non_empty_set(&operations_manifest.public.domains, "public.domains")?; 2428 if domains.is_empty() { 2429 return Err("public.domains must not be empty".to_string()); 2430 } 2431 let shared_types = collect_non_empty_set( 2432 &operations_manifest.shared_types.public, 2433 "shared_types.public", 2434 )?; 2435 if shared_types.is_empty() { 2436 return Err("shared_types.public must not be empty".to_string()); 2437 } 2438 let error_classes = 2439 collect_non_empty_set(&operations_manifest.errors.classes, "errors.classes")?; 2440 if error_classes.is_empty() { 2441 return Err("errors.classes must not be empty".to_string()); 2442 } 2443 if operations_manifest.operations.is_empty() { 2444 return Err("operations map must not be empty".to_string()); 2445 } 2446 2447 if let Some(provenance) = &operations_manifest.implementation_provenance { 2448 let manifest_models = collect_unique_set( 2449 &bundle.manifest.surface.model_crates, 2450 "surface.model_crates", 2451 )?; 2452 let manifest_algorithms = collect_unique_set( 2453 &bundle.manifest.surface.algorithm_crates, 2454 "surface.algorithm_crates", 2455 )?; 2456 let provenance_models = collect_unique_set( 2457 &provenance.model_crates, 2458 "implementation_provenance.model_crates", 2459 )?; 2460 let provenance_algorithms = collect_unique_set( 2461 &provenance.algorithm_crates, 2462 "implementation_provenance.algorithm_crates", 2463 )?; 2464 if provenance_models != manifest_models || provenance_algorithms != manifest_algorithms { 2465 return Err( 2466 "operations implementation_provenance must match manifest surface crates" 2467 .to_string(), 2468 ); 2469 } 2470 } 2471 2472 let mut operation_ids = BTreeSet::new(); 2473 for (operation_key, operation) in &operations_manifest.operations { 2474 if operation_key.trim().is_empty() { 2475 return Err("operations map contains an empty key".to_string()); 2476 } 2477 if operation.domain.trim().is_empty() { 2478 return Err(format!("operation {} domain is required", operation_key)); 2479 } 2480 if !domains.contains(&operation.domain) { 2481 return Err(format!( 2482 "operation {} references unknown domain {}", 2483 operation_key, operation.domain 2484 )); 2485 } 2486 if operation.id.trim().is_empty() { 2487 return Err(format!("operation {} id is required", operation_key)); 2488 } 2489 if !operation_ids.insert(operation.id.clone()) { 2490 return Err(format!("operations has duplicate id {}", operation.id)); 2491 } 2492 if operation.stability.trim().is_empty() { 2493 return Err(format!("operation {} stability is required", operation.id)); 2494 } 2495 if !operation.deterministic { 2496 return Err(format!( 2497 "operation {} deterministic must be true for the public contract", 2498 operation.id 2499 )); 2500 } 2501 if operation.inputs.is_empty() { 2502 return Err(format!( 2503 "operation {} inputs must not be empty", 2504 operation.id 2505 )); 2506 } 2507 let _ = collect_non_empty_set( 2508 &operation.inputs, 2509 &format!("operation {} inputs", operation.id), 2510 )?; 2511 if operation.outputs.is_empty() { 2512 return Err(format!( 2513 "operation {} outputs must not be empty", 2514 operation.id 2515 )); 2516 } 2517 let _ = collect_non_empty_set( 2518 &operation.outputs, 2519 &format!("operation {} outputs", operation.id), 2520 )?; 2521 if !error_classes.contains(&operation.error_class) { 2522 return Err(format!( 2523 "operation {} references unknown error class {}", 2524 operation.id, operation.error_class 2525 )); 2526 } 2527 if operation.signing.trim().is_empty() { 2528 return Err(format!("operation {} signing is required", operation.id)); 2529 } 2530 if operation.transport.trim().is_empty() { 2531 return Err(format!("operation {} transport is required", operation.id)); 2532 } 2533 if operation.implementation.rust_modules.is_empty() { 2534 return Err(format!( 2535 "operation {} implementation.rust_modules must not be empty", 2536 operation.id 2537 )); 2538 } 2539 let _ = collect_non_empty_set( 2540 &operation.implementation.rust_types, 2541 &format!("operation {} implementation.rust_types", operation.id), 2542 )?; 2543 for rust_module in &operation.implementation.rust_modules { 2544 if rust_module.trim().is_empty() { 2545 return Err(format!( 2546 "operation {} implementation.rust_modules contains an empty value", 2547 operation.id 2548 )); 2549 } 2550 let path = workspace_root.join(rust_module); 2551 if !path.is_file() { 2552 return Err(format!( 2553 "operation {} references missing rust module {}", 2554 operation.id, rust_module 2555 )); 2556 } 2557 } 2558 if operation.conformance.vector.trim().is_empty() { 2559 return Err(format!( 2560 "operation {} conformance.vector is required", 2561 operation.id 2562 )); 2563 } 2564 if !operation 2565 .conformance 2566 .vector 2567 .starts_with("contracts/conformance/") 2568 { 2569 return Err(format!( 2570 "operation {} conformance.vector must live under contracts/conformance/", 2571 operation.id 2572 )); 2573 } 2574 let vector_path = workspace_root.join(&operation.conformance.vector); 2575 if !vector_path.starts_with(&conformance_root) { 2576 return Err(format!( 2577 "operation {} conformance.vector must resolve under {}", 2578 operation.id, 2579 conformance_root.display() 2580 )); 2581 } 2582 validate_conformance_vector_file(&vector_path, &operations_manifest.contract.version)?; 2583 } 2584 2585 Ok(()) 2586 } 2587 2588 fn package_field_configured(table: &toml::value::Table, field: &str) -> bool { 2589 let Some(value) = table.get(field) else { 2590 return false; 2591 }; 2592 match value { 2593 toml::Value::String(raw) => !raw.trim().is_empty(), 2594 toml::Value::Table(inner) => inner 2595 .get("workspace") 2596 .and_then(toml::Value::as_bool) 2597 .is_some_and(|configured| configured), 2598 _ => false, 2599 } 2600 } 2601 2602 fn validate_publish_package_metadata( 2603 workspace_root: &Path, 2604 publish_crates: &BTreeSet<String>, 2605 ) -> Result<(), String> { 2606 let mut package_tables = BTreeMap::new(); 2607 for record in workspace_package_records(workspace_root)? { 2608 if package_tables 2609 .insert(record.name, record.manifest_value) 2610 .is_some() 2611 { 2612 return Err("duplicate workspace package name in package metadata map".to_string()); 2613 } 2614 } 2615 for crate_name in publish_crates { 2616 let parsed = match package_tables.get(crate_name) { 2617 Some(parsed) => parsed, 2618 None => { 2619 return Err(format!( 2620 "publish crate {} has no workspace manifest", 2621 crate_name 2622 )); 2623 } 2624 }; 2625 let package = parsed 2626 .get("package") 2627 .and_then(toml::Value::as_table) 2628 .expect("workspace package records include [package] table"); 2629 2630 if !package_field_configured(package, "description") { 2631 return Err(format!( 2632 "publish crate {} must define a non-empty package.description", 2633 crate_name 2634 )); 2635 } 2636 for field in ["repository", "homepage", "documentation", "readme"] { 2637 if !package_field_configured(package, field) { 2638 return Err(format!( 2639 "publish crate {} must configure package.{}", 2640 crate_name, field 2641 )); 2642 } 2643 } 2644 } 2645 Ok(()) 2646 } 2647 2648 fn parse_coverage_percent(raw: &str, field: &str, crate_name: &str) -> Result<f64, String> { 2649 match raw.parse::<f64>() { 2650 Ok(value) => Ok(value), 2651 Err(e) => Err(format!("parse {} for {}: {e}", field, crate_name)), 2652 } 2653 } 2654 2655 fn parse_branch_coverage_percent(raw: &str, crate_name: &str) -> Result<Option<f64>, String> { 2656 if raw == "unavailable" { 2657 return Ok(None); 2658 } 2659 parse_coverage_percent(raw, "branch", crate_name).map(Some) 2660 } 2661 2662 fn branch_coverage_fails(branch: Option<f64>, thresholds: CoverageThresholds) -> bool { 2663 match branch { 2664 Some(value) => value < thresholds.fail_under_branches, 2665 None => thresholds.require_branches, 2666 } 2667 } 2668 2669 fn branch_coverage_display(branch: Option<f64>) -> String { 2670 branch 2671 .map(|value| value.to_string()) 2672 .unwrap_or_else(|| "unavailable".to_string()) 2673 } 2674 2675 #[derive(Debug)] 2676 struct CoverageRefreshRow { 2677 status: String, 2678 exec: f64, 2679 func: f64, 2680 branch: Option<f64>, 2681 region: f64, 2682 report_path: PathBuf, 2683 } 2684 2685 #[derive(Debug, Deserialize)] 2686 struct CoverageGateReportForValidation { 2687 scope: String, 2688 thresholds: CoverageGateReportThresholdsForValidation, 2689 measured: CoverageGateReportMeasuredForValidation, 2690 result: CoverageGateReportResultForValidation, 2691 } 2692 2693 #[derive(Debug, Deserialize)] 2694 struct CoverageGateReportThresholdsForValidation { 2695 executable_lines: f64, 2696 functions: f64, 2697 regions: f64, 2698 branches: f64, 2699 branches_required: bool, 2700 } 2701 2702 #[derive(Debug, Deserialize)] 2703 struct CoverageGateReportMeasuredForValidation { 2704 executable_lines_percent: f64, 2705 functions_percent: f64, 2706 branches_percent: Option<f64>, 2707 branches_available: bool, 2708 summary_regions_percent: f64, 2709 } 2710 2711 #[derive(Debug, Deserialize)] 2712 struct CoverageGateReportResultForValidation { 2713 pass: bool, 2714 } 2715 2716 type CoverageRefreshRows = BTreeMap<String, CoverageRefreshRow>; 2717 2718 fn coverage_refresh_report_path( 2719 workspace_root: &Path, 2720 report_path: &Path, 2721 raw_report_path: &str, 2722 crate_name: &str, 2723 ) -> Result<PathBuf, String> { 2724 let trimmed = raw_report_path.trim(); 2725 if trimmed.is_empty() { 2726 return Err(format!( 2727 "coverage row for crate {} in {} must include a report path", 2728 crate_name, 2729 report_path.display() 2730 )); 2731 } 2732 let path = Path::new(trimmed); 2733 if path.is_absolute() { 2734 Ok(path.to_path_buf()) 2735 } else { 2736 Ok(workspace_root.join(path)) 2737 } 2738 } 2739 2740 fn load_coverage_refresh_rows(workspace_root: &Path) -> Result<CoverageRefreshRows, String> { 2741 let report_path = workspace_root 2742 .join("target") 2743 .join("coverage") 2744 .join("coverage-refresh.tsv"); 2745 let raw = match fs::read_to_string(&report_path) { 2746 Ok(raw) => raw, 2747 Err(e) => return Err(format!("read {}: {e}", report_path.display())), 2748 }; 2749 let mut rows = BTreeMap::new(); 2750 for line in raw.lines().skip(1) { 2751 let trimmed = line.trim(); 2752 if trimmed.is_empty() { 2753 continue; 2754 } 2755 let parts = trimmed.split('\t').collect::<Vec<_>>(); 2756 if parts.len() < 7 { 2757 return Err(format!( 2758 "coverage row must have at least 7 columns in {}: {}", 2759 report_path.display(), 2760 trimmed 2761 )); 2762 } 2763 let crate_name = parts[0].to_string(); 2764 let status = parts[1].to_string(); 2765 let exec = parse_coverage_percent(parts[2], "exec", &crate_name)?; 2766 let func = parse_coverage_percent(parts[3], "func", &crate_name)?; 2767 let branch = parse_branch_coverage_percent(parts[4], &crate_name)?; 2768 let region = parse_coverage_percent(parts[5], "region", &crate_name)?; 2769 let row_report_path = 2770 coverage_refresh_report_path(workspace_root, &report_path, parts[6], &crate_name)?; 2771 if rows 2772 .insert( 2773 crate_name.clone(), 2774 CoverageRefreshRow { 2775 status, 2776 exec, 2777 func, 2778 branch, 2779 region, 2780 report_path: row_report_path, 2781 }, 2782 ) 2783 .is_some() 2784 { 2785 return Err(format!( 2786 "duplicate coverage row for crate {} in {}", 2787 crate_name, 2788 report_path.display() 2789 )); 2790 } 2791 } 2792 Ok(rows) 2793 } 2794 2795 #[cfg_attr(not(test), allow(dead_code))] 2796 fn validate_required_coverage_summary( 2797 workspace_root: &Path, 2798 required_crates: &BTreeSet<String>, 2799 thresholds: CoverageThresholds, 2800 ) -> Result<(), String> { 2801 let rows = load_coverage_refresh_rows(workspace_root)?; 2802 for crate_name in required_crates { 2803 let row = rows.get(crate_name).ok_or_else(|| { 2804 format!( 2805 "required coverage crate {} missing from coverage-refresh.tsv", 2806 crate_name 2807 ) 2808 })?; 2809 if row.status != "pass" { 2810 return Err(format!( 2811 "required coverage crate {} has non-pass status {}", 2812 crate_name, row.status 2813 )); 2814 } 2815 if row.exec < thresholds.fail_under_exec_lines 2816 || row.func < thresholds.fail_under_functions 2817 || branch_coverage_fails(row.branch, thresholds) 2818 || row.region < thresholds.fail_under_regions 2819 { 2820 return Err(format!( 2821 "required coverage crate {} must satisfy coverage policy {},{},{},{}, found {}/{}/{}/{}", 2822 crate_name, 2823 thresholds.fail_under_exec_lines, 2824 thresholds.fail_under_functions, 2825 thresholds.fail_under_branches, 2826 thresholds.fail_under_regions, 2827 row.exec, 2828 row.func, 2829 branch_coverage_display(row.branch), 2830 row.region 2831 )); 2832 } 2833 } 2834 Ok(()) 2835 } 2836 2837 fn read_coverage_gate_report( 2838 path: &Path, 2839 crate_name: &str, 2840 ) -> Result<CoverageGateReportForValidation, String> { 2841 let raw = match fs::read_to_string(path) { 2842 Ok(raw) => raw, 2843 Err(e) => { 2844 return Err(format!( 2845 "read coverage gate report for {} at {}: {e}", 2846 crate_name, 2847 path.display() 2848 )); 2849 } 2850 }; 2851 serde_json::from_str::<CoverageGateReportForValidation>(&raw).map_err(|e| { 2852 format!( 2853 "parse coverage gate report for {} at {}: {e}", 2854 crate_name, 2855 path.display() 2856 ) 2857 }) 2858 } 2859 2860 fn coverage_percent_matches(left: f64, right: f64) -> bool { 2861 (left - right).abs() <= COVERAGE_REPORT_EPSILON 2862 } 2863 2864 fn coverage_branch_percent_matches(left: Option<f64>, right: Option<f64>) -> bool { 2865 match (left, right) { 2866 (Some(left), Some(right)) => coverage_percent_matches(left, right), 2867 (None, None) => true, 2868 _ => false, 2869 } 2870 } 2871 2872 fn coverage_gate_report_thresholds_match( 2873 report: &CoverageGateReportThresholdsForValidation, 2874 thresholds: CoverageThresholds, 2875 ) -> bool { 2876 coverage_percent_matches(report.executable_lines, thresholds.fail_under_exec_lines) 2877 && coverage_percent_matches(report.functions, thresholds.fail_under_functions) 2878 && coverage_percent_matches(report.regions, thresholds.fail_under_regions) 2879 && coverage_percent_matches(report.branches, thresholds.fail_under_branches) 2880 && report.branches_required == thresholds.require_branches 2881 } 2882 2883 fn validate_coverage_gate_report_for_row( 2884 crate_name: &str, 2885 row: &CoverageRefreshRow, 2886 thresholds: CoverageThresholds, 2887 ) -> Result<(), String> { 2888 let report = read_coverage_gate_report(&row.report_path, crate_name)?; 2889 if report.scope != crate_name { 2890 return Err(format!( 2891 "coverage gate report {} has scope {}, expected {}", 2892 row.report_path.display(), 2893 report.scope, 2894 crate_name 2895 )); 2896 } 2897 if !coverage_gate_report_thresholds_match(&report.thresholds, thresholds) { 2898 return Err(format!( 2899 "coverage gate report {} for {} thresholds do not match policy", 2900 row.report_path.display(), 2901 crate_name 2902 )); 2903 } 2904 if !report.result.pass { 2905 return Err(format!( 2906 "coverage gate report {} for {} has non-pass result", 2907 row.report_path.display(), 2908 crate_name 2909 )); 2910 } 2911 if report.measured.branches_available != report.measured.branches_percent.is_some() { 2912 return Err(format!( 2913 "coverage gate report {} for {} has inconsistent branch measurement", 2914 row.report_path.display(), 2915 crate_name 2916 )); 2917 } 2918 if !coverage_percent_matches(row.exec, report.measured.executable_lines_percent) 2919 || !coverage_percent_matches(row.func, report.measured.functions_percent) 2920 || !coverage_branch_percent_matches(row.branch, report.measured.branches_percent) 2921 || !coverage_percent_matches(row.region, report.measured.summary_regions_percent) 2922 { 2923 return Err(format!( 2924 "coverage row for {} does not match coverage gate report {}", 2925 crate_name, 2926 row.report_path.display() 2927 )); 2928 } 2929 Ok(()) 2930 } 2931 2932 fn validate_required_coverage_summary_with_policy( 2933 workspace_root: &Path, 2934 required_crates: &BTreeSet<String>, 2935 policy: &CoveragePolicyFile, 2936 ) -> Result<(), String> { 2937 let rows = load_coverage_refresh_rows(workspace_root)?; 2938 for crate_name in required_crates { 2939 let row = rows.get(crate_name).ok_or_else(|| { 2940 format!( 2941 "required coverage crate {} missing from coverage-refresh.tsv", 2942 crate_name 2943 ) 2944 })?; 2945 if row.status != "pass" { 2946 return Err(format!( 2947 "required coverage crate {} has non-pass status {}", 2948 crate_name, row.status 2949 )); 2950 } 2951 let thresholds = policy.thresholds_for_scope(crate_name); 2952 validate_coverage_gate_report_for_row(crate_name, row, thresholds)?; 2953 if row.exec < thresholds.fail_under_exec_lines 2954 || row.func < thresholds.fail_under_functions 2955 || branch_coverage_fails(row.branch, thresholds) 2956 || row.region < thresholds.fail_under_regions 2957 { 2958 return Err(format!( 2959 "required coverage crate {} must satisfy coverage policy {},{},{},{}, found {}/{}/{}/{}", 2960 crate_name, 2961 thresholds.fail_under_exec_lines, 2962 thresholds.fail_under_functions, 2963 thresholds.fail_under_branches, 2964 thresholds.fail_under_regions, 2965 row.exec, 2966 row.func, 2967 branch_coverage_display(row.branch), 2968 row.region 2969 )); 2970 } 2971 } 2972 Ok(()) 2973 } 2974 2975 const CORE_UNIT_DIMENSION_ENUM: &str = "RadrootsCoreUnitDimension"; 2976 const CORE_UNIT_DIMENSION_ORDER: [&str; 3] = ["Count", "Mass", "Volume"]; 2977 2978 fn extract_enum_body<'a>(source: &'a str, enum_name: &str) -> Result<&'a str, String> { 2979 let marker = format!("pub enum {enum_name}"); 2980 let enum_start = match source.find(&marker) { 2981 Some(index) => index, 2982 None => return Err(format!("missing enum {enum_name}")), 2983 }; 2984 let after_start = &source[enum_start..]; 2985 let open_rel = match after_start.find('{') { 2986 Some(index) => index, 2987 None => return Err(format!("missing opening brace for enum {enum_name}")), 2988 }; 2989 let open_idx = enum_start + open_rel; 2990 let mut depth = 0usize; 2991 for (offset, ch) in source[open_idx..].char_indices() { 2992 if ch == '{' { 2993 depth += 1; 2994 continue; 2995 } 2996 if ch != '}' { 2997 continue; 2998 } 2999 depth = depth.saturating_sub(1); 3000 if depth == 0 { 3001 let close_idx = open_idx + offset; 3002 return Ok(&source[(open_idx + 1)..close_idx]); 3003 } 3004 } 3005 Err(format!("missing closing brace for enum {enum_name}")) 3006 } 3007 3008 fn parse_enum_variants(enum_body: &str) -> Vec<String> { 3009 enum_body 3010 .lines() 3011 .filter_map(|line| { 3012 let trimmed = line.trim(); 3013 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") { 3014 return None; 3015 } 3016 let before_comma = trimmed 3017 .split_once(',') 3018 .map_or(trimmed, |(head, _)| head) 3019 .trim(); 3020 if before_comma.is_empty() { 3021 return None; 3022 } 3023 let before_discriminant = before_comma 3024 .split_once('=') 3025 .map_or(before_comma, |(head, _)| head) 3026 .trim(); 3027 if before_discriminant.is_empty() { 3028 return None; 3029 } 3030 let ident = before_discriminant 3031 .split_whitespace() 3032 .next() 3033 .unwrap_or_default(); 3034 Some(ident.to_string()) 3035 }) 3036 .collect() 3037 } 3038 3039 fn validate_core_unit_dimension_variant_order(workspace_root: &Path) -> Result<(), String> { 3040 let source_path = workspace_root 3041 .join("crates") 3042 .join("core") 3043 .join("src") 3044 .join("unit.rs"); 3045 let source = match fs::read_to_string(&source_path) { 3046 Ok(source) => source, 3047 Err(e) => return Err(format!("read {}: {e}", source_path.display())), 3048 }; 3049 let enum_body = extract_enum_body(&source, CORE_UNIT_DIMENSION_ENUM)?; 3050 let variants = parse_enum_variants(enum_body); 3051 let expected = CORE_UNIT_DIMENSION_ORDER 3052 .iter() 3053 .map(|item| (*item).to_string()) 3054 .collect::<Vec<_>>(); 3055 if variants != expected { 3056 return Err(format!( 3057 "core unit dimension variant order must be {} but was {}", 3058 CORE_UNIT_DIMENSION_ORDER.join(", "), 3059 variants.join(", ") 3060 )); 3061 } 3062 Ok(()) 3063 } 3064 3065 fn validate_coverage_policy_parity( 3066 workspace_root: &Path, 3067 contract_root: &Path, 3068 ) -> Result<(), String> { 3069 let policy = load_coverage_policy(contract_root)?; 3070 let thresholds = policy.thresholds(); 3071 if thresholds.fail_under_exec_lines != COVERAGE_REQUIRED_THRESHOLD 3072 || thresholds.fail_under_functions != COVERAGE_REQUIRED_THRESHOLD 3073 || thresholds.fail_under_regions != COVERAGE_REQUIRED_THRESHOLD 3074 || thresholds.fail_under_branches != COVERAGE_REQUIRED_THRESHOLD 3075 || !thresholds.require_branches 3076 { 3077 return Err(format!( 3078 "coverage policy must enforce {COVERAGE_REQUIRED_THRESHOLD_LABEL} with required branches" 3079 )); 3080 } 3081 3082 let required_packages = policy 3083 .required_crate_entries() 3084 .iter() 3085 .cloned() 3086 .collect::<BTreeSet<_>>(); 3087 let expected_packages = coverage_required_workspace_crates(workspace_root)?; 3088 if expected_packages != required_packages { 3089 let missing = expected_packages 3090 .difference(&required_packages) 3091 .cloned() 3092 .collect::<BTreeSet<_>>(); 3093 let extra = required_packages 3094 .difference(&expected_packages) 3095 .cloned() 3096 .collect::<BTreeSet<_>>(); 3097 return Err(format!( 3098 "coverage policy missing workspace crates: {}; coverage policy includes excluded or unknown crates: {}", 3099 join_set(&missing), 3100 join_set(&extra) 3101 )); 3102 } 3103 3104 Ok(()) 3105 } 3106 3107 fn publish_config_is_public(publish: Option<&PackagePublish>) -> bool { 3108 matches!( 3109 publish, 3110 Some(PackagePublish::Registries(registries)) 3111 if registries.len() == 1 && registries[0] == "crates-io" 3112 ) 3113 } 3114 3115 fn publish_config_is_non_public(publish: Option<&PackagePublish>) -> bool { 3116 matches!(publish, Some(PackagePublish::Bool(false))) 3117 } 3118 3119 fn validate_release_publish_policy( 3120 workspace_root: &Path, 3121 contract_root: &Path, 3122 contract_version: &str, 3123 ) -> Result<(), String> { 3124 let release = load_release_contract(workspace_root, contract_root)?; 3125 if release.release.version.trim().is_empty() { 3126 return Err("release.version must not be empty".to_string()); 3127 } 3128 if release.release.version != contract_version { 3129 return Err(format!( 3130 "release.version {} must match contract version {}", 3131 release.release.version, contract_version 3132 )); 3133 } 3134 3135 let workspace_packages = workspace_package_names(workspace_root)? 3136 .into_iter() 3137 .collect::<BTreeSet<_>>(); 3138 let uses_classification = release.uses_classification(); 3139 let public_field = if uses_classification { 3140 "classification.public" 3141 } else { 3142 "publish.crates" 3143 }; 3144 let internal_field = if uses_classification { 3145 "classification.internal" 3146 } else { 3147 "internal.crates" 3148 }; 3149 3150 let public_set = collect_unique_set(&release.public_crates(), public_field)?; 3151 let internal_set = collect_unique_set(&release.internal_crates(), internal_field)?; 3152 let deferred_set = collect_unique_set(&release.deferred_crates(), "classification.deferred")?; 3153 let retired_set = collect_unique_set(&release.retired_crates(), "classification.retired")?; 3154 let yank_only_set = 3155 collect_unique_set(&release.yank_only_crates(), "classification.yank_only")?; 3156 let publish_order = &release.publish_order.crates; 3157 let publish_order_set = collect_unique_set(publish_order, "publish_order.crates")?; 3158 3159 let class_sets = [ 3160 ("public", &public_set), 3161 ("internal", &internal_set), 3162 ("deferred", &deferred_set), 3163 ("retired", &retired_set), 3164 ("yank-only", &yank_only_set), 3165 ]; 3166 for idx in 0..class_sets.len() { 3167 for other_idx in (idx + 1)..class_sets.len() { 3168 let overlap = class_sets[idx] 3169 .1 3170 .intersection(class_sets[other_idx].1) 3171 .cloned() 3172 .collect::<BTreeSet<_>>(); 3173 if !overlap.is_empty() { 3174 return Err(format!( 3175 "release classification overlap is not allowed between {} and {}: {}", 3176 class_sets[idx].0, 3177 class_sets[other_idx].0, 3178 join_set(&overlap) 3179 )); 3180 } 3181 } 3182 } 3183 3184 let mut combined = public_set.clone(); 3185 combined.extend(internal_set.iter().cloned()); 3186 combined.extend(deferred_set.iter().cloned()); 3187 combined.extend(retired_set.iter().cloned()); 3188 combined.extend(yank_only_set.iter().cloned()); 3189 if combined != workspace_packages { 3190 let missing = workspace_packages 3191 .difference(&combined) 3192 .cloned() 3193 .collect::<BTreeSet<_>>(); 3194 let extra = combined 3195 .difference(&workspace_packages) 3196 .cloned() 3197 .collect::<BTreeSet<_>>(); 3198 return Err(format!( 3199 "release classification sets are missing workspace crates: {}; release classification sets include unknown crates: {}", 3200 join_set(&missing), 3201 join_set(&extra) 3202 )); 3203 } 3204 3205 if publish_order_set != public_set { 3206 let missing = public_set 3207 .difference(&publish_order_set) 3208 .cloned() 3209 .collect::<BTreeSet<_>>(); 3210 let extra = publish_order_set 3211 .difference(&public_set) 3212 .cloned() 3213 .collect::<BTreeSet<_>>(); 3214 return Err(format!( 3215 "publish_order.crates is missing publish crates: {}; publish_order.crates has non-publish crates: {}", 3216 join_set(&missing), 3217 join_set(&extra) 3218 )); 3219 } 3220 3221 let order_index = publish_order 3222 .iter() 3223 .enumerate() 3224 .map(|(idx, name)| (name.clone(), idx)) 3225 .collect::<BTreeMap<_, _>>(); 3226 let dependencies = read_workspace_package_dependencies(workspace_root) 3227 .expect("workspace package manifests were already parsed"); 3228 for crate_name in &public_set { 3229 let crate_deps = &dependencies[crate_name]; 3230 let crate_order = order_index[crate_name]; 3231 for dep in crate_deps { 3232 if !public_set.contains(dep) { 3233 continue; 3234 } 3235 let dep_order = order_index[dep]; 3236 if dep_order >= crate_order { 3237 return Err(format!( 3238 "publish order must place dependency {} before {}", 3239 dep, crate_name 3240 )); 3241 } 3242 } 3243 } 3244 3245 let publish_configs = workspace_package_publish_configs(workspace_root) 3246 .expect("workspace publish configs are stable"); 3247 for crate_name in &public_set { 3248 let publish = publish_configs[crate_name].as_ref(); 3249 if !publish_config_is_public(publish) { 3250 return Err(format!( 3251 "public crate {} must set publish = [\"crates-io\"]", 3252 crate_name 3253 )); 3254 } 3255 } 3256 for crate_name in internal_set 3257 .iter() 3258 .chain(deferred_set.iter()) 3259 .chain(retired_set.iter()) 3260 .chain(yank_only_set.iter()) 3261 { 3262 let publish = publish_configs[crate_name].as_ref(); 3263 if !publish_config_is_non_public(publish) { 3264 return Err(format!( 3265 "non-public crate {} must set publish = false", 3266 crate_name 3267 )); 3268 } 3269 } 3270 3271 Ok(()) 3272 } 3273 3274 pub fn validate_release_preflight(workspace_root: &Path) -> Result<(), String> { 3275 validate_release_preflight_with_override(workspace_root, None) 3276 } 3277 3278 pub fn validate_release_preflight_with_override( 3279 workspace_root: &Path, 3280 release_policy_override: Option<PathBuf>, 3281 ) -> Result<(), String> { 3282 let bundle = load_contract_bundle(workspace_root)?; 3283 validate_contract_bundle_with_release_policy_override( 3284 &bundle, 3285 release_policy_override.clone(), 3286 )?; 3287 let release = 3288 load_release_contract_with_override(workspace_root, &bundle.root, release_policy_override)?; 3289 let policy = 3290 load_coverage_policy(&bundle.root).expect("validated contract includes coverage policy"); 3291 let publish_crates = collect_unique_set( 3292 &release.public_crates(), 3293 if release.uses_classification() { 3294 "classification.public" 3295 } else { 3296 "publish.crates" 3297 }, 3298 ) 3299 .expect("validated contract enforces unique public crates"); 3300 let required_crate_list = policy 3301 .required_crates() 3302 .expect("validated contract includes required crates"); 3303 let required_crates = collect_unique_set(&required_crate_list, "required.crates") 3304 .expect("validated contract enforces unique required.crates"); 3305 validate_publish_package_metadata(workspace_root, &publish_crates)?; 3306 validate_required_coverage_summary_with_policy(workspace_root, &required_crates, &policy)?; 3307 Ok(()) 3308 } 3309 3310 fn validate_contract_bundle_with_release_policy_override( 3311 bundle: &ContractBundle, 3312 release_policy_override: Option<PathBuf>, 3313 ) -> Result<(), String> { 3314 if bundle.manifest.contract.name.trim().is_empty() { 3315 return Err("contract name is required".to_string()); 3316 } 3317 if bundle.manifest.contract.version.trim().is_empty() { 3318 return Err("contract version is required".to_string()); 3319 } 3320 if bundle.manifest.contract.source.trim().is_empty() { 3321 return Err("contract source is required".to_string()); 3322 } 3323 if bundle.manifest.surface.model_crates.is_empty() { 3324 return Err("contract surface.model_crates must not be empty".to_string()); 3325 } 3326 if bundle.manifest.surface.algorithm_crates.is_empty() { 3327 return Err("contract surface.algorithm_crates must not be empty".to_string()); 3328 } 3329 validate_surface_metadata(&bundle.manifest.surface)?; 3330 if bundle.version.contract.version.trim().is_empty() { 3331 return Err("version.contract.version is required".to_string()); 3332 } 3333 if bundle.version.contract.stability.trim().is_empty() { 3334 return Err("version.contract.stability is required".to_string()); 3335 } 3336 if bundle.version.semver.major_on.is_empty() 3337 || bundle.version.semver.minor_on.is_empty() 3338 || bundle.version.semver.patch_on.is_empty() 3339 { 3340 return Err("version.semver rules must all be non-empty".to_string()); 3341 } 3342 if !bundle.version.compatibility.requires_conformance_pass { 3343 return Err("compatibility.requires_conformance_pass must be true".to_string()); 3344 } 3345 if !bundle.version.compatibility.requires_contract_manifest_diff { 3346 return Err("compatibility.requires_contract_manifest_diff must be true".to_string()); 3347 } 3348 if !bundle.version.compatibility.requires_release_notes { 3349 return Err("compatibility.requires_release_notes must be true".to_string()); 3350 } 3351 validate_policy_metadata(&bundle.manifest.policy)?; 3352 let workspace_root = bundle 3353 .root 3354 .parent() 3355 .expect("contract root must have a workspace parent"); 3356 if let Some(operations_manifest) = bundle.operations_manifest.as_ref() { 3357 validate_operations_contract(bundle, operations_manifest, workspace_root)?; 3358 } 3359 validate_all_conformance_vectors(workspace_root, &bundle.manifest.contract.version)?; 3360 validate_core_unit_dimension_variant_order(workspace_root)?; 3361 validate_coverage_policy_parity(workspace_root, &bundle.root)?; 3362 if resolve_release_contract_path_with_override(workspace_root, release_policy_override.clone()) 3363 .expect("validated release contract path resolution should not fail") 3364 .is_some() 3365 { 3366 validate_release_publish_policy_with_override( 3367 workspace_root, 3368 &bundle.root, 3369 bundle.version.contract.version.as_str(), 3370 release_policy_override, 3371 )?; 3372 } 3373 Ok(()) 3374 } 3375 3376 fn validate_release_publish_policy_with_override( 3377 workspace_root: &Path, 3378 contract_root: &Path, 3379 contract_version: &str, 3380 release_policy_override: Option<PathBuf>, 3381 ) -> Result<(), String> { 3382 let release = load_release_contract_with_override( 3383 workspace_root, 3384 contract_root, 3385 release_policy_override, 3386 )?; 3387 if release.release.version.trim().is_empty() { 3388 return Err("release.version must not be empty".to_string()); 3389 } 3390 if release.release.version != contract_version { 3391 return Err(format!( 3392 "release.version {} must match contract version {}", 3393 release.release.version, contract_version 3394 )); 3395 } 3396 3397 let workspace_packages = workspace_package_names(workspace_root)? 3398 .into_iter() 3399 .collect::<BTreeSet<_>>(); 3400 let uses_classification = release.uses_classification(); 3401 let public_field = if uses_classification { 3402 "classification.public" 3403 } else { 3404 "publish.crates" 3405 }; 3406 let internal_field = if uses_classification { 3407 "classification.internal" 3408 } else { 3409 "internal.crates" 3410 }; 3411 3412 let public_set = collect_unique_set(&release.public_crates(), public_field)?; 3413 let internal_set = collect_unique_set(&release.internal_crates(), internal_field)?; 3414 let deferred_set = collect_unique_set(&release.deferred_crates(), "classification.deferred")?; 3415 let retired_set = collect_unique_set(&release.retired_crates(), "classification.retired")?; 3416 let yank_only_set = 3417 collect_unique_set(&release.yank_only_crates(), "classification.yank_only")?; 3418 let publish_order = &release.publish_order.crates; 3419 let publish_order_set = collect_unique_set(publish_order, "publish_order.crates")?; 3420 3421 let class_sets = [ 3422 ("public", &public_set), 3423 ("internal", &internal_set), 3424 ("deferred", &deferred_set), 3425 ("retired", &retired_set), 3426 ("yank-only", &yank_only_set), 3427 ]; 3428 for idx in 0..class_sets.len() { 3429 for other_idx in (idx + 1)..class_sets.len() { 3430 let overlap = class_sets[idx] 3431 .1 3432 .intersection(class_sets[other_idx].1) 3433 .cloned() 3434 .collect::<BTreeSet<_>>(); 3435 if !overlap.is_empty() { 3436 return Err(format!( 3437 "release classification overlap is not allowed between {} and {}: {}", 3438 class_sets[idx].0, 3439 class_sets[other_idx].0, 3440 join_set(&overlap) 3441 )); 3442 } 3443 } 3444 } 3445 3446 let mut combined = public_set.clone(); 3447 combined.extend(internal_set.iter().cloned()); 3448 combined.extend(deferred_set.iter().cloned()); 3449 combined.extend(retired_set.iter().cloned()); 3450 combined.extend(yank_only_set.iter().cloned()); 3451 if combined != workspace_packages { 3452 let missing = workspace_packages 3453 .difference(&combined) 3454 .cloned() 3455 .collect::<BTreeSet<_>>(); 3456 let extra = combined 3457 .difference(&workspace_packages) 3458 .cloned() 3459 .collect::<BTreeSet<_>>(); 3460 return Err(format!( 3461 "release classification sets are missing workspace crates: {}; release classification sets include unknown crates: {}", 3462 join_set(&missing), 3463 join_set(&extra) 3464 )); 3465 } 3466 3467 if publish_order_set != public_set { 3468 let missing = public_set 3469 .difference(&publish_order_set) 3470 .cloned() 3471 .collect::<BTreeSet<_>>(); 3472 let extra = publish_order_set 3473 .difference(&public_set) 3474 .cloned() 3475 .collect::<BTreeSet<_>>(); 3476 return Err(format!( 3477 "publish_order.crates is missing publish crates: {}; publish_order.crates has non-publish crates: {}", 3478 join_set(&missing), 3479 join_set(&extra) 3480 )); 3481 } 3482 3483 let order_index = publish_order 3484 .iter() 3485 .enumerate() 3486 .map(|(idx, name)| (name.clone(), idx)) 3487 .collect::<BTreeMap<_, _>>(); 3488 let dependencies = read_workspace_package_dependencies(workspace_root) 3489 .expect("workspace package manifests were already parsed"); 3490 for crate_name in &public_set { 3491 let crate_deps = &dependencies[crate_name]; 3492 let crate_order = order_index[crate_name]; 3493 for dep in crate_deps { 3494 if !public_set.contains(dep) { 3495 continue; 3496 } 3497 let dep_order = order_index[dep]; 3498 if dep_order >= crate_order { 3499 return Err(format!( 3500 "publish order must place dependency {} before {}", 3501 dep, crate_name 3502 )); 3503 } 3504 } 3505 } 3506 3507 let publish_configs = workspace_package_publish_configs(workspace_root) 3508 .expect("workspace publish configs are stable"); 3509 for crate_name in &public_set { 3510 let publish = publish_configs[crate_name].as_ref(); 3511 if !publish_config_is_public(publish) { 3512 return Err(format!( 3513 "public crate {} must set publish = [\"crates-io\"]", 3514 crate_name 3515 )); 3516 } 3517 } 3518 for crate_name in internal_set 3519 .iter() 3520 .chain(deferred_set.iter()) 3521 .chain(retired_set.iter()) 3522 .chain(yank_only_set.iter()) 3523 { 3524 let publish = publish_configs[crate_name].as_ref(); 3525 if !publish_config_is_non_public(publish) { 3526 return Err(format!( 3527 "non-public crate {} must set publish = false", 3528 crate_name 3529 )); 3530 } 3531 } 3532 3533 Ok(()) 3534 } 3535 3536 #[cfg(test)] 3537 #[cfg_attr(coverage_nightly, coverage(off))] 3538 pub fn synthetic_release_policy_for_workspace(workspace_root: &Path) -> Result<String, String> { 3539 let bundle = load_contract_bundle(workspace_root)?; 3540 let publish_configs = workspace_package_publish_configs(workspace_root)?; 3541 let dependencies = read_workspace_package_dependencies(workspace_root)?; 3542 3543 let mut public = BTreeSet::new(); 3544 let mut internal = BTreeSet::new(); 3545 for (crate_name, publish) in &publish_configs { 3546 if publish_config_is_public(publish.as_ref()) { 3547 public.insert(crate_name.clone()); 3548 } else { 3549 internal.insert(crate_name.clone()); 3550 } 3551 } 3552 3553 let mut in_degree = BTreeMap::new(); 3554 let mut dependents = BTreeMap::<String, BTreeSet<String>>::new(); 3555 for crate_name in &public { 3556 in_degree.insert(crate_name.clone(), 0usize); 3557 dependents.insert(crate_name.clone(), BTreeSet::new()); 3558 } 3559 for crate_name in &public { 3560 for dep in &dependencies[crate_name] { 3561 if !public.contains(dep) { 3562 continue; 3563 } 3564 *in_degree 3565 .get_mut(crate_name) 3566 .expect("public crate present in indegree map") += 1; 3567 dependents 3568 .get_mut(dep) 3569 .expect("public dependency present in dependents map") 3570 .insert(crate_name.clone()); 3571 } 3572 } 3573 3574 let mut ready = in_degree 3575 .iter() 3576 .filter(|(_, degree)| **degree == 0) 3577 .map(|(crate_name, _)| crate_name.clone()) 3578 .collect::<BTreeSet<_>>(); 3579 let mut publish_order = Vec::new(); 3580 while let Some(crate_name) = ready.pop_first() { 3581 publish_order.push(crate_name.clone()); 3582 for dependent in dependents[&crate_name].clone() { 3583 let degree = in_degree 3584 .get_mut(&dependent) 3585 .expect("dependent crate present in indegree map"); 3586 *degree -= 1; 3587 if *degree == 0 { 3588 ready.insert(dependent); 3589 } 3590 } 3591 } 3592 if publish_order.len() != public.len() { 3593 return Err("public crate dependency graph contains a cycle".to_string()); 3594 } 3595 3596 let public = public.into_iter().collect::<Vec<_>>(); 3597 let internal = internal.into_iter().collect::<Vec<_>>(); 3598 Ok(format!( 3599 "[release]\nversion = \"{}\"\n\n[classification]\npublic = {}\ninternal = {}\ndeferred = []\nretired = []\nyank_only = []\n\n[publish_order]\ncrates = {}\n", 3600 bundle.version.contract.version, 3601 toml_inline_array(&public), 3602 toml_inline_array(&internal), 3603 toml_inline_array(&publish_order), 3604 )) 3605 } 3606 3607 #[cfg(test)] 3608 #[cfg_attr(coverage_nightly, coverage(off))] 3609 fn toml_inline_array(values: &[String]) -> String { 3610 let joined = values 3611 .iter() 3612 .map(|value| format!("\"{value}\"")) 3613 .collect::<Vec<_>>() 3614 .join(", "); 3615 format!("[{joined}]") 3616 } 3617 3618 pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, String> { 3619 reject_legacy_contract_roots(workspace_root)?; 3620 let root = contract_root(workspace_root); 3621 let manifest = parse_toml::<ContractManifest>(&root.join("manifest.toml"))?; 3622 let version = parse_toml::<VersionPolicy>(&root.join("version.toml"))?; 3623 let operations_manifest_path = root.join("operations.toml"); 3624 let operations_manifest = if operations_manifest_path.is_file() { 3625 Some(parse_toml::<OperationsContractManifest>( 3626 &operations_manifest_path, 3627 )?) 3628 } else { 3629 None 3630 }; 3631 Ok(ContractBundle { 3632 root, 3633 manifest, 3634 version, 3635 operations_manifest, 3636 }) 3637 } 3638 3639 fn reject_legacy_contract_roots(workspace_root: &Path) -> Result<(), String> { 3640 for relative in ["spec", "policy"] { 3641 let legacy_root = workspace_root.join(relative); 3642 if legacy_root.exists() { 3643 return Err(format!( 3644 "legacy contract root {} is forbidden; use contracts/", 3645 legacy_root.display() 3646 )); 3647 } 3648 } 3649 Ok(()) 3650 } 3651 3652 pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { 3653 if bundle.manifest.contract.name.trim().is_empty() { 3654 return Err("contract name is required".to_string()); 3655 } 3656 if bundle.manifest.contract.version.trim().is_empty() { 3657 return Err("contract version is required".to_string()); 3658 } 3659 if bundle.manifest.contract.source.trim().is_empty() { 3660 return Err("contract source is required".to_string()); 3661 } 3662 if bundle.manifest.surface.model_crates.is_empty() { 3663 return Err("contract surface.model_crates must not be empty".to_string()); 3664 } 3665 if bundle.manifest.surface.algorithm_crates.is_empty() { 3666 return Err("contract surface.algorithm_crates must not be empty".to_string()); 3667 } 3668 validate_surface_metadata(&bundle.manifest.surface)?; 3669 if bundle.version.contract.version.trim().is_empty() { 3670 return Err("version.contract.version is required".to_string()); 3671 } 3672 if bundle.version.contract.stability.trim().is_empty() { 3673 return Err("version.contract.stability is required".to_string()); 3674 } 3675 if bundle.version.semver.major_on.is_empty() 3676 || bundle.version.semver.minor_on.is_empty() 3677 || bundle.version.semver.patch_on.is_empty() 3678 { 3679 return Err("version.semver rules must all be non-empty".to_string()); 3680 } 3681 if !bundle.version.compatibility.requires_conformance_pass { 3682 return Err("compatibility.requires_conformance_pass must be true".to_string()); 3683 } 3684 if !bundle.version.compatibility.requires_contract_manifest_diff { 3685 return Err("compatibility.requires_contract_manifest_diff must be true".to_string()); 3686 } 3687 if !bundle.version.compatibility.requires_release_notes { 3688 return Err("compatibility.requires_release_notes must be true".to_string()); 3689 } 3690 validate_policy_metadata(&bundle.manifest.policy)?; 3691 let workspace_root = bundle 3692 .root 3693 .parent() 3694 .expect("contract root must have a workspace parent"); 3695 if let Some(operations_manifest) = bundle.operations_manifest.as_ref() { 3696 validate_operations_contract(bundle, operations_manifest, workspace_root)?; 3697 } 3698 validate_all_conformance_vectors(workspace_root, &bundle.manifest.contract.version)?; 3699 validate_core_unit_dimension_variant_order(workspace_root)?; 3700 validate_coverage_policy_parity(workspace_root, &bundle.root)?; 3701 if resolve_release_contract_path(workspace_root) 3702 .expect("validated release contract path resolution should not fail") 3703 .is_some() 3704 { 3705 validate_release_publish_policy( 3706 workspace_root, 3707 &bundle.root, 3708 bundle.version.contract.version.as_str(), 3709 )?; 3710 } 3711 Ok(()) 3712 } 3713 3714 #[cfg(test)] 3715 mod tests { 3716 use super::*; 3717 use std::collections::BTreeSet; 3718 use std::fs; 3719 use std::path::{Path, PathBuf}; 3720 use std::time::{SystemTime, UNIX_EPOCH}; 3721 3722 fn workspace_root() -> PathBuf { 3723 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 3724 manifest_dir 3725 .join("../..") 3726 .canonicalize() 3727 .expect("canonical workspace root") 3728 } 3729 3730 fn temp_root(prefix: &str) -> PathBuf { 3731 let nanos = SystemTime::now() 3732 .duration_since(UNIX_EPOCH) 3733 .expect("clock") 3734 .as_nanos(); 3735 let root = std::env::temp_dir().join(format!("radroots_xtask_{prefix}_{nanos}")); 3736 fs::create_dir_all(&root).expect("create temp root"); 3737 root 3738 } 3739 3740 fn write_file(path: &Path, content: &str) { 3741 let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new(""))); 3742 fs::write(path, content).expect("write file"); 3743 } 3744 3745 fn strict_thresholds() -> CoverageThresholds { 3746 CoverageThresholds { 3747 fail_under_exec_lines: 100.0, 3748 fail_under_functions: 100.0, 3749 fail_under_regions: 100.0, 3750 fail_under_branches: 100.0, 3751 require_branches: true, 3752 } 3753 } 3754 3755 fn coverage_thresholds(value: f64, require_branches: bool) -> CoverageThresholds { 3756 CoverageThresholds { 3757 fail_under_exec_lines: value, 3758 fail_under_functions: value, 3759 fail_under_regions: value, 3760 fail_under_branches: value, 3761 require_branches, 3762 } 3763 } 3764 3765 struct TestCoverageRefreshRow<'a> { 3766 crate_name: &'a str, 3767 status: &'a str, 3768 thresholds: CoverageThresholds, 3769 exec: f64, 3770 func: f64, 3771 branch: Option<f64>, 3772 region: f64, 3773 report_pass: bool, 3774 } 3775 3776 fn passing_coverage_row(crate_name: &str) -> TestCoverageRefreshRow<'_> { 3777 TestCoverageRefreshRow { 3778 crate_name, 3779 status: "pass", 3780 thresholds: coverage_thresholds(100.0, true), 3781 exec: 100.0, 3782 func: 100.0, 3783 branch: Some(100.0), 3784 region: 100.0, 3785 report_pass: true, 3786 } 3787 } 3788 3789 fn coverage_refresh_branch_value(branch: Option<f64>) -> String { 3790 branch 3791 .map(|value| value.to_string()) 3792 .unwrap_or_else(|| "unavailable".to_string()) 3793 } 3794 3795 fn write_test_coverage_gate_report(root: &Path, row: &TestCoverageRefreshRow<'_>) -> String { 3796 let report_relative = format!("target/coverage/{}/gate-report.json", row.crate_name); 3797 let report_path = root.join(&report_relative); 3798 let fail_reasons = if row.report_pass { 3799 Vec::<&str>::new() 3800 } else { 3801 vec!["policy gate failed"] 3802 }; 3803 let report = serde_json::json!({ 3804 "scope": row.crate_name, 3805 "thresholds": { 3806 "executable_lines": row.thresholds.fail_under_exec_lines, 3807 "functions": row.thresholds.fail_under_functions, 3808 "regions": row.thresholds.fail_under_regions, 3809 "branches": row.thresholds.fail_under_branches, 3810 "branches_required": row.thresholds.require_branches 3811 }, 3812 "measured": { 3813 "executable_lines_percent": row.exec, 3814 "executable_lines_source": "da", 3815 "functions_percent": row.func, 3816 "branches_percent": row.branch, 3817 "branches_available": row.branch.is_some(), 3818 "summary_lines_percent": row.exec, 3819 "summary_regions_percent": row.region 3820 }, 3821 "counts": { 3822 "executable_lines": { 3823 "covered": 1, 3824 "total": 1 3825 }, 3826 "branches": { 3827 "covered": if row.branch.is_some() { 1 } else { 0 }, 3828 "total": if row.branch.is_some() { 1 } else { 0 } 3829 } 3830 }, 3831 "result": { 3832 "pass": row.report_pass, 3833 "fail_reasons": fail_reasons 3834 } 3835 }); 3836 let json = 3837 serde_json::to_string_pretty(&report).expect("serialize test coverage gate report"); 3838 write_file(&report_path, &format!("{json}\n")); 3839 report_relative 3840 } 3841 3842 fn write_test_coverage_refresh(root: &Path, rows: &[TestCoverageRefreshRow<'_>]) { 3843 let mut refresh_rows = String::from("crate\tstatus\texec\tfunc\tbranch\tregion\treport\n"); 3844 for row in rows { 3845 let report_relative = write_test_coverage_gate_report(root, row); 3846 refresh_rows.push_str(&format!( 3847 "{}\t{}\t{}\t{}\t{}\t{}\t{}\n", 3848 row.crate_name, 3849 row.status, 3850 row.exec, 3851 row.func, 3852 coverage_refresh_branch_value(row.branch), 3853 row.region, 3854 report_relative 3855 )); 3856 } 3857 write_file( 3858 &root 3859 .join("target") 3860 .join("coverage") 3861 .join("coverage-refresh.tsv"), 3862 &refresh_rows, 3863 ); 3864 } 3865 3866 fn create_synthetic_workspace(prefix: &str) -> PathBuf { 3867 let root = temp_root(prefix); 3868 write_file( 3869 &root.join("Cargo.toml"), 3870 r#"[workspace] 3871 members = ["crates/a", "crates/b"] 3872 resolver = "2" 3873 "#, 3874 ); 3875 write_file( 3876 &root.join("crates").join("a").join("Cargo.toml"), 3877 r#"[package] 3878 name = "radroots_a" 3879 publish = ["crates-io"] 3880 version = "0.1.0" 3881 edition = "2024" 3882 description = "crate a" 3883 repository = "https://example.com/a" 3884 homepage = "https://example.com/a" 3885 documentation = "https://docs.example.com/a" 3886 readme = "README" 3887 "#, 3888 ); 3889 write_file( 3890 &root.join("crates").join("b").join("Cargo.toml"), 3891 r#"[package] 3892 name = "radroots_b" 3893 version = "0.1.0" 3894 edition = "2024" 3895 publish = false 3896 "#, 3897 ); 3898 write_file( 3899 &root.join("crates").join("core").join("src").join("unit.rs"), 3900 r#"pub enum RadrootsCoreUnitDimension { 3901 Count, 3902 Mass, 3903 Volume, 3904 } 3905 "#, 3906 ); 3907 3908 write_file( 3909 &root.join("contracts").join("manifest.toml"), 3910 r#"[contract] 3911 name = "radroots_contract" 3912 version = "1.0.0" 3913 source = "synthetic" 3914 3915 [surface] 3916 model_crates = ["radroots_a"] 3917 algorithm_crates = ["radroots_b"] 3918 3919 [policy] 3920 exclude_internal_workspace_crates = true 3921 require_reproducible_exports = true 3922 require_conformance_vectors = true 3923 "#, 3924 ); 3925 write_file( 3926 &root.join("contracts").join("version.toml"), 3927 r#"[contract] 3928 version = "1.0.0" 3929 stability = "alpha" 3930 3931 [semver] 3932 major_on = ["breaking"] 3933 minor_on = ["feature"] 3934 patch_on = ["fix"] 3935 3936 [compatibility] 3937 requires_conformance_pass = true 3938 requires_contract_manifest_diff = true 3939 requires_release_notes = true 3940 "#, 3941 ); 3942 write_file( 3943 &root.join("contracts").join("coverage.toml"), 3944 r#"[gate] 3945 fail_under_exec_lines = 100.0 3946 fail_under_functions = 100.0 3947 fail_under_regions = 100.0 3948 fail_under_branches = 100.0 3949 require_branches = true 3950 3951 [required] 3952 crates = ["radroots_a", "radroots_b"] 3953 "#, 3954 ); 3955 write_file( 3956 &root_release_policy_path(&root), 3957 r#"[release] 3958 version = "1.0.0" 3959 3960 [publish] 3961 crates = ["radroots_a"] 3962 3963 [internal] 3964 crates = ["radroots_b"] 3965 3966 [publish_order] 3967 crates = ["radroots_a"] 3968 "#, 3969 ); 3970 write_test_coverage_refresh( 3971 &root, 3972 &[ 3973 passing_coverage_row("radroots_a"), 3974 passing_coverage_row("radroots_b"), 3975 ], 3976 ); 3977 root 3978 } 3979 3980 fn add_operation_contract_files(root: &Path) { 3981 write_file( 3982 &root.join("contracts").join("operations.toml"), 3983 r#"[contract] 3984 name = "radroots_contract" 3985 version = "1.0.0" 3986 source = "synthetic" 3987 3988 [public] 3989 domains = ["profile", "farm", "listing", "trade"] 3990 3991 [shared_types] 3992 public = [ 3993 "WireEventParts", 3994 "RadrootsFrozenEventDraft", 3995 "RadrootsSignedNostrEvent", 3996 "RadrootsNostrEvent", 3997 "RadrootsNostrEventRef", 3998 "RadrootsNostrEventPtr", 3999 "RadrootsListingAddress", 4000 "RadrootsProfile", 4001 "RadrootsFarm", 4002 "RadrootsListing", 4003 ] 4004 4005 [errors] 4006 classes = ["encode_error", "parse_error", "validation_error", "address_error"] 4007 4008 [implementation_provenance] 4009 model_crates = ["radroots_a"] 4010 algorithm_crates = ["radroots_b"] 4011 4012 [operations.profile_build_draft] 4013 domain = "profile" 4014 id = "profile.build_draft" 4015 stability = "beta" 4016 inputs = ["RadrootsProfile", "RadrootsProfileType?"] 4017 outputs = ["WireEventParts"] 4018 error_class = "encode_error" 4019 deterministic = true 4020 signing = "native" 4021 transport = "native" 4022 4023 [operations.profile_build_draft.implementation] 4024 rust_modules = ["crates/core/src/unit.rs"] 4025 rust_types = ["radroots_events::profile::RadrootsProfile"] 4026 4027 [operations.profile_build_draft.conformance] 4028 vector = "contracts/conformance/vectors/profile/build_draft.v1.json" 4029 4030 [operations.listing_build_draft] 4031 domain = "listing" 4032 id = "listing.build_draft" 4033 stability = "beta" 4034 inputs = ["RadrootsListing"] 4035 outputs = ["WireEventParts"] 4036 error_class = "encode_error" 4037 deterministic = true 4038 signing = "native" 4039 transport = "native" 4040 4041 [operations.listing_build_draft.implementation] 4042 rust_modules = ["crates/core/src/unit.rs"] 4043 rust_types = ["radroots_events::listing::RadrootsListing"] 4044 4045 [operations.listing_build_draft.conformance] 4046 vector = "contracts/conformance/vectors/listing/build_draft.v1.json" 4047 "#, 4048 ); 4049 write_file( 4050 &root 4051 .join("contracts") 4052 .join("conformance") 4053 .join("schema") 4054 .join("vector.schema.json"), 4055 r#"{ 4056 "$schema": "http://json-schema.org/draft-07/schema#", 4057 "$id": "https://radroots.org/core/conformance/vector.schema.json", 4058 "title": "radroots core conformance vector", 4059 "type": "object", 4060 "required": ["suite", "contract_version", "vectors"], 4061 "properties": { 4062 "suite": { 4063 "type": "string", 4064 "minLength": 1 4065 }, 4066 "contract_version": { 4067 "type": "string", 4068 "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" 4069 }, 4070 "vectors": { 4071 "type": "array", 4072 "items": { 4073 "type": "object", 4074 "required": ["id", "kind", "input", "expected"], 4075 "properties": { 4076 "id": { 4077 "type": "string", 4078 "minLength": 1 4079 }, 4080 "kind": { 4081 "type": "string", 4082 "minLength": 1 4083 }, 4084 "input": {}, 4085 "expected": {} 4086 }, 4087 "additionalProperties": false 4088 } 4089 } 4090 }, 4091 "additionalProperties": false 4092 } 4093 "#, 4094 ); 4095 write_file( 4096 &root 4097 .join("contracts") 4098 .join("conformance") 4099 .join("vectors") 4100 .join("profile") 4101 .join("build_draft.v1.json"), 4102 r#"{ 4103 "suite": "profile", 4104 "contract_version": "1.0.0", 4105 "vectors": [ 4106 { 4107 "id": "profile_build_draft_minimal_001", 4108 "kind": "profile.build_draft", 4109 "input": {}, 4110 "expected": {} 4111 } 4112 ] 4113 } 4114 "#, 4115 ); 4116 write_file( 4117 &root 4118 .join("contracts") 4119 .join("conformance") 4120 .join("vectors") 4121 .join("listing") 4122 .join("build_draft.v1.json"), 4123 r#"{ 4124 "suite": "listing", 4125 "contract_version": "1.0.0", 4126 "vectors": [ 4127 { 4128 "id": "listing_build_draft_minimal_001", 4129 "kind": "listing.build_draft", 4130 "input": {}, 4131 "expected": {} 4132 } 4133 ] 4134 } 4135 "#, 4136 ); 4137 } 4138 4139 fn write_root_release_policy(root: &Path, raw: &str) { 4140 write_file(&root.join(ROOT_RELEASE_POLICY_RELATIVE), raw); 4141 } 4142 4143 fn configure_root_release_policy_workspace(root: &Path) { 4144 write_file( 4145 &root.join("Cargo.toml"), 4146 r#"[workspace] 4147 members = ["crates/a", "crates/b", "crates/c", "crates/d", "crates/e"] 4148 resolver = "2" 4149 "#, 4150 ); 4151 for crate_name in ["c", "d", "e"] { 4152 write_file( 4153 &root.join("crates").join(crate_name).join("Cargo.toml"), 4154 &format!( 4155 r#"[package] 4156 name = "radroots_{crate_name}" 4157 version = "0.1.0" 4158 edition = "2024" 4159 publish = false 4160 "# 4161 ), 4162 ); 4163 } 4164 write_file( 4165 &root.join("contracts").join("coverage.toml"), 4166 r#"[gate] 4167 fail_under_exec_lines = 100.0 4168 fail_under_functions = 100.0 4169 fail_under_regions = 100.0 4170 fail_under_branches = 100.0 4171 require_branches = true 4172 4173 [required] 4174 crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] 4175 "#, 4176 ); 4177 write_test_coverage_refresh( 4178 root, 4179 &[ 4180 passing_coverage_row("radroots_a"), 4181 passing_coverage_row("radroots_b"), 4182 passing_coverage_row("radroots_c"), 4183 passing_coverage_row("radroots_d"), 4184 passing_coverage_row("radroots_e"), 4185 ], 4186 ); 4187 let _ = fs::remove_file(root_release_policy_path(&root)); 4188 } 4189 4190 #[test] 4191 fn validate_current_contract_bundle() { 4192 let root = workspace_root(); 4193 let bundle = load_contract_bundle(&root).expect("load contract"); 4194 validate_contract_bundle(&bundle).expect("validate contract"); 4195 } 4196 4197 #[test] 4198 fn validate_current_canonical_event_boundary() { 4199 let root = workspace_root(); 4200 validate_canonical_event_boundary(&root).expect("validate canonical event boundary"); 4201 } 4202 4203 #[test] 4204 fn canonical_event_boundary_reports_row_drift() { 4205 let root = workspace_root(); 4206 let matrix_path = 4207 resolve_event_boundary_matrix_path_with_override(&root, None).expect("matrix path"); 4208 let raw = fs::read_to_string(&matrix_path).expect("read matrix"); 4209 let drifted = raw.replacen( 4210 "| message | 14 | RadrootsMessage |", 4211 "| message | 999 | RadrootsMessage |", 4212 1, 4213 ); 4214 let temp = temp_root("event_boundary_drift"); 4215 let override_path = temp.join("spec-coverage.md"); 4216 write_file(&override_path, &drifted); 4217 4218 let err = validate_canonical_event_boundary_with_override(&root, Some(override_path)) 4219 .expect_err("message kind drift should fail"); 4220 assert!(err.contains("message kind drift")); 4221 4222 let _ = fs::remove_dir_all(temp); 4223 } 4224 4225 #[test] 4226 fn validate_synthetic_operation_contract_bundle() { 4227 let root = create_synthetic_workspace("operation_contract_bundle"); 4228 add_operation_contract_files(&root); 4229 let bundle = load_contract_bundle(&root).expect("load contract"); 4230 validate_contract_bundle(&bundle).expect("validate contract"); 4231 let _ = fs::remove_dir_all(root); 4232 } 4233 4234 #[test] 4235 fn parses_enum_variants_in_declared_order() { 4236 let source = r#" 4237 pub enum RadrootsCoreUnitDimension { 4238 Count, 4239 Mass, 4240 Volume, 4241 } 4242 "#; 4243 let enum_body = extract_enum_body(source, "RadrootsCoreUnitDimension").expect("enum body"); 4244 let variants = parse_enum_variants(enum_body); 4245 assert_eq!(variants, vec!["Count", "Mass", "Volume"]); 4246 } 4247 4248 #[test] 4249 fn fails_when_enum_order_does_not_match_contract() { 4250 let source = r#" 4251 pub enum RadrootsCoreUnitDimension { 4252 Mass, 4253 Count, 4254 Volume, 4255 } 4256 "#; 4257 let enum_body = extract_enum_body(source, "RadrootsCoreUnitDimension").expect("enum body"); 4258 let variants = parse_enum_variants(enum_body); 4259 let expected = CORE_UNIT_DIMENSION_ORDER 4260 .iter() 4261 .map(|item| (*item).to_string()) 4262 .collect::<Vec<_>>(); 4263 assert_ne!(variants, expected); 4264 } 4265 4266 #[test] 4267 fn coverage_policy_matches_non_simplex_workspace_crates() { 4268 let root = workspace_root(); 4269 let expected_names = 4270 coverage_required_workspace_crates(&root).expect("workspace coverage crates"); 4271 let policy = load_coverage_policy(&root.join("contracts")).expect("coverage policy"); 4272 let required_names = policy 4273 .required_crates() 4274 .expect("required crates") 4275 .into_iter() 4276 .collect::<BTreeSet<_>>(); 4277 assert_eq!(expected_names, required_names); 4278 assert!( 4279 required_names 4280 .iter() 4281 .all(|crate_name| !coverage_policy_excludes_workspace_crate(crate_name)) 4282 ); 4283 } 4284 4285 #[test] 4286 fn coverage_required_workspace_crates_excludes_simplex_packages() { 4287 let root = temp_root("coverage_required_workspace_simplex"); 4288 write_file( 4289 &root.join("Cargo.toml"), 4290 r#"[workspace] 4291 members = ["crates/a", "crates/radroots_simplex_probe", "crates/simplex_probe"] 4292 resolver = "2" 4293 "#, 4294 ); 4295 write_file( 4296 &root.join("crates").join("a").join("Cargo.toml"), 4297 r#"[package] 4298 name = "radroots_a" 4299 version = "0.1.0" 4300 edition = "2024" 4301 "#, 4302 ); 4303 write_file( 4304 &root 4305 .join("crates") 4306 .join("radroots_simplex_probe") 4307 .join("Cargo.toml"), 4308 r#"[package] 4309 name = "radroots_simplex_probe" 4310 version = "0.1.0" 4311 edition = "2024" 4312 "#, 4313 ); 4314 write_file( 4315 &root.join("crates").join("simplex_probe").join("Cargo.toml"), 4316 r#"[package] 4317 name = "simplex_probe" 4318 version = "0.1.0" 4319 edition = "2024" 4320 "#, 4321 ); 4322 4323 let required = 4324 coverage_required_workspace_crates(&root).expect("workspace coverage crates"); 4325 assert_eq!( 4326 required, 4327 ["radroots_a".to_string()] 4328 .into_iter() 4329 .collect::<BTreeSet<_>>() 4330 ); 4331 assert!(coverage_policy_excludes_workspace_crate( 4332 "radroots_simplex_probe" 4333 )); 4334 assert!(coverage_policy_excludes_workspace_crate("simplex_probe")); 4335 assert!(!coverage_policy_excludes_workspace_crate("radroots_a")); 4336 4337 let _ = fs::remove_dir_all(root); 4338 } 4339 4340 #[test] 4341 fn coverage_required_crates_match_policy_required_status() { 4342 let root = workspace_root(); 4343 let contract_root = root.join("contracts"); 4344 let policy = load_coverage_policy(&contract_root).expect("coverage policy"); 4345 let required = CoverageRequiredFile { 4346 required: CoverageRequiredSection { 4347 crates: policy.required_crates().expect("coverage required"), 4348 }, 4349 }; 4350 let required_names = required 4351 .required 4352 .crates 4353 .into_iter() 4354 .collect::<BTreeSet<_>>(); 4355 let policy_required = policy 4356 .required_crates() 4357 .expect("policy required crates") 4358 .into_iter() 4359 .collect::<BTreeSet<_>>(); 4360 assert_eq!(required_names, policy_required); 4361 } 4362 4363 #[test] 4364 fn coverage_policy_required_crates_report_policy_errors() { 4365 let missing_root = temp_root("load_coverage_required_missing_policy"); 4366 let missing_err = 4367 load_coverage_policy(&missing_root).expect_err("missing policy should fail"); 4368 assert!(missing_err.contains("coverage.toml")); 4369 let _ = fs::remove_dir_all(&missing_root); 4370 4371 let duplicate_root = 4372 create_synthetic_workspace("load_coverage_required_duplicate_required"); 4373 let contract_root = duplicate_root.join("contracts"); 4374 let coverage_root = coverage_root(&contract_root); 4375 write_file( 4376 &coverage_root.join("coverage.toml"), 4377 "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_a\"]\n", 4378 ); 4379 let duplicate_err = 4380 load_coverage_policy(&contract_root).expect_err("duplicate required crates"); 4381 assert!(duplicate_err.contains("duplicate crate")); 4382 let _ = fs::remove_dir_all(&duplicate_root); 4383 } 4384 4385 #[test] 4386 fn package_field_configured_accepts_workspace_table() { 4387 let mut package = toml::value::Table::new(); 4388 let mut repository = toml::value::Table::new(); 4389 repository.insert("workspace".to_string(), toml::Value::Boolean(true)); 4390 package.insert("repository".to_string(), toml::Value::Table(repository)); 4391 assert!(package_field_configured(&package, "repository")); 4392 } 4393 4394 #[test] 4395 fn validate_required_coverage_summary_enforces_strict_threshold() { 4396 let root = temp_root("coverage_summary"); 4397 let coverage_dir = root.join("target").join("coverage"); 4398 fs::create_dir_all(&coverage_dir).expect("create coverage dir"); 4399 fs::write( 4400 coverage_dir.join("coverage-refresh.tsv"), 4401 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_core\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", 4402 ) 4403 .expect("write coverage file"); 4404 let required = ["radroots_core".to_string()] 4405 .into_iter() 4406 .collect::<BTreeSet<_>>(); 4407 validate_required_coverage_summary(&root, &required, strict_thresholds()) 4408 .expect("coverage summary"); 4409 4410 fs::write( 4411 coverage_dir.join("coverage-refresh.tsv"), 4412 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_core\tpass\t100.0\t99.9\t100.0\t100.0\tfile\n", 4413 ) 4414 .expect("write function coverage file"); 4415 let func_err = validate_required_coverage_summary(&root, &required, strict_thresholds()) 4416 .expect_err("function coverage below 100"); 4417 assert!(func_err.contains("must satisfy coverage policy")); 4418 4419 fs::write( 4420 coverage_dir.join("coverage-refresh.tsv"), 4421 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_core\tpass\t100.0\t100.0\t99.9\t100.0\tfile\n", 4422 ) 4423 .expect("write branch coverage file"); 4424 let branch_err = validate_required_coverage_summary(&root, &required, strict_thresholds()) 4425 .expect_err("branch coverage below 100"); 4426 assert!(branch_err.contains("must satisfy coverage policy")); 4427 4428 fs::write( 4429 coverage_dir.join("coverage-refresh.tsv"), 4430 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_core\tpass\t100.0\t100.0\tunavailable\t100.0\tfile\n", 4431 ) 4432 .expect("write unavailable branch coverage file"); 4433 let missing_branch_err = 4434 validate_required_coverage_summary(&root, &required, strict_thresholds()) 4435 .expect_err("branch coverage missing under strict policy"); 4436 assert!(missing_branch_err.contains("unavailable")); 4437 4438 fs::write( 4439 coverage_dir.join("coverage-refresh.tsv"), 4440 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_core\tpass\t100.0\t100.0\t100.0\t99.9\tfile\n", 4441 ) 4442 .expect("write region coverage file"); 4443 let region_err = validate_required_coverage_summary(&root, &required, strict_thresholds()) 4444 .expect_err("region coverage below 100"); 4445 assert!(region_err.contains("must satisfy coverage policy")); 4446 let _ = fs::remove_dir_all(&root); 4447 } 4448 4449 #[test] 4450 fn validate_required_coverage_summary_with_policy_honors_scope_override() { 4451 let root = temp_root("coverage_summary_override"); 4452 write_test_coverage_refresh( 4453 &root, 4454 &[ 4455 TestCoverageRefreshRow { 4456 crate_name: "radroots_events_codec", 4457 status: "pass", 4458 thresholds: CoverageThresholds { 4459 fail_under_exec_lines: 100.0, 4460 fail_under_functions: 100.0, 4461 fail_under_regions: 99.946, 4462 fail_under_branches: 100.0, 4463 require_branches: true, 4464 }, 4465 exec: 100.0, 4466 func: 100.0, 4467 branch: Some(100.0), 4468 region: 99.946385, 4469 report_pass: true, 4470 }, 4471 TestCoverageRefreshRow { 4472 crate_name: "radroots_log", 4473 status: "pass", 4474 thresholds: coverage_thresholds(100.0, false), 4475 exec: 100.0, 4476 func: 100.0, 4477 branch: None, 4478 region: 100.0, 4479 report_pass: true, 4480 }, 4481 ], 4482 ); 4483 let policy_dir = root.join("contracts"); 4484 fs::create_dir_all(&policy_dir).expect("create policy dir"); 4485 fs::write( 4486 policy_dir.join("coverage.toml"), 4487 "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[overrides.radroots_events_codec]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 99.946\nfail_under_branches = 100.0\ntemporary = true\nreason = \"publish 0.1.0-alpha.2 temporary coverage override\"\n\n[overrides.radroots_log]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = false\ntemporary = true\nreason = \"branch coverage is not applicable while the crate has no measured branch records\"\n\n[required]\ncrates = [\"radroots_events_codec\", \"radroots_log\"]\n", 4488 ) 4489 .expect("write coverage policy"); 4490 let required = [ 4491 "radroots_events_codec".to_string(), 4492 "radroots_log".to_string(), 4493 ] 4494 .into_iter() 4495 .collect::<BTreeSet<_>>(); 4496 let policy = read_coverage_policy(&policy_dir.join("coverage.toml")) 4497 .expect("parse override coverage policy"); 4498 validate_required_coverage_summary_with_policy(&root, &required, &policy) 4499 .expect("coverage summary should honor override"); 4500 let _ = fs::remove_dir_all(&root); 4501 } 4502 4503 #[test] 4504 fn validate_required_coverage_summary_with_policy_rejects_synthetic_report_path() { 4505 let root = temp_root("coverage_summary_synthetic_report_path"); 4506 write_file( 4507 &root 4508 .join("target") 4509 .join("coverage") 4510 .join("coverage-refresh.tsv"), 4511 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", 4512 ); 4513 let required = ["radroots_a".to_string()] 4514 .into_iter() 4515 .collect::<BTreeSet<_>>(); 4516 let policy_dir = root.join("contracts"); 4517 write_file( 4518 &policy_dir.join("coverage.toml"), 4519 "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", 4520 ); 4521 let policy = 4522 read_coverage_policy(&policy_dir.join("coverage.toml")).expect("parse coverage policy"); 4523 let err = validate_required_coverage_summary_with_policy(&root, &required, &policy) 4524 .expect_err("synthetic report path should fail"); 4525 assert!(err.contains("coverage gate report")); 4526 let _ = fs::remove_dir_all(&root); 4527 } 4528 4529 #[test] 4530 fn validate_required_coverage_summary_with_policy_rejects_stale_gate_report_thresholds() { 4531 let root = temp_root("coverage_summary_stale_gate_report_thresholds"); 4532 let row = TestCoverageRefreshRow { 4533 crate_name: "radroots_a", 4534 status: "pass", 4535 thresholds: coverage_thresholds(90.0, true), 4536 exec: 100.0, 4537 func: 100.0, 4538 branch: Some(100.0), 4539 region: 100.0, 4540 report_pass: true, 4541 }; 4542 write_test_coverage_refresh(&root, &[row]); 4543 let required = ["radroots_a".to_string()] 4544 .into_iter() 4545 .collect::<BTreeSet<_>>(); 4546 let policy_dir = root.join("contracts"); 4547 write_file( 4548 &policy_dir.join("coverage.toml"), 4549 "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", 4550 ); 4551 let policy = 4552 read_coverage_policy(&policy_dir.join("coverage.toml")).expect("parse coverage policy"); 4553 let err = validate_required_coverage_summary_with_policy(&root, &required, &policy) 4554 .expect_err("stale threshold report should fail"); 4555 assert!(err.contains("thresholds do not match policy")); 4556 let _ = fs::remove_dir_all(&root); 4557 } 4558 4559 #[test] 4560 fn validate_required_coverage_summary_with_policy_rejects_row_report_mismatch() { 4561 let root = temp_root("coverage_summary_row_report_mismatch"); 4562 let row = TestCoverageRefreshRow { 4563 crate_name: "radroots_a", 4564 status: "pass", 4565 thresholds: coverage_thresholds(100.0, true), 4566 exec: 99.0, 4567 func: 100.0, 4568 branch: Some(100.0), 4569 region: 100.0, 4570 report_pass: true, 4571 }; 4572 let report_relative = write_test_coverage_gate_report(&root, &row); 4573 write_file( 4574 &root 4575 .join("target") 4576 .join("coverage") 4577 .join("coverage-refresh.tsv"), 4578 &format!( 4579 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100.0\t100.0\t100.0\t100.0\t{report_relative}\n" 4580 ), 4581 ); 4582 let required = ["radroots_a".to_string()] 4583 .into_iter() 4584 .collect::<BTreeSet<_>>(); 4585 let policy_dir = root.join("contracts"); 4586 write_file( 4587 &policy_dir.join("coverage.toml"), 4588 "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", 4589 ); 4590 let policy = 4591 read_coverage_policy(&policy_dir.join("coverage.toml")).expect("parse coverage policy"); 4592 let err = validate_required_coverage_summary_with_policy(&root, &required, &policy) 4593 .expect_err("row and report mismatch should fail"); 4594 assert!(err.contains("does not match coverage gate report")); 4595 let _ = fs::remove_dir_all(&root); 4596 } 4597 4598 #[test] 4599 fn validate_publish_package_metadata_requires_description() { 4600 let root = temp_root("publish_metadata"); 4601 fs::create_dir_all(root.join("crates").join("a")).expect("create crate dir"); 4602 fs::write( 4603 root.join("Cargo.toml"), 4604 r#"[workspace] 4605 members = ["crates/a"] 4606 "#, 4607 ) 4608 .expect("write workspace manifest"); 4609 fs::write( 4610 root.join("crates").join("a").join("Cargo.toml"), 4611 r#"[package] 4612 name = "radroots_a" 4613 version = "0.1.0" 4614 edition = "2024" 4615 repository = { workspace = true } 4616 homepage = { workspace = true } 4617 documentation = "https://docs.rs/radroots_a" 4618 readme = { workspace = true } 4619 "#, 4620 ) 4621 .expect("write package manifest"); 4622 let publish = ["radroots_a".to_string()] 4623 .into_iter() 4624 .collect::<BTreeSet<_>>(); 4625 let err = 4626 validate_publish_package_metadata(&root, &publish).expect_err("missing description"); 4627 assert!(err.contains("package.description")); 4628 let _ = fs::remove_dir_all(&root); 4629 } 4630 4631 #[test] 4632 fn synthetic_workspace_validates_contract_and_release_preflight() { 4633 let root = create_synthetic_workspace("synthetic_valid"); 4634 let bundle = load_contract_bundle(&root).expect("load synthetic bundle"); 4635 validate_contract_bundle(&bundle).expect("validate synthetic bundle"); 4636 validate_release_preflight(&root).expect("validate synthetic preflight"); 4637 let _ = fs::remove_dir_all(root); 4638 } 4639 4640 #[test] 4641 fn helper_functions_cover_error_paths() { 4642 let empty = collect_unique_set(&["".to_string()], "field").expect_err("empty value"); 4643 assert!(empty.contains("field contains an empty crate name")); 4644 let duplicate = collect_unique_set(&["a".to_string(), "a".to_string()], "field") 4645 .expect_err("duplicate value"); 4646 assert!(duplicate.contains("field has duplicate crate a")); 4647 4648 let values = ["b".to_string(), "a".to_string()]; 4649 let set = collect_unique_set(&values, "field").expect("unique values"); 4650 assert_eq!(join_set(&set), "a, b".to_string()); 4651 4652 assert!(package_publish_enabled(None)); 4653 assert!(package_publish_enabled(Some(&PackagePublish::Bool(true)))); 4654 assert!(!package_publish_enabled(Some(&PackagePublish::Bool(false)))); 4655 assert!(package_publish_enabled(Some(&PackagePublish::Registries( 4656 vec!["crates-io".to_string(),] 4657 )))); 4658 assert!(!package_publish_enabled(Some(&PackagePublish::Registries( 4659 Vec::new() 4660 )))); 4661 4662 let mut package = toml::value::Table::new(); 4663 package.insert("description".to_string(), toml::Value::Integer(42)); 4664 assert!(!package_field_configured(&package, "description")); 4665 4666 assert!(!publish_config_is_public(None)); 4667 assert!(!publish_config_is_public(Some(&PackagePublish::Bool(true)))); 4668 assert!(publish_config_is_public(Some(&PackagePublish::Registries( 4669 vec!["crates-io".to_string(),] 4670 )))); 4671 assert!(!publish_config_is_public(Some( 4672 &PackagePublish::Registries(vec!["crates-io".to_string(), "mirror".to_string(),]) 4673 ))); 4674 assert!(!publish_config_is_public(Some( 4675 &PackagePublish::Registries(vec!["mirror".to_string(),]) 4676 ))); 4677 4678 assert!(!publish_config_is_non_public(None)); 4679 assert!(!publish_config_is_non_public(Some(&PackagePublish::Bool( 4680 true 4681 )))); 4682 assert!(publish_config_is_non_public(Some(&PackagePublish::Bool( 4683 false 4684 )))); 4685 assert!(!publish_config_is_non_public(Some( 4686 &PackagePublish::Registries(vec!["crates-io".to_string(),]) 4687 ))); 4688 } 4689 4690 #[test] 4691 fn release_contract_helpers_cover_classification_and_env_override_paths() { 4692 let release = ReleaseSection { 4693 version: "1.0.0".to_string(), 4694 }; 4695 let empty_order = ReleaseCrateSet { crates: Vec::new() }; 4696 4697 let legacy = ReleaseContractFile { 4698 release: ReleaseSection { 4699 version: release.version.clone(), 4700 }, 4701 classification: ReleaseClassification::default(), 4702 publish: Some(ReleaseCrateSet { 4703 crates: vec!["radroots_public".to_string()], 4704 }), 4705 internal: Some(ReleaseCrateSet { 4706 crates: vec!["radroots_internal".to_string()], 4707 }), 4708 publish_order: ReleaseCrateSet { 4709 crates: empty_order.crates.clone(), 4710 }, 4711 }; 4712 assert!(!legacy.uses_classification()); 4713 assert_eq!(legacy.public_crates(), vec!["radroots_public".to_string()]); 4714 assert_eq!( 4715 legacy.internal_crates(), 4716 vec!["radroots_internal".to_string()] 4717 ); 4718 4719 let empty_legacy = ReleaseContractFile { 4720 release: ReleaseSection { 4721 version: release.version.clone(), 4722 }, 4723 classification: ReleaseClassification::default(), 4724 publish: None, 4725 internal: None, 4726 publish_order: ReleaseCrateSet { 4727 crates: empty_order.crates.clone(), 4728 }, 4729 }; 4730 assert!(!empty_legacy.uses_classification()); 4731 assert_eq!(empty_legacy.public_crates(), Vec::<String>::new()); 4732 assert_eq!(empty_legacy.internal_crates(), Vec::<String>::new()); 4733 4734 let internal = ReleaseContractFile { 4735 release: ReleaseSection { 4736 version: release.version.clone(), 4737 }, 4738 classification: ReleaseClassification { 4739 internal: vec!["radroots_internal_only".to_string()], 4740 ..ReleaseClassification::default() 4741 }, 4742 publish: None, 4743 internal: None, 4744 publish_order: ReleaseCrateSet { 4745 crates: empty_order.crates.clone(), 4746 }, 4747 }; 4748 assert!(internal.uses_classification()); 4749 4750 let deferred = ReleaseContractFile { 4751 release: ReleaseSection { 4752 version: release.version.clone(), 4753 }, 4754 classification: ReleaseClassification { 4755 deferred: vec!["radroots_deferred".to_string()], 4756 ..ReleaseClassification::default() 4757 }, 4758 publish: None, 4759 internal: None, 4760 publish_order: ReleaseCrateSet { 4761 crates: empty_order.crates.clone(), 4762 }, 4763 }; 4764 assert!(deferred.uses_classification()); 4765 assert_eq!( 4766 deferred.deferred_crates(), 4767 vec!["radroots_deferred".to_string()] 4768 ); 4769 4770 let retired = ReleaseContractFile { 4771 release: ReleaseSection { 4772 version: release.version.clone(), 4773 }, 4774 classification: ReleaseClassification { 4775 retired: vec!["radroots_retired".to_string()], 4776 ..ReleaseClassification::default() 4777 }, 4778 publish: None, 4779 internal: None, 4780 publish_order: ReleaseCrateSet { 4781 crates: empty_order.crates.clone(), 4782 }, 4783 }; 4784 assert!(retired.uses_classification()); 4785 assert_eq!( 4786 retired.retired_crates(), 4787 vec!["radroots_retired".to_string()] 4788 ); 4789 4790 let yank_only = ReleaseContractFile { 4791 release, 4792 classification: ReleaseClassification { 4793 yank_only: vec!["radroots_yank_only".to_string()], 4794 ..ReleaseClassification::default() 4795 }, 4796 publish: None, 4797 internal: None, 4798 publish_order: empty_order, 4799 }; 4800 assert!(yank_only.uses_classification()); 4801 assert_eq!( 4802 yank_only.yank_only_crates(), 4803 vec!["radroots_yank_only".to_string()] 4804 ); 4805 4806 let root = create_synthetic_workspace("release_contract_env_override"); 4807 let policy_path = root_release_policy_path(&root); 4808 let resolved = 4809 resolve_release_contract_path_with_override(&root, Some(policy_path.clone())) 4810 .expect("existing override policy should resolve"); 4811 assert_eq!(resolved, Some(policy_path)); 4812 4813 let missing_policy = root.join("missing-release-policy.toml"); 4814 let err = resolve_release_contract_path_with_override(&root, Some(missing_policy.clone())) 4815 .expect_err("missing env policy should fail"); 4816 assert!(err.contains(RELEASE_POLICY_ENV)); 4817 assert!(err.contains("missing release policy file")); 4818 assert!(err.contains(&missing_policy.display().to_string())); 4819 4820 let _ = fs::remove_dir_all(&root); 4821 } 4822 4823 #[test] 4824 fn workspace_package_manifests_reject_duplicate_package_names() { 4825 let root = temp_root("workspace_manifest_duplicates"); 4826 write_file( 4827 &root.join("Cargo.toml"), 4828 r#"[workspace] 4829 members = ["crates/a", "crates/b"] 4830 "#, 4831 ); 4832 let package_manifest = 4833 "[package]\nname = \"duplicate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"; 4834 write_file( 4835 &root.join("crates").join("a").join("Cargo.toml"), 4836 package_manifest, 4837 ); 4838 write_file( 4839 &root.join("crates").join("b").join("Cargo.toml"), 4840 package_manifest, 4841 ); 4842 let err = workspace_package_manifests(&root) 4843 .expect_err("duplicate package names in manifest map"); 4844 assert!(err.contains("duplicate workspace package name in manifest map")); 4845 let _ = fs::remove_dir_all(root); 4846 } 4847 4848 #[test] 4849 fn coverage_refresh_parsing_and_summary_errors_are_reported() { 4850 let root = temp_root("coverage_refresh_errors"); 4851 let coverage_dir = root.join("target").join("coverage"); 4852 fs::create_dir_all(&coverage_dir).expect("create coverage dir"); 4853 4854 write_file( 4855 &coverage_dir.join("coverage-refresh.tsv"), 4856 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nbad-row\n", 4857 ); 4858 let bad_row = load_coverage_refresh_rows(&root).expect_err("invalid coverage row"); 4859 assert!(bad_row.contains("at least 7 columns")); 4860 4861 write_file( 4862 &coverage_dir.join("coverage-refresh.tsv"), 4863 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\tnot-a-number\t100\t100\t100\tfile\n", 4864 ); 4865 let bad_percent = load_coverage_refresh_rows(&root).expect_err("invalid coverage percent"); 4866 assert!(bad_percent.contains("parse exec")); 4867 4868 write_file( 4869 &coverage_dir.join("coverage-refresh.tsv"), 4870 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100\t100\t100\tnot-a-number\tfile\n", 4871 ); 4872 let bad_region = 4873 load_coverage_refresh_rows(&root).expect_err("invalid region coverage percent"); 4874 assert!(bad_region.contains("parse region")); 4875 4876 write_file( 4877 &coverage_dir.join("coverage-refresh.tsv"), 4878 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100\t100\t100\t100\tfile\nradroots_a\tpass\t100\t100\t100\t100\tfile\n", 4879 ); 4880 let duplicate_row = load_coverage_refresh_rows(&root).expect_err("duplicate coverage row"); 4881 assert!(duplicate_row.contains("duplicate coverage row")); 4882 4883 write_file( 4884 &coverage_dir.join("coverage-refresh.tsv"), 4885 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tfail\t100\t100\t100\t100\tfile\n", 4886 ); 4887 let required = ["radroots_a".to_string()] 4888 .into_iter() 4889 .collect::<BTreeSet<_>>(); 4890 let non_pass = validate_required_coverage_summary(&root, &required, strict_thresholds()) 4891 .expect_err("non-pass status"); 4892 assert!(non_pass.contains("non-pass status")); 4893 4894 write_file( 4895 &coverage_dir.join("coverage-refresh.tsv"), 4896 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t99.9\t100\t100\t100\tfile\n", 4897 ); 4898 let below_100 = validate_required_coverage_summary(&root, &required, strict_thresholds()) 4899 .expect_err("coverage below 100"); 4900 assert!(below_100.contains("must satisfy coverage policy")); 4901 4902 let missing = ["missing".to_string()].into_iter().collect::<BTreeSet<_>>(); 4903 let missing_err = validate_required_coverage_summary(&root, &missing, strict_thresholds()) 4904 .expect_err("missing required row"); 4905 assert!(missing_err.contains("missing from coverage-refresh.tsv")); 4906 4907 let _ = fs::remove_dir_all(root); 4908 } 4909 4910 #[test] 4911 fn enum_extract_and_parse_error_paths_are_reported() { 4912 let missing = extract_enum_body("pub struct X;", "RadrootsCoreUnitDimension") 4913 .expect_err("missing enum"); 4914 assert!(missing.contains("missing enum")); 4915 4916 let missing_brace = extract_enum_body( 4917 "pub enum RadrootsCoreUnitDimension", 4918 "RadrootsCoreUnitDimension", 4919 ) 4920 .expect_err("missing opening brace"); 4921 assert!(missing_brace.contains("missing opening brace")); 4922 4923 let missing_close = extract_enum_body( 4924 "pub enum RadrootsCoreUnitDimension { Count, Mass", 4925 "RadrootsCoreUnitDimension", 4926 ) 4927 .expect_err("missing closing brace"); 4928 assert!(missing_close.contains("missing closing brace")); 4929 4930 let variants = parse_enum_variants( 4931 r#" 4932 , 4933 = 1, 4934 // skip 4935 #![cfg(test)] 4936 Count, 4937 "#, 4938 ); 4939 assert_eq!(variants, vec!["Count".to_string()]); 4940 4941 let nested = extract_enum_body( 4942 "pub enum RadrootsCoreUnitDimension { Count = { 1 }, Mass = 2 }", 4943 "RadrootsCoreUnitDimension", 4944 ) 4945 .expect("nested braces in enum body"); 4946 assert!(nested.contains("Count")); 4947 } 4948 4949 #[test] 4950 fn coverage_policy_parity_reports_contract_errors() { 4951 let root = create_synthetic_workspace("coverage_policy_errors"); 4952 let contract_root = root.join("contracts"); 4953 let coverage_root = coverage_root(&contract_root); 4954 4955 write_file( 4956 &coverage_root.join("coverage.toml"), 4957 r#"[gate] 4958 fail_under_exec_lines = 100.0 4959 fail_under_functions = 100.0 4960 fail_under_regions = 100.0 4961 fail_under_branches = 100.0 4962 require_branches = true 4963 4964 [required] 4965 crates = [] 4966 "#, 4967 ); 4968 let empty_required = 4969 validate_coverage_policy_parity(&root, &contract_root).expect_err("empty required"); 4970 assert!(empty_required.contains("required crates list must not be empty")); 4971 4972 write_file( 4973 &coverage_root.join("coverage.toml"), 4974 r#"[gate] 4975 fail_under_exec_lines = 97.0 4976 fail_under_functions = 100.0 4977 fail_under_regions = 100.0 4978 fail_under_branches = 100.0 4979 require_branches = true 4980 4981 [required] 4982 crates = ["radroots_a", "radroots_b"] 4983 "#, 4984 ); 4985 let invalid_gate = validate_coverage_policy_parity(&root, &contract_root) 4986 .expect_err("invalid policy thresholds"); 4987 assert!(invalid_gate.contains("100/100/100/100")); 4988 4989 write_file( 4990 &coverage_root.join("coverage.toml"), 4991 r#"[gate] 4992 fail_under_exec_lines = 100.0 4993 fail_under_functions = 97.0 4994 fail_under_regions = 100.0 4995 fail_under_branches = 100.0 4996 require_branches = true 4997 4998 [required] 4999 crates = ["radroots_a", "radroots_b"] 5000 "#, 5001 ); 5002 let invalid_functions = validate_coverage_policy_parity(&root, &contract_root) 5003 .expect_err("invalid function threshold"); 5004 assert!(invalid_functions.contains("100/100/100/100")); 5005 5006 write_file( 5007 &coverage_root.join("coverage.toml"), 5008 r#"[gate] 5009 fail_under_exec_lines = 100.0 5010 fail_under_functions = 100.0 5011 fail_under_regions = 97.0 5012 fail_under_branches = 100.0 5013 require_branches = true 5014 5015 [required] 5016 crates = ["radroots_a", "radroots_b"] 5017 "#, 5018 ); 5019 let invalid_regions = validate_coverage_policy_parity(&root, &contract_root) 5020 .expect_err("invalid region threshold"); 5021 assert!(invalid_regions.contains("100/100/100/100")); 5022 5023 write_file( 5024 &coverage_root.join("coverage.toml"), 5025 r#"[gate] 5026 fail_under_exec_lines = 100.0 5027 fail_under_functions = 100.0 5028 fail_under_regions = 100.0 5029 fail_under_branches = 97.0 5030 require_branches = true 5031 5032 [required] 5033 crates = ["radroots_a", "radroots_b"] 5034 "#, 5035 ); 5036 let invalid_branches = validate_coverage_policy_parity(&root, &contract_root) 5037 .expect_err("invalid branch threshold"); 5038 assert!(invalid_branches.contains("100/100/100/100")); 5039 5040 write_file( 5041 &coverage_root.join("coverage.toml"), 5042 r#"[gate] 5043 fail_under_exec_lines = 100.0 5044 fail_under_functions = 100.0 5045 fail_under_regions = 100.0 5046 fail_under_branches = 100.0 5047 require_branches = true 5048 5049 [required] 5050 crates = ["radroots_a", "radroots_a"] 5051 "#, 5052 ); 5053 let duplicate_required = validate_coverage_policy_parity(&root, &contract_root) 5054 .expect_err("duplicate required crate"); 5055 assert!(duplicate_required.contains("duplicate crate")); 5056 5057 write_file( 5058 &coverage_root.join("coverage.toml"), 5059 r#"[gate] 5060 fail_under_exec_lines = 100.0 5061 fail_under_functions = 100.0 5062 fail_under_regions = 100.0 5063 fail_under_branches = 100.0 5064 require_branches = false 5065 5066 [required] 5067 crates = ["radroots_a", "radroots_b"] 5068 "#, 5069 ); 5070 let branches_optional = validate_coverage_policy_parity(&root, &contract_root) 5071 .expect_err("branches must be required"); 5072 assert!(branches_optional.contains("required branches")); 5073 5074 write_file( 5075 &coverage_root.join("coverage.toml"), 5076 r#"[gate] 5077 fail_under_exec_lines = 100.0 5078 fail_under_functions = 100.0 5079 fail_under_regions = 100.0 5080 fail_under_branches = 100.0 5081 require_branches = true 5082 5083 [required] 5084 crates = ["radroots_b"] 5085 "#, 5086 ); 5087 let missing_workspace = validate_coverage_policy_parity(&root, &contract_root) 5088 .expect_err("missing workspace crate in policy"); 5089 assert!(missing_workspace.contains("missing workspace crates")); 5090 5091 write_file( 5092 &coverage_root.join("coverage.toml"), 5093 r#"[gate] 5094 fail_under_exec_lines = 100.0 5095 fail_under_functions = 100.0 5096 fail_under_regions = 100.0 5097 fail_under_branches = 100.0 5098 require_branches = true 5099 5100 [required] 5101 crates = ["unknown"] 5102 "#, 5103 ); 5104 let required_unknown = validate_coverage_policy_parity(&root, &contract_root) 5105 .expect_err("unknown required crate"); 5106 assert!(required_unknown.contains("includes excluded or unknown crates")); 5107 5108 let _ = fs::remove_dir_all(root); 5109 } 5110 5111 #[test] 5112 fn release_publish_policy_reports_contract_errors() { 5113 let root = create_synthetic_workspace("release_policy_errors"); 5114 let contract_root = root.join("contracts"); 5115 let release_policy_path = root_release_policy_path(&root); 5116 5117 write_file( 5118 &release_policy_path, 5119 r#"[release] 5120 version = "" 5121 5122 [publish] 5123 crates = ["radroots_a"] 5124 5125 [internal] 5126 crates = ["radroots_b"] 5127 5128 [publish_order] 5129 crates = ["radroots_a"] 5130 "#, 5131 ); 5132 let empty_version = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5133 .expect_err("empty release version"); 5134 assert!(empty_version.contains("must not be empty")); 5135 5136 write_file( 5137 &release_policy_path, 5138 r#"[release] 5139 version = "2.0.0" 5140 5141 [publish] 5142 crates = ["radroots_a"] 5143 5144 [internal] 5145 crates = ["radroots_b"] 5146 5147 [publish_order] 5148 crates = ["radroots_a"] 5149 "#, 5150 ); 5151 let version_mismatch = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5152 .expect_err("release version mismatch"); 5153 assert!(version_mismatch.contains("must match contract version")); 5154 5155 write_file( 5156 &release_policy_path, 5157 r#"[release] 5158 version = "1.0.0" 5159 5160 [publish] 5161 crates = ["radroots_a"] 5162 5163 [internal] 5164 crates = ["radroots_a"] 5165 5166 [publish_order] 5167 crates = ["radroots_a"] 5168 "#, 5169 ); 5170 let overlap = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5171 .expect_err("publish/internal overlap"); 5172 assert!(overlap.contains("overlap is not allowed")); 5173 5174 write_file( 5175 &release_policy_path, 5176 r#"[release] 5177 version = "1.0.0" 5178 5179 [publish] 5180 crates = ["radroots_a"] 5181 5182 [internal] 5183 crates = [] 5184 5185 [publish_order] 5186 crates = ["radroots_a"] 5187 "#, 5188 ); 5189 let missing_workspace = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5190 .expect_err("missing workspace crate"); 5191 assert!(missing_workspace.contains("missing workspace crates")); 5192 5193 write_file( 5194 &release_policy_path, 5195 r#"[release] 5196 version = "1.0.0" 5197 5198 [publish] 5199 crates = ["radroots_a"] 5200 5201 [internal] 5202 crates = ["radroots_b"] 5203 5204 [publish_order] 5205 crates = [] 5206 "#, 5207 ); 5208 let missing_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5209 .expect_err("missing publish order entries"); 5210 assert!(missing_publish_order.contains("missing publish crates")); 5211 5212 write_file( 5213 &release_policy_path, 5214 r#"[release] 5215 version = "1.0.0" 5216 5217 [publish] 5218 crates = ["radroots_a"] 5219 5220 [internal] 5221 crates = ["radroots_b"] 5222 5223 [publish_order] 5224 crates = ["radroots_a", "radroots_b"] 5225 "#, 5226 ); 5227 let extra_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5228 .expect_err("extra publish order entries"); 5229 assert!(extra_publish_order.contains("non-publish crates")); 5230 5231 write_file( 5232 &root.join("crates").join("a").join("Cargo.toml"), 5233 r#"[package] 5234 name = "radroots_a" 5235 publish = ["crates-io"] 5236 version = "0.1.0" 5237 edition = "2024" 5238 description = "crate a" 5239 repository = "https://example.com/a" 5240 homepage = "https://example.com/a" 5241 documentation = "https://docs.example.com/a" 5242 readme = "README" 5243 5244 [dependencies] 5245 radroots_b = { path = "../b" } 5246 "#, 5247 ); 5248 write_file( 5249 &root.join("crates").join("b").join("Cargo.toml"), 5250 r#"[package] 5251 name = "radroots_b" 5252 version = "0.1.0" 5253 edition = "2024" 5254 description = "crate b" 5255 repository = "https://example.com/b" 5256 homepage = "https://example.com/b" 5257 documentation = "https://docs.example.com/b" 5258 readme = "README" 5259 "#, 5260 ); 5261 write_file( 5262 &release_policy_path, 5263 r#"[release] 5264 version = "1.0.0" 5265 5266 [publish] 5267 crates = ["radroots_a", "radroots_b"] 5268 5269 [internal] 5270 crates = [] 5271 5272 [publish_order] 5273 crates = ["radroots_a", "radroots_b"] 5274 "#, 5275 ); 5276 let dependency_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5277 .expect_err("dependency order violation"); 5278 assert!(dependency_order.contains("must place dependency")); 5279 5280 write_file( 5281 &release_policy_path, 5282 r#"[release] 5283 version = "1.0.0" 5284 5285 [publish] 5286 crates = ["radroots_a"] 5287 5288 [internal] 5289 crates = ["radroots_b"] 5290 5291 [publish_order] 5292 crates = ["radroots_a"] 5293 "#, 5294 ); 5295 write_file( 5296 &root.join("crates").join("b").join("Cargo.toml"), 5297 r#"[package] 5298 name = "radroots_b" 5299 version = "0.1.0" 5300 edition = "2024" 5301 publish = false 5302 "#, 5303 ); 5304 validate_release_publish_policy(&root, &contract_root, "1.0.0") 5305 .expect("internal dependency should be ignored in publish ordering"); 5306 5307 write_file( 5308 &root.join("crates").join("a").join("Cargo.toml"), 5309 r#"[package] 5310 name = "radroots_a" 5311 version = "0.1.0" 5312 edition = "2024" 5313 publish = false 5314 "#, 5315 ); 5316 write_file( 5317 &release_policy_path, 5318 r#"[release] 5319 version = "1.0.0" 5320 5321 [publish] 5322 crates = ["radroots_a"] 5323 5324 [internal] 5325 crates = ["radroots_b"] 5326 5327 [publish_order] 5328 crates = ["radroots_a"] 5329 "#, 5330 ); 5331 let publish_flag = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5332 .expect_err("publish crate must be publishable"); 5333 assert!(publish_flag.contains("must set publish = [\"crates-io\"]")); 5334 5335 write_file( 5336 &root.join("crates").join("a").join("Cargo.toml"), 5337 r#"[package] 5338 name = "radroots_a" 5339 publish = ["crates-io"] 5340 version = "0.1.0" 5341 edition = "2024" 5342 description = "crate a" 5343 repository = "https://example.com/a" 5344 homepage = "https://example.com/a" 5345 documentation = "https://docs.example.com/a" 5346 readme = "README" 5347 "#, 5348 ); 5349 write_file( 5350 &root.join("crates").join("b").join("Cargo.toml"), 5351 r#"[package] 5352 name = "radroots_b" 5353 version = "0.1.0" 5354 edition = "2024" 5355 "#, 5356 ); 5357 let internal_flag = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5358 .expect_err("internal crate must be non-publishable"); 5359 assert!(internal_flag.contains("non-public crate")); 5360 5361 let _ = fs::remove_dir_all(root); 5362 } 5363 5364 #[test] 5365 fn validate_contract_bundle_reports_required_field_errors() { 5366 let root = create_synthetic_workspace("contract_bundle_errors"); 5367 5368 let assert_bundle_error = |expected: &str, mutator: fn(&mut ContractBundle)| { 5369 let mut bundle = load_contract_bundle(&root).expect("load bundle"); 5370 mutator(&mut bundle); 5371 let err = match validate_contract_bundle(&bundle) { 5372 Ok(()) => panic!("expected bundle validation error: {expected}"), 5373 Err(err) => err, 5374 }; 5375 assert!(err.contains(expected), "expected `{expected}` in `{err}`"); 5376 }; 5377 5378 assert_bundle_error("contract name is required", |bundle| { 5379 bundle.manifest.contract.name.clear(); 5380 }); 5381 assert_bundle_error("contract version is required", |bundle| { 5382 bundle.manifest.contract.version.clear(); 5383 }); 5384 assert_bundle_error("contract source is required", |bundle| { 5385 bundle.manifest.contract.source.clear(); 5386 }); 5387 assert_bundle_error("surface.model_crates must not be empty", |bundle| { 5388 bundle.manifest.surface.model_crates.clear(); 5389 }); 5390 assert_bundle_error("surface.algorithm_crates must not be empty", |bundle| { 5391 bundle.manifest.surface.algorithm_crates.clear(); 5392 }); 5393 assert_bundle_error( 5394 "surface.internal_replica_crates.storage must be a crate identifier", 5395 |bundle| { 5396 bundle.manifest.surface.internal_replica_crates = Some(InternalReplicaCrates { 5397 schema: "radroots_replica_db_schema".to_string(), 5398 storage: "crates/replica_db".to_string(), 5399 sync: "radroots_replica_sync".to_string(), 5400 }); 5401 }, 5402 ); 5403 assert_bundle_error("version.contract.version is required", |bundle| { 5404 bundle.version.contract.version.clear(); 5405 }); 5406 assert_bundle_error("version.contract.stability is required", |bundle| { 5407 bundle.version.contract.stability.clear(); 5408 }); 5409 assert_bundle_error("version.semver rules must all be non-empty", |bundle| { 5410 bundle.version.semver.major_on.clear(); 5411 }); 5412 assert_bundle_error("version.semver rules must all be non-empty", |bundle| { 5413 bundle.version.semver.minor_on.clear(); 5414 }); 5415 assert_bundle_error("version.semver rules must all be non-empty", |bundle| { 5416 bundle.version.semver.patch_on.clear(); 5417 }); 5418 assert_bundle_error( 5419 "compatibility.requires_conformance_pass must be true", 5420 |bundle| { 5421 bundle.version.compatibility.requires_conformance_pass = false; 5422 }, 5423 ); 5424 assert_bundle_error( 5425 "compatibility.requires_contract_manifest_diff must be true", 5426 |bundle| { 5427 bundle.version.compatibility.requires_contract_manifest_diff = false; 5428 }, 5429 ); 5430 assert_bundle_error( 5431 "compatibility.requires_release_notes must be true", 5432 |bundle| { 5433 bundle.version.compatibility.requires_release_notes = false; 5434 }, 5435 ); 5436 assert_bundle_error("contract policy flags must all be true", |bundle| { 5437 bundle.manifest.policy.exclude_internal_workspace_crates = false; 5438 }); 5439 assert_bundle_error("contract policy flags must all be true", |bundle| { 5440 bundle.manifest.policy.require_reproducible_exports = false; 5441 }); 5442 assert_bundle_error("contract policy flags must all be true", |bundle| { 5443 bundle.manifest.policy.require_conformance_vectors = false; 5444 }); 5445 assert_bundle_error("contract replica policy flags must all be true", |bundle| { 5446 bundle.manifest.policy.replica = Some(ReplicaPolicy { 5447 forbid_legacy_alias_identifiers: false, 5448 require_transport_agnostic_sync_contract: true, 5449 require_deterministic_emit_ingest: true, 5450 }); 5451 }); 5452 5453 let _ = fs::remove_dir_all(root); 5454 } 5455 5456 #[test] 5457 fn load_contract_bundle_rejects_stale_consumer_sdk_tables() { 5458 let stale_manifest_root = create_synthetic_workspace("stale_manifest_consumer_sdk"); 5459 let manifest_path = stale_manifest_root.join("contracts").join("manifest.toml"); 5460 let mut manifest = fs::read_to_string(&manifest_path).expect("manifest"); 5461 manifest.push_str( 5462 r#" 5463 [consumer_sdk] 5464 rust_package = "radroots_sdk" 5465 "#, 5466 ); 5467 write_file(&manifest_path, &manifest); 5468 let manifest_err = 5469 load_contract_bundle(&stale_manifest_root).expect_err("stale manifest table"); 5470 assert!(manifest_err.contains("manifest.toml")); 5471 assert!(manifest_err.contains("consumer_sdk")); 5472 let _ = fs::remove_dir_all(stale_manifest_root); 5473 5474 let stale_operations_root = create_synthetic_workspace("stale_operations_consumer_sdk"); 5475 add_operation_contract_files(&stale_operations_root); 5476 let operations_path = stale_operations_root 5477 .join("contracts") 5478 .join("operations.toml"); 5479 let mut operations = fs::read_to_string(&operations_path).expect("operations"); 5480 operations.push_str( 5481 r#" 5482 [consumer_sdk] 5483 rust_package = "radroots_sdk" 5484 "#, 5485 ); 5486 write_file(&operations_path, &operations); 5487 let operations_err = 5488 load_contract_bundle(&stale_operations_root).expect_err("stale operations table"); 5489 assert!(operations_err.contains("operations.toml")); 5490 assert!(operations_err.contains("consumer_sdk")); 5491 let _ = fs::remove_dir_all(stale_operations_root); 5492 } 5493 5494 #[test] 5495 fn load_contract_bundle_rejects_legacy_contract_roots() { 5496 let stale_spec_root = create_synthetic_workspace("stale_spec_root"); 5497 fs::create_dir_all(stale_spec_root.join("spec")).expect("create spec root"); 5498 let spec_err = load_contract_bundle(&stale_spec_root).expect_err("stale spec root"); 5499 assert!(spec_err.contains("legacy contract root")); 5500 assert!(spec_err.contains("spec")); 5501 let _ = fs::remove_dir_all(stale_spec_root); 5502 5503 let stale_policy_root = create_synthetic_workspace("stale_policy_root"); 5504 fs::create_dir_all(stale_policy_root.join("policy")).expect("create policy root"); 5505 let policy_err = load_contract_bundle(&stale_policy_root).expect_err("stale policy root"); 5506 assert!(policy_err.contains("legacy contract root")); 5507 assert!(policy_err.contains("policy")); 5508 let _ = fs::remove_dir_all(stale_policy_root); 5509 } 5510 5511 #[test] 5512 fn validate_contract_bundle_reports_operation_contract_errors() { 5513 let root = create_synthetic_workspace("operation_contract_bundle_errors"); 5514 add_operation_contract_files(&root); 5515 5516 let assert_bundle_error = |expected: &str, mutator: fn(&mut ContractBundle)| { 5517 let mut bundle = load_contract_bundle(&root).expect("load bundle"); 5518 mutator(&mut bundle); 5519 let err = validate_contract_bundle(&bundle).expect_err("bundle validation error"); 5520 assert!(err.contains(expected), "expected `{expected}` in `{err}`"); 5521 }; 5522 5523 assert_bundle_error("public.domains must not be empty", |bundle| { 5524 bundle 5525 .operations_manifest 5526 .as_mut() 5527 .expect("operations manifest") 5528 .public 5529 .domains 5530 .clear(); 5531 }); 5532 let _ = fs::remove_dir_all(root); 5533 } 5534 5535 #[test] 5536 fn validate_contract_bundle_requires_real_conformance_assets() { 5537 let missing_schema_root = create_synthetic_workspace("operation_contract_missing_schema"); 5538 add_operation_contract_files(&missing_schema_root); 5539 let _ = fs::remove_file(conformance_schema_path(&missing_schema_root)); 5540 let bundle = load_contract_bundle(&missing_schema_root).expect("load bundle"); 5541 let err = validate_contract_bundle(&bundle).expect_err("missing schema should fail"); 5542 assert!(err.contains("vector.schema.json")); 5543 let _ = fs::remove_dir_all(&missing_schema_root); 5544 5545 let invalid_vector_root = create_synthetic_workspace("operation_contract_invalid_vector"); 5546 add_operation_contract_files(&invalid_vector_root); 5547 write_file( 5548 &invalid_vector_root 5549 .join("contracts") 5550 .join("conformance") 5551 .join("vectors") 5552 .join("profile") 5553 .join("build_draft.v1.json"), 5554 r#"{ 5555 "suite": "profile", 5556 "contract_version": "1.0.0", 5557 "vectors": [ 5558 { 5559 "id": "profile_build_draft_minimal_001", 5560 "kind": "profile.build_draft", 5561 "input": {} 5562 } 5563 ] 5564 } 5565 "#, 5566 ); 5567 let bundle = load_contract_bundle(&invalid_vector_root).expect("load bundle"); 5568 let err = validate_contract_bundle(&bundle).expect_err("invalid vector should fail"); 5569 assert!(err.contains("build_draft.v1.json")); 5570 assert!(err.contains("parse")); 5571 let _ = fs::remove_dir_all(&invalid_vector_root); 5572 5573 let root = create_synthetic_workspace("operation_contract_vector_path"); 5574 add_operation_contract_files(&root); 5575 let mut bundle = load_contract_bundle(&root).expect("load bundle"); 5576 bundle 5577 .operations_manifest 5578 .as_mut() 5579 .expect("operations manifest") 5580 .operations 5581 .get_mut("profile_build_draft") 5582 .expect("profile operation") 5583 .conformance 5584 .vector = "conformance/vectors/profile/build_draft.v1.json".to_string(); 5585 let err = validate_contract_bundle(&bundle).expect_err("legacy path should fail"); 5586 assert!(err.contains("must live under contracts/conformance/")); 5587 let _ = fs::remove_dir_all(root); 5588 } 5589 5590 #[test] 5591 fn parse_toml_and_publish_flags_report_failures() { 5592 let missing = temp_root("parse_toml_missing"); 5593 let read_err = 5594 parse_toml::<WorkspaceCargoManifest>(&missing.join("Cargo.toml")).expect_err("missing"); 5595 assert!(read_err.contains("read")); 5596 let _ = fs::remove_dir_all(&missing); 5597 5598 let invalid = temp_root("parse_toml_invalid"); 5599 write_file(&invalid.join("Cargo.toml"), "[workspace]\nmembers = ["); 5600 let parse_err = parse_toml::<WorkspaceCargoManifest>(&invalid.join("Cargo.toml")) 5601 .expect_err("invalid manifest"); 5602 assert!(parse_err.contains("parse")); 5603 let _ = fs::remove_dir_all(&invalid); 5604 5605 let contract_manifest_missing = temp_root("parse_contract_manifest_missing"); 5606 let contract_manifest_read_err = 5607 parse_toml::<ContractManifest>(&contract_manifest_missing.join("manifest.toml")) 5608 .expect_err("missing contract manifest"); 5609 assert!(contract_manifest_read_err.contains("read")); 5610 let _ = fs::remove_dir_all(&contract_manifest_missing); 5611 5612 let contract_manifest_invalid = temp_root("parse_contract_manifest_invalid"); 5613 write_file( 5614 &contract_manifest_invalid.join("manifest.toml"), 5615 "[contract", 5616 ); 5617 let contract_manifest_parse_err = 5618 parse_toml::<ContractManifest>(&contract_manifest_invalid.join("manifest.toml")) 5619 .expect_err("invalid contract manifest"); 5620 assert!(contract_manifest_parse_err.contains("parse")); 5621 let _ = fs::remove_dir_all(&contract_manifest_invalid); 5622 5623 let version_missing = temp_root("parse_version_policy_missing"); 5624 let version_read_err = parse_toml::<VersionPolicy>(&version_missing.join("version.toml")) 5625 .expect_err("missing version policy"); 5626 assert!(version_read_err.contains("read")); 5627 let _ = fs::remove_dir_all(&version_missing); 5628 5629 let version_invalid = temp_root("parse_version_policy_invalid"); 5630 write_file(&version_invalid.join("version.toml"), "[version"); 5631 let version_parse_err = parse_toml::<VersionPolicy>(&version_invalid.join("version.toml")) 5632 .expect_err("invalid version policy"); 5633 assert!(version_parse_err.contains("parse")); 5634 let _ = fs::remove_dir_all(&version_invalid); 5635 5636 let release_missing = temp_root("parse_release_contract_missing"); 5637 let release_read_err = 5638 parse_toml::<ReleaseContractFile>(&release_missing.join("publish-set.toml")) 5639 .expect_err("missing release contract"); 5640 assert!(release_read_err.contains("read")); 5641 let _ = fs::remove_dir_all(&release_missing); 5642 5643 let release_invalid = temp_root("parse_release_contract_invalid"); 5644 write_file(&release_invalid.join("publish-set.toml"), "[release"); 5645 let release_parse_err = 5646 parse_toml::<ReleaseContractFile>(&release_invalid.join("publish-set.toml")) 5647 .expect_err("invalid release contract"); 5648 assert!(release_parse_err.contains("parse")); 5649 let _ = fs::remove_dir_all(&release_invalid); 5650 5651 let operations_missing = temp_root("parse_operations_manifest_missing"); 5652 let operations_read_err = 5653 parse_toml::<OperationsContractManifest>(&operations_missing.join("operations.toml")) 5654 .expect_err("missing operations manifest"); 5655 assert!(operations_read_err.contains("read")); 5656 let _ = fs::remove_dir_all(&operations_missing); 5657 5658 let operations_invalid = temp_root("parse_operations_manifest_invalid"); 5659 write_file(&operations_invalid.join("operations.toml"), "[operations"); 5660 let operations_parse_err = 5661 parse_toml::<OperationsContractManifest>(&operations_invalid.join("operations.toml")) 5662 .expect_err("invalid operations manifest"); 5663 assert!(operations_parse_err.contains("parse")); 5664 let _ = fs::remove_dir_all(&operations_invalid); 5665 5666 let dup = temp_root("publish_flags_duplicate"); 5667 write_file( 5668 &dup.join("Cargo.toml"), 5669 r#"[workspace] 5670 members = ["crates/a", "crates/b"] 5671 "#, 5672 ); 5673 let member_manifest = 5674 "[package]\nname = \"duplicate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"; 5675 write_file( 5676 &dup.join("crates").join("a").join("Cargo.toml"), 5677 member_manifest, 5678 ); 5679 write_file( 5680 &dup.join("crates").join("b").join("Cargo.toml"), 5681 member_manifest, 5682 ); 5683 let dup_err = workspace_package_publish_flags(&dup).expect_err("duplicate publish flags"); 5684 assert!(dup_err.contains("duplicate workspace package name")); 5685 let _ = fs::remove_dir_all(&dup); 5686 } 5687 5688 #[test] 5689 fn workspace_package_records_and_callers_report_member_manifest_errors() { 5690 let root = temp_root("workspace_package_record_errors"); 5691 write_file( 5692 &root.join("Cargo.toml"), 5693 r#"[workspace] 5694 members = ["crates/a"] 5695 "#, 5696 ); 5697 5698 let read_err = 5699 workspace_package_records(&root).expect_err("missing member manifest should fail"); 5700 assert!(read_err.contains("read")); 5701 5702 let names_err = workspace_package_names(&root).expect_err("names should fail"); 5703 assert!(names_err.contains("read")); 5704 let manifests_err = workspace_package_manifests(&root).expect_err("manifests should fail"); 5705 assert!(manifests_err.contains("read")); 5706 let flags_err = workspace_package_publish_flags(&root).expect_err("flags should fail"); 5707 assert!(flags_err.contains("read")); 5708 let deps_err = read_workspace_package_dependencies(&root).expect_err("deps should fail"); 5709 assert!(deps_err.contains("read")); 5710 5711 let publish = ["radroots_a".to_string()] 5712 .into_iter() 5713 .collect::<BTreeSet<_>>(); 5714 let publish_err = 5715 validate_publish_package_metadata(&root, &publish).expect_err("publish metadata"); 5716 assert!(publish_err.contains("read")); 5717 5718 write_file( 5719 &root.join("crates").join("a").join("Cargo.toml"), 5720 "[package", 5721 ); 5722 let parse_value_err = 5723 workspace_package_records(&root).expect_err("invalid toml should fail"); 5724 assert!(parse_value_err.contains("parse")); 5725 5726 write_file( 5727 &root.join("crates").join("a").join("Cargo.toml"), 5728 r#"[workspace] 5729 resolver = "2" 5730 "#, 5731 ); 5732 let parse_package_err = 5733 workspace_package_records(&root).expect_err("missing package table should fail"); 5734 assert!(parse_package_err.contains("parse")); 5735 5736 let _ = fs::remove_dir_all(&root); 5737 } 5738 5739 #[test] 5740 fn workspace_package_manifests_success_and_publish_metadata_duplicate_names() { 5741 let root = create_synthetic_workspace("workspace_manifest_success"); 5742 let manifests = workspace_package_manifests(&root).expect("workspace manifests"); 5743 assert_eq!(manifests.len(), 2); 5744 assert!(manifests.contains_key("radroots_a")); 5745 assert!(manifests.contains_key("radroots_b")); 5746 5747 write_file( 5748 &root.join("crates").join("b").join("Cargo.toml"), 5749 r#"[package] 5750 name = "radroots_a" 5751 version = "0.1.0" 5752 edition = "2024" 5753 description = "crate b duplicate name" 5754 repository = "https://example.com/b" 5755 homepage = "https://example.com/b" 5756 documentation = "https://docs.example.com/b" 5757 readme = "README" 5758 publish = false 5759 "#, 5760 ); 5761 let publish = ["radroots_a".to_string()] 5762 .into_iter() 5763 .collect::<BTreeSet<_>>(); 5764 let duplicate_err = 5765 validate_publish_package_metadata(&root, &publish).expect_err("duplicate package map"); 5766 assert!(duplicate_err.contains("duplicate workspace package name")); 5767 5768 let _ = fs::remove_dir_all(&root); 5769 } 5770 5771 #[test] 5772 fn workspace_package_publish_configs_cover_success_and_duplicate_names() { 5773 let root = create_synthetic_workspace("workspace_publish_configs"); 5774 let flags = workspace_package_publish_flags(&root).expect("publish flags"); 5775 assert_eq!(flags["radroots_a"], true); 5776 assert_eq!(flags["radroots_b"], false); 5777 5778 let configs = workspace_package_publish_configs(&root).expect("publish configs"); 5779 assert_eq!( 5780 configs["radroots_a"], 5781 Some(PackagePublish::Registries(vec!["crates-io".to_string()])) 5782 ); 5783 assert_eq!(configs["radroots_b"], Some(PackagePublish::Bool(false))); 5784 5785 write_file( 5786 &root.join("crates").join("b").join("Cargo.toml"), 5787 r#"[package] 5788 name = "radroots_a" 5789 version = "0.1.0" 5790 edition = "2024" 5791 publish = false 5792 "#, 5793 ); 5794 let duplicate_err = workspace_package_publish_configs(&root) 5795 .expect_err("duplicate package name in publish configs"); 5796 assert!(duplicate_err.contains("duplicate workspace package name")); 5797 5798 let _ = fs::remove_dir_all(&root); 5799 } 5800 5801 #[test] 5802 fn workspace_package_publish_configs_report_workspace_record_errors() { 5803 let root = temp_root("workspace_publish_configs_errors"); 5804 let err = workspace_package_publish_configs(&root) 5805 .expect_err("missing workspace manifest should fail"); 5806 assert!(err.contains("Cargo.toml")); 5807 5808 let _ = fs::remove_dir_all(&root); 5809 } 5810 5811 #[test] 5812 fn coverage_release_and_bundle_loaders_report_parse_and_read_errors() { 5813 let root = create_synthetic_workspace("coverage_release_loader_errors"); 5814 let contract_root = root.join("contracts"); 5815 let coverage_root = coverage_root(&contract_root); 5816 let release_policy_path = root_release_policy_path(&root); 5817 5818 let missing_workspace = temp_root("coverage_missing_workspace_manifest"); 5819 let policy_workspace_err = 5820 validate_coverage_policy_parity(&missing_workspace, &contract_root) 5821 .expect_err("coverage workspace lookup error"); 5822 assert!(policy_workspace_err.contains("Cargo.toml")); 5823 let _ = fs::remove_dir_all(&missing_workspace); 5824 5825 let _ = fs::remove_file(coverage_root.join("coverage.toml")); 5826 let policy_load_err = validate_coverage_policy_parity(&root, &contract_root) 5827 .expect_err("coverage policy read error"); 5828 assert!(policy_load_err.contains("coverage.toml")); 5829 write_file( 5830 &coverage_root.join("coverage.toml"), 5831 r#"[gate] 5832 fail_under_exec_lines = 100.0 5833 fail_under_functions = 100.0 5834 fail_under_regions = 100.0 5835 fail_under_branches = 100.0 5836 require_branches = true 5837 5838 [required] 5839 crates = ["radroots_a", "radroots_b"] 5840 "#, 5841 ); 5842 5843 let missing_release = temp_root("release_missing_workspace_manifest"); 5844 write_root_release_policy( 5845 &missing_release, 5846 r#"[release] 5847 version = "1.0.0" 5848 5849 [publish] 5850 crates = ["radroots_a"] 5851 5852 [internal] 5853 crates = ["radroots_b"] 5854 5855 [publish_order] 5856 crates = ["radroots_a"] 5857 "#, 5858 ); 5859 let release_workspace_err = 5860 validate_release_publish_policy(&missing_release, &contract_root, "1.0.0") 5861 .expect_err("release workspace read error"); 5862 assert!(release_workspace_err.contains("Cargo.toml")); 5863 let _ = fs::remove_dir_all(&missing_release); 5864 5865 let _ = fs::remove_file(&release_policy_path); 5866 let release_load_err = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5867 .expect_err("release contract read error"); 5868 assert!(release_load_err.contains(ROOT_RELEASE_POLICY_RELATIVE)); 5869 5870 write_file( 5871 &release_policy_path, 5872 r#"[release] 5873 version = "1.0.0" 5874 5875 [publish] 5876 crates = ["radroots_a", "radroots_a"] 5877 5878 [internal] 5879 crates = ["radroots_b"] 5880 5881 [publish_order] 5882 crates = ["radroots_a"] 5883 "#, 5884 ); 5885 let duplicate_publish = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5886 .expect_err("duplicate publish crates"); 5887 assert!(duplicate_publish.contains("publish.crates has duplicate crate")); 5888 5889 write_file( 5890 &release_policy_path, 5891 r#"[release] 5892 version = "1.0.0" 5893 5894 [publish] 5895 crates = ["radroots_a"] 5896 5897 [internal] 5898 crates = ["radroots_b", "radroots_b"] 5899 5900 [publish_order] 5901 crates = ["radroots_a"] 5902 "#, 5903 ); 5904 let duplicate_internal = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5905 .expect_err("duplicate internal crates"); 5906 assert!(duplicate_internal.contains("internal.crates has duplicate crate")); 5907 5908 write_file( 5909 &release_policy_path, 5910 r#"[release] 5911 version = "1.0.0" 5912 5913 [publish] 5914 crates = ["radroots_a"] 5915 5916 [internal] 5917 crates = ["radroots_b"] 5918 5919 [publish_order] 5920 crates = ["radroots_a", "radroots_a"] 5921 "#, 5922 ); 5923 let duplicate_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5924 .expect_err("duplicate publish order"); 5925 assert!(duplicate_order.contains("publish_order.crates has duplicate crate")); 5926 5927 write_file( 5928 &release_policy_path, 5929 r#"[release] 5930 version = "1.0.0" 5931 5932 [publish] 5933 crates = ["radroots_a"] 5934 5935 [internal] 5936 crates = ["radroots_b"] 5937 5938 [publish_order] 5939 crates = ["radroots_a"] 5940 "#, 5941 ); 5942 write_file( 5943 &root.join("crates").join("a").join("Cargo.toml"), 5944 "[package", 5945 ); 5946 let dependency_err = validate_release_publish_policy(&root, &contract_root, "1.0.0") 5947 .expect_err("workspace dependency parse error"); 5948 assert!(dependency_err.contains("parse")); 5949 5950 let _ = fs::remove_dir_all(&root); 5951 } 5952 5953 #[test] 5954 fn load_release_contract_with_override_reports_override_and_missing_policy_errors() { 5955 let root = create_synthetic_workspace("release_contract_loader_errors"); 5956 let contract_root = root.join("contracts"); 5957 5958 let missing_override = root.join("missing-release-policy.toml"); 5959 let override_err = load_release_contract_with_override( 5960 &root, 5961 &contract_root, 5962 Some(missing_override.clone()), 5963 ) 5964 .expect_err("missing override should fail"); 5965 assert!(override_err.contains(RELEASE_POLICY_ENV)); 5966 assert!(override_err.contains("missing release policy file")); 5967 5968 let _ = fs::remove_file(root_release_policy_path(&root)); 5969 let missing_policy_err = load_release_contract_with_override(&root, &contract_root, None) 5970 .expect_err("missing release policy should fail"); 5971 assert!(missing_policy_err.contains("release publish policy not found")); 5972 assert!(missing_policy_err.contains(ROOT_RELEASE_POLICY_RELATIVE)); 5973 5974 let _ = fs::remove_dir_all(&root); 5975 } 5976 5977 #[test] 5978 fn root_release_policy_preflight_covers_classification_variants() { 5979 let root = create_synthetic_workspace("root_release_policy_classifications"); 5980 configure_root_release_policy_workspace(&root); 5981 write_root_release_policy( 5982 &root, 5983 r#"[release] 5984 version = "1.0.0" 5985 5986 [classification] 5987 public = ["radroots_a"] 5988 internal = ["radroots_b"] 5989 deferred = ["radroots_c"] 5990 retired = ["radroots_d"] 5991 yank_only = ["radroots_e"] 5992 5993 [publish_order] 5994 crates = ["radroots_a"] 5995 "#, 5996 ); 5997 5998 let bundle = load_contract_bundle(&root).expect("load root release policy bundle"); 5999 validate_contract_bundle(&bundle).expect("validate root release policy bundle"); 6000 validate_release_preflight(&root).expect("validate root release policy preflight"); 6001 6002 let _ = fs::remove_dir_all(&root); 6003 } 6004 6005 #[test] 6006 fn root_release_policy_reports_deferred_retired_and_yank_only_errors() { 6007 for (label, policy_body, expected) in [ 6008 ( 6009 "deferred", 6010 r#"[release] 6011 version = "1.0.0" 6012 6013 [classification] 6014 public = ["radroots_a"] 6015 internal = ["radroots_b"] 6016 deferred = ["radroots_c", "radroots_c"] 6017 retired = ["radroots_d"] 6018 yank_only = ["radroots_e"] 6019 6020 [publish_order] 6021 crates = ["radroots_a"] 6022 "#, 6023 "classification.deferred has duplicate crate radroots_c", 6024 ), 6025 ( 6026 "retired", 6027 r#"[release] 6028 version = "1.0.0" 6029 6030 [classification] 6031 public = ["radroots_a"] 6032 internal = ["radroots_b"] 6033 deferred = ["radroots_c"] 6034 retired = [""] 6035 yank_only = ["radroots_e"] 6036 6037 [publish_order] 6038 crates = ["radroots_a"] 6039 "#, 6040 "classification.retired contains an empty crate name", 6041 ), 6042 ( 6043 "yank_only", 6044 r#"[release] 6045 version = "1.0.0" 6046 6047 [classification] 6048 public = ["radroots_a"] 6049 internal = ["radroots_b"] 6050 deferred = ["radroots_c"] 6051 retired = ["radroots_d"] 6052 yank_only = ["radroots_e", "radroots_e"] 6053 6054 [publish_order] 6055 crates = ["radroots_a"] 6056 "#, 6057 "classification.yank_only has duplicate crate radroots_e", 6058 ), 6059 ] { 6060 let root = create_synthetic_workspace(&format!("root_release_policy_{label}_error")); 6061 configure_root_release_policy_workspace(&root); 6062 write_root_release_policy(&root, policy_body); 6063 6064 let err = validate_release_publish_policy(&root, &root.join("contracts"), "1.0.0") 6065 .expect_err("invalid non-public classification should fail"); 6066 assert!(err.contains(expected), "{label} err: {err}"); 6067 6068 let _ = fs::remove_dir_all(&root); 6069 } 6070 } 6071 6072 #[test] 6073 fn validate_release_preflight_reports_each_stage_error() { 6074 let missing_contract_root = temp_root("preflight_missing_contract"); 6075 let missing_contract_err = 6076 validate_release_preflight(&missing_contract_root).expect_err("missing contract"); 6077 assert!(missing_contract_err.contains("manifest.toml")); 6078 let _ = fs::remove_dir_all(&missing_contract_root); 6079 6080 let invalid_bundle = create_synthetic_workspace("preflight_invalid_bundle"); 6081 write_file( 6082 &invalid_bundle.join("contracts").join("manifest.toml"), 6083 r#"[contract] 6084 name = "radroots_contract" 6085 version = "1.0.0" 6086 source = "synthetic" 6087 6088 [surface] 6089 model_crates = ["radroots_a"] 6090 algorithm_crates = ["radroots_b"] 6091 6092 [policy] 6093 exclude_internal_workspace_crates = false 6094 require_reproducible_exports = true 6095 require_conformance_vectors = true 6096 "#, 6097 ); 6098 let invalid_bundle_err = 6099 validate_release_preflight(&invalid_bundle).expect_err("bundle validation"); 6100 assert!(invalid_bundle_err.contains("contract policy flags must all be true")); 6101 let _ = fs::remove_dir_all(&invalid_bundle); 6102 6103 let missing_release = create_synthetic_workspace("preflight_missing_release"); 6104 let _ = fs::remove_file(root_release_policy_path(&missing_release)); 6105 let missing_release_err = 6106 validate_release_preflight(&missing_release).expect_err("missing release"); 6107 assert!(missing_release_err.contains(ROOT_RELEASE_POLICY_RELATIVE)); 6108 let _ = fs::remove_dir_all(&missing_release); 6109 6110 let missing_required = create_synthetic_workspace("preflight_missing_required"); 6111 let _ = fs::remove_file(missing_required.join("contracts").join("coverage.toml")); 6112 let missing_required_err = 6113 validate_release_preflight(&missing_required).expect_err("missing required list"); 6114 assert!(missing_required_err.contains("coverage.toml")); 6115 let _ = fs::remove_dir_all(&missing_required); 6116 6117 let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish"); 6118 write_file( 6119 &root_release_policy_path(&duplicate_publish), 6120 r#"[release] 6121 version = "1.0.0" 6122 6123 [publish] 6124 crates = ["radroots_a", "radroots_a"] 6125 6126 [internal] 6127 crates = ["radroots_b"] 6128 6129 [publish_order] 6130 crates = ["radroots_a"] 6131 "#, 6132 ); 6133 let duplicate_publish_err = 6134 validate_release_preflight(&duplicate_publish).expect_err("duplicate publish crates"); 6135 assert!(duplicate_publish_err.contains("publish.crates has duplicate crate")); 6136 let _ = fs::remove_dir_all(&duplicate_publish); 6137 6138 let duplicate_required = create_synthetic_workspace("preflight_duplicate_required"); 6139 write_file( 6140 &duplicate_required.join("contracts").join("coverage.toml"), 6141 "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_a\"]\n", 6142 ); 6143 let duplicate_required_err = 6144 validate_release_preflight(&duplicate_required).expect_err("duplicate required crates"); 6145 assert!(duplicate_required_err.contains("duplicate crate")); 6146 let _ = fs::remove_dir_all(&duplicate_required); 6147 6148 let publish_metadata = create_synthetic_workspace("preflight_publish_metadata"); 6149 write_file( 6150 &publish_metadata.join("crates").join("a").join("Cargo.toml"), 6151 r#"[package] 6152 name = "radroots_a" 6153 publish = ["crates-io"] 6154 version = "0.1.0" 6155 edition = "2024" 6156 "#, 6157 ); 6158 let publish_metadata_err = 6159 validate_release_preflight(&publish_metadata).expect_err("publish metadata validation"); 6160 assert!(publish_metadata_err.contains("must define a non-empty package.description")); 6161 let _ = fs::remove_dir_all(&publish_metadata); 6162 6163 let missing_coverage_row = create_synthetic_workspace("preflight_missing_coverage_row"); 6164 write_file( 6165 &missing_coverage_row 6166 .join("target") 6167 .join("coverage") 6168 .join("coverage-refresh.tsv"), 6169 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n", 6170 ); 6171 let missing_coverage_row_err = validate_release_preflight(&missing_coverage_row) 6172 .expect_err("required coverage refresh row missing"); 6173 assert!(missing_coverage_row_err.contains("missing from coverage-refresh.tsv")); 6174 let _ = fs::remove_dir_all(&missing_coverage_row); 6175 } 6176 6177 #[test] 6178 fn load_contract_bundle_and_validation_report_version_core_and_coverage_errors() { 6179 let root = create_synthetic_workspace("bundle_version_core_and_coverage_errors"); 6180 write_file(&root.join("contracts").join("version.toml"), "[contract"); 6181 let version_parse_err = load_contract_bundle(&root).expect_err("invalid version file"); 6182 assert!(version_parse_err.contains("version.toml")); 6183 6184 write_file( 6185 &root.join("contracts").join("version.toml"), 6186 r#"[contract] 6187 version = "1.0.0" 6188 stability = "alpha" 6189 6190 [semver] 6191 major_on = ["breaking"] 6192 minor_on = ["feature"] 6193 patch_on = ["fix"] 6194 6195 [compatibility] 6196 requires_conformance_pass = true 6197 requires_contract_manifest_diff = true 6198 requires_release_notes = true 6199 "#, 6200 ); 6201 let bundle = load_contract_bundle(&root).expect("load bundle"); 6202 write_file( 6203 &root.join("crates").join("core").join("src").join("unit.rs"), 6204 r#"pub enum RadrootsCoreUnitDimension { 6205 Mass, 6206 Count, 6207 Volume, 6208 } 6209 "#, 6210 ); 6211 let core_err = validate_contract_bundle(&bundle).expect_err("core unit mismatch"); 6212 assert!(core_err.contains("variant order must be")); 6213 6214 write_file( 6215 &root.join("crates").join("core").join("src").join("unit.rs"), 6216 r#"pub enum RadrootsCoreUnitDimension { 6217 Count, 6218 Mass, 6219 Volume, 6220 } 6221 "#, 6222 ); 6223 write_file( 6224 &root.join("contracts").join("coverage.toml"), 6225 r#"[gate] 6226 fail_under_exec_lines = 100.0 6227 fail_under_functions = 100.0 6228 fail_under_regions = 100.0 6229 fail_under_branches = 100.0 6230 require_branches = false 6231 6232 [required] 6233 crates = ["radroots_a", "radroots_b"] 6234 "#, 6235 ); 6236 let policy_err = validate_contract_bundle(&bundle).expect_err("coverage policy validation"); 6237 assert!(policy_err.contains("100/100/100/100")); 6238 6239 let _ = fs::remove_dir_all(&root); 6240 } 6241 6242 #[test] 6243 fn coverage_summary_and_core_enum_additional_error_paths() { 6244 let coverage_root = temp_root("coverage_summary_additional_errors"); 6245 write_file( 6246 &coverage_root 6247 .join("target") 6248 .join("coverage") 6249 .join("coverage-refresh.tsv"), 6250 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100\tbad\t100\t100\tfile\n", 6251 ); 6252 let func_err = load_coverage_refresh_rows(&coverage_root).expect_err("func parse error"); 6253 assert!(func_err.contains("parse func")); 6254 write_file( 6255 &coverage_root 6256 .join("target") 6257 .join("coverage") 6258 .join("coverage-refresh.tsv"), 6259 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100\t100\tbad\t100\tfile\n", 6260 ); 6261 let branch_err = 6262 load_coverage_refresh_rows(&coverage_root).expect_err("branch parse error"); 6263 assert!(branch_err.contains("parse branch")); 6264 let _ = fs::remove_dir_all(&coverage_root); 6265 6266 let missing_refresh_root = temp_root("coverage_summary_missing_refresh"); 6267 let required = ["radroots_a".to_string()] 6268 .into_iter() 6269 .collect::<BTreeSet<_>>(); 6270 let missing_refresh_err = validate_required_coverage_summary( 6271 &missing_refresh_root, 6272 &required, 6273 strict_thresholds(), 6274 ) 6275 .expect_err("missing refresh should fail"); 6276 assert!(missing_refresh_err.contains("coverage-refresh.tsv")); 6277 let _ = fs::remove_dir_all(&missing_refresh_root); 6278 6279 let enum_root = temp_root("core_unit_missing_enum"); 6280 write_file( 6281 &enum_root 6282 .join("crates") 6283 .join("core") 6284 .join("src") 6285 .join("unit.rs"), 6286 "pub struct NotTheEnum;", 6287 ); 6288 let enum_err = 6289 validate_core_unit_dimension_variant_order(&enum_root).expect_err("missing enum"); 6290 assert!(enum_err.contains("missing enum")); 6291 let _ = fs::remove_dir_all(&enum_root); 6292 } 6293 6294 #[test] 6295 fn publish_metadata_and_coverage_refresh_report_missing_paths() { 6296 let root = temp_root("publish_missing_manifest"); 6297 write_file( 6298 &root.join("Cargo.toml"), 6299 r#"[workspace] 6300 members = ["crates/a"] 6301 "#, 6302 ); 6303 write_file( 6304 &root.join("crates").join("a").join("Cargo.toml"), 6305 r#"[package] 6306 name = "radroots_a" 6307 version = "0.1.0" 6308 edition = "2024" 6309 description = "crate a" 6310 repository = { workspace = true } 6311 homepage = { workspace = true } 6312 readme = { workspace = true } 6313 "#, 6314 ); 6315 let missing_manifest = ["radroots_b".to_string()] 6316 .into_iter() 6317 .collect::<BTreeSet<_>>(); 6318 let missing_err = validate_publish_package_metadata(&root, &missing_manifest) 6319 .expect_err("missing workspace manifest"); 6320 assert!(missing_err.contains("has no workspace manifest")); 6321 6322 let missing_field = ["radroots_a".to_string()] 6323 .into_iter() 6324 .collect::<BTreeSet<_>>(); 6325 let field_err = validate_publish_package_metadata(&root, &missing_field) 6326 .expect_err("missing configured field"); 6327 assert!(field_err.contains("must configure package.documentation")); 6328 6329 let refresh_missing = 6330 load_coverage_refresh_rows(&root).expect_err("missing coverage-refresh.tsv"); 6331 assert!(refresh_missing.contains("coverage-refresh.tsv")); 6332 let _ = fs::remove_dir_all(&root); 6333 } 6334 6335 #[test] 6336 fn coverage_refresh_parser_skips_blank_lines() { 6337 let root = temp_root("coverage_refresh_blank_lines"); 6338 write_file( 6339 &root 6340 .join("target") 6341 .join("coverage") 6342 .join("coverage-refresh.tsv"), 6343 "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n\nradroots_a\tpass\t100\t100\t100\t100\tfile\n", 6344 ); 6345 let rows = load_coverage_refresh_rows(&root).expect("rows"); 6346 assert_eq!(rows.len(), 1); 6347 assert!(rows.contains_key("radroots_a")); 6348 let _ = fs::remove_dir_all(&root); 6349 } 6350 6351 #[test] 6352 fn core_unit_dimension_validation_reports_missing_and_mismatch() { 6353 let missing = temp_root("core_unit_missing"); 6354 let missing_err = validate_core_unit_dimension_variant_order(&missing) 6355 .expect_err("missing unit file should fail"); 6356 assert!(missing_err.contains("unit.rs")); 6357 let _ = fs::remove_dir_all(&missing); 6358 6359 let mismatch = temp_root("core_unit_mismatch"); 6360 write_file( 6361 &mismatch 6362 .join("crates") 6363 .join("core") 6364 .join("src") 6365 .join("unit.rs"), 6366 r#"pub enum RadrootsCoreUnitDimension { 6367 Mass, 6368 Count, 6369 Volume, 6370 } 6371 "#, 6372 ); 6373 let mismatch_err = validate_core_unit_dimension_variant_order(&mismatch) 6374 .expect_err("mismatched enum order should fail"); 6375 assert!(mismatch_err.contains("variant order must be")); 6376 let _ = fs::remove_dir_all(&mismatch); 6377 } 6378 6379 #[test] 6380 fn coverage_and_release_additional_error_branches_are_reported() { 6381 let root = create_synthetic_workspace("coverage_release_extra_errors"); 6382 let contract_root = root.join("contracts"); 6383 let coverage_root = coverage_root(&contract_root); 6384 let release_policy_path = root_release_policy_path(&root); 6385 6386 write_file( 6387 &coverage_root.join("coverage.toml"), 6388 r#"[gate] 6389 fail_under_exec_lines = 100.0 6390 fail_under_functions = 100.0 6391 fail_under_regions = 100.0 6392 fail_under_branches = 100.0 6393 require_branches = true 6394 6395 [required] 6396 crates = ["radroots_a", "radroots_b", "radroots_extra"] 6397 "#, 6398 ); 6399 let coverage_extra = validate_coverage_policy_parity(&root, &contract_root) 6400 .expect_err("coverage unknown crate"); 6401 assert!(coverage_extra.contains("includes excluded or unknown crates")); 6402 6403 write_file( 6404 &coverage_root.join("coverage.toml"), 6405 r#"[gate] 6406 fail_under_exec_lines = 100.0 6407 fail_under_functions = 100.0 6408 fail_under_regions = 100.0 6409 fail_under_branches = 100.0 6410 require_branches = true 6411 6412 [required] 6413 crates = ["radroots_b"] 6414 "#, 6415 ); 6416 let required_list_mismatch = validate_coverage_policy_parity(&root, &contract_root) 6417 .expect_err("required list must match workspace crates"); 6418 assert!(required_list_mismatch.contains("missing workspace crates")); 6419 6420 write_file( 6421 &release_policy_path, 6422 r#"[release] 6423 version = "1.0.0" 6424 6425 [publish] 6426 crates = ["radroots_a", "radroots_b", "radroots_extra"] 6427 6428 [internal] 6429 crates = [] 6430 6431 [publish_order] 6432 crates = ["radroots_a", "radroots_b"] 6433 "#, 6434 ); 6435 let release_extra = validate_release_publish_policy(&root, &contract_root, "1.0.0") 6436 .expect_err("release extra crate"); 6437 assert!(release_extra.contains("include unknown crates")); 6438 6439 write_file( 6440 &release_policy_path, 6441 r#"[release] 6442 version = "1.0.0" 6443 6444 [publish] 6445 crates = ["radroots_a"] 6446 6447 [internal] 6448 crates = ["radroots_b"] 6449 6450 [publish_order] 6451 crates = ["radroots_a", "radroots_b"] 6452 "#, 6453 ); 6454 let publish_order_extra = validate_release_publish_policy(&root, &contract_root, "1.0.0") 6455 .expect_err("publish order non-publish crate"); 6456 assert!(publish_order_extra.contains("non-publish crates")); 6457 6458 let _ = fs::remove_dir_all(&root); 6459 } 6460 6461 #[test] 6462 fn validate_contract_bundle_reports_release_policy_errors() { 6463 let release_error_root = create_synthetic_workspace("bundle_release_policy_error"); 6464 write_file( 6465 &root_release_policy_path(&release_error_root), 6466 r#"[release] 6467 version = "1.0.0" 6468 6469 [publish] 6470 crates = ["radroots_a"] 6471 6472 [internal] 6473 crates = ["radroots_b"] 6474 6475 [publish_order] 6476 crates = [] 6477 "#, 6478 ); 6479 let bundle = load_contract_bundle(&release_error_root).expect("load release error bundle"); 6480 let release_err = validate_contract_bundle(&bundle).expect_err("release policy failure"); 6481 assert!(release_err.contains("publish_order.crates is missing publish crates")); 6482 let _ = fs::remove_dir_all(&release_error_root); 6483 } 6484 }