lib

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

commit b9cb676cc5c2d2f4535b2186caff2d31350b47e3
parent b41c2e949131356fea27f7938f72574d30bc4c53
Author: triesap <tyson@radroots.org>
Date:   Wed,  4 Mar 2026 21:42:23 +0000

coverage: enforce regions as fourth strict gate metric

- add fail-under-regions to xtask coverage report thresholds and gate evaluation
- include regions in coverage gate report payloads, cli help, and failure reasons
- require region column in coverage-refresh.tsv and enforce 100/100/100/100 in contract validation
- wire regions into release preflight and sdk coverage ci workflows with updated policy/rollout docs

Diffstat:
M.github/workflows/sdk-coverage-ci.yml | 3+++
Mcontract/coverage/POLICY.md | 5+++--
Mcontract/coverage/rollout.toml | 1+
Mcrates/xtask/src/contract.rs | 60+++++++++++++++++++++++++++++++++++++++---------------------
Mcrates/xtask/src/coverage.rs | 34++++++++++++++++++++++++++++++----
Mcrates/xtask/src/export_ts.rs | 2+-
Mcrates/xtask/src/main.rs | 8+++++---
Mscripts/ci/release_preflight.sh | 5+++--
8 files changed, 85 insertions(+), 33 deletions(-)

diff --git a/.github/workflows/sdk-coverage-ci.yml b/.github/workflows/sdk-coverage-ci.yml @@ -52,6 +52,7 @@ jobs: --out "${run_dir}/coverage-gate-summary.json" \ --fail-under-exec-lines 0 \ --fail-under-functions 0 \ + --fail-under-regions 0 \ --fail-under-branches 0; then status="report-failed" fi @@ -63,6 +64,7 @@ jobs: "thresholds": { "executable_lines": 0, "functions": 0, + "regions": 0, "branches": 0, "branches_required": false }, @@ -114,6 +116,7 @@ JSON --out "${crate_dir}/coverage-gate-blocking.json" \ --fail-under-exec-lines 100 \ --fail-under-functions 100 \ + --fail-under-regions 100 \ --fail-under-branches 100 \ --require-branches done < /tmp/radroots_required_coverage_crates.txt diff --git a/contract/coverage/POLICY.md b/contract/coverage/POLICY.md @@ -6,10 +6,11 @@ This document defines the required coverage gate for the oss rust workspace. - executable lines coverage: 100.0 - function coverage: 100.0 +- region coverage: 100.0 - branch coverage: 100.0 - branch records must be present in lcov data -All three thresholds are release-blocking. +All four thresholds are release-blocking. ## toolchain contract @@ -21,7 +22,7 @@ All three thresholds are release-blocking. ## enforcement contract - run coverage checks per crate, not only aggregate workspace totals -- a crate cannot be promoted to required unless it is at 100/100/100 +- a crate cannot be promoted to required unless it is at 100/100/100/100 - once required, the crate remains blocking on every pull request and push to `master` ## rollout contract diff --git a/contract/coverage/rollout.toml b/contract/coverage/rollout.toml @@ -1,6 +1,7 @@ [policy] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 +fail_under_regions = 100.0 fail_under_branches = 100.0 require_branches = true diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -364,7 +364,7 @@ fn parse_coverage_percent(raw: &str, field: &str, crate_name: &str) -> Result<f6 fn load_coverage_refresh_rows( workspace_root: &Path, -) -> Result<BTreeMap<String, (String, f64, f64, f64)>, String> { +) -> Result<BTreeMap<String, (String, f64, f64, f64, f64)>, String> { let report_path = workspace_root .join("target") .join("coverage") @@ -380,9 +380,9 @@ fn load_coverage_refresh_rows( continue; } let parts = trimmed.split('\t').collect::<Vec<_>>(); - if parts.len() < 5 { + if parts.len() < 6 { return Err(format!( - "coverage row must have at least 5 columns in {}: {}", + "coverage row must have at least 6 columns in {}: {}", report_path.display(), trimmed )); @@ -392,7 +392,8 @@ fn load_coverage_refresh_rows( let exec = parse_coverage_percent(parts[2], "exec", &crate_name)?; let func = parse_coverage_percent(parts[3], "func", &crate_name)?; let branch = parse_coverage_percent(parts[4], "branch", &crate_name)?; - rows.insert(crate_name, (status, exec, func, branch)); + let region = parse_coverage_percent(parts[5], "region", &crate_name)?; + rows.insert(crate_name, (status, exec, func, branch, region)); } Ok(rows) } @@ -403,7 +404,7 @@ fn validate_required_coverage_summary( ) -> Result<(), String> { let rows = load_coverage_refresh_rows(workspace_root)?; for crate_name in required_crates { - let (status, exec, func, branch) = rows.get(crate_name).ok_or_else(|| { + let (status, exec, func, branch, region) = rows.get(crate_name).ok_or_else(|| { format!( "required coverage crate {} missing from coverage-refresh.tsv", crate_name @@ -415,10 +416,10 @@ fn validate_required_coverage_summary( crate_name, status )); } - if *exec < 100.0 || *func < 100.0 || *branch < 100.0 { + if *exec < 100.0 || *func < 100.0 || *branch < 100.0 || *region < 100.0 { return Err(format!( - "required coverage crate {} must be 100/100/100, found {}/{}/{}", - crate_name, exec, func, branch + "required coverage crate {} must be 100/100/100/100, found {}/{}/{}/{}", + crate_name, exec, func, branch, region )); } } @@ -1037,7 +1038,7 @@ crates = ["radroots-a"] .join("target") .join("coverage") .join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", ); root } @@ -1193,7 +1194,7 @@ pub enum RadrootsCoreUnitDimension { fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t100.0\t100.0\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", ) .expect("write coverage file"); let required = ["radroots-core".to_string()] @@ -1203,21 +1204,30 @@ pub enum RadrootsCoreUnitDimension { fs::write( coverage_dir.join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t99.9\t100.0\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t99.9\t100.0\t100.0\tfile\n", ) .expect("write function coverage file"); let func_err = validate_required_coverage_summary(&root, &required) .expect_err("function coverage below 100"); - assert!(func_err.contains("must be 100/100/100")); + assert!(func_err.contains("must be 100/100/100/100")); fs::write( coverage_dir.join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t100.0\t99.9\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t100.0\t99.9\t100.0\tfile\n", ) .expect("write branch coverage file"); let branch_err = validate_required_coverage_summary(&root, &required) .expect_err("branch coverage below 100"); - assert!(branch_err.contains("must be 100/100/100")); + assert!(branch_err.contains("must be 100/100/100/100")); + + fs::write( + coverage_dir.join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t100.0\t100.0\t99.9\tfile\n", + ) + .expect("write region coverage file"); + let region_err = validate_required_coverage_summary(&root, &required) + .expect_err("region coverage below 100"); + assert!(region_err.contains("must be 100/100/100/100")); let _ = fs::remove_dir_all(&root); } @@ -1323,21 +1333,29 @@ members = ["crates/a", "crates/b"] write_file( &coverage_dir.join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nbad-row\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nbad-row\n", ); let bad_row = load_coverage_refresh_rows(&root).expect_err("invalid coverage row"); - assert!(bad_row.contains("at least 5 columns")); + assert!(bad_row.contains("at least 6 columns")); write_file( &coverage_dir.join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\tnot-a-number\t100\t100\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\tnot-a-number\t100\t100\t100\tfile\n", ); let bad_percent = load_coverage_refresh_rows(&root).expect_err("invalid coverage percent"); assert!(bad_percent.contains("parse exec")); write_file( &coverage_dir.join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tfail\t100\t100\t100\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100\t100\t100\tnot-a-number\tfile\n", + ); + let bad_region = + load_coverage_refresh_rows(&root).expect_err("invalid region coverage percent"); + assert!(bad_region.contains("parse region")); + + write_file( + &coverage_dir.join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tfail\t100\t100\t100\t100\tfile\n", ); let required = ["radroots-a".to_string()] .into_iter() @@ -1348,11 +1366,11 @@ members = ["crates/a", "crates/b"] write_file( &coverage_dir.join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\t99.9\t100\t100\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t99.9\t100\t100\t100\tfile\n", ); let below_100 = validate_required_coverage_summary(&root, &required).expect_err("coverage below 100"); - assert!(below_100.contains("must be 100/100/100")); + assert!(below_100.contains("must be 100/100/100/100")); let missing = ["missing".to_string()].into_iter().collect::<BTreeSet<_>>(); let missing_err = @@ -1963,7 +1981,7 @@ readme = { workspace = true } .join("target") .join("coverage") .join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\n\nradroots-a\tpass\t100\t100\t100\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n\nradroots-a\tpass\t100\t100\t100\t100\tfile\n", ); let rows = load_coverage_refresh_rows(&root).expect("rows"); assert_eq!(rows.len(), 1); diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -38,6 +38,7 @@ pub struct LcovCoverage { pub struct CoverageThresholds { pub fail_under_exec_lines: f64, pub fail_under_functions: f64, + pub fail_under_regions: f64, pub fail_under_branches: f64, pub require_branches: bool, } @@ -61,6 +62,7 @@ struct CoverageGateReport { struct CoverageGateReportThresholds { executable_lines: f64, functions: f64, + regions: f64, branches: f64, branches_required: bool, } @@ -478,12 +480,13 @@ pub fn evaluate_gate( ) -> CoverageGateResult { let exec_ok = lcov.executable_percent >= thresholds.fail_under_exec_lines; let functions_ok = summary.functions_percent >= thresholds.fail_under_functions; + let regions_ok = summary.summary_regions_percent >= thresholds.fail_under_regions; let branch_presence_ok = !thresholds.require_branches || lcov.branches_available; let branch_ok = lcov .branch_percent .is_none_or(|branch_percent| branch_percent >= thresholds.fail_under_branches); - let pass = exec_ok && functions_ok && branch_presence_ok && branch_ok; + let pass = exec_ok && functions_ok && regions_ok && branch_presence_ok && branch_ok; let mut fail_reasons: Vec<String> = Vec::new(); if !exec_ok { @@ -500,6 +503,13 @@ pub fn evaluate_gate( )); } + if !regions_ok { + fail_reasons.push(format!( + "regions={:.6} < {:.6}", + summary.summary_regions_percent, thresholds.fail_under_regions + )); + } + if thresholds.require_branches && !lcov.branches_available { fail_reasons.push("branches=unavailable".to_string()); } @@ -704,6 +714,7 @@ fn report_gate(args: &[String]) -> Result<(), String> { let thresholds = CoverageThresholds { fail_under_exec_lines: parse_f64_arg(args, "fail-under-exec-lines", 100.0)?, fail_under_functions: parse_f64_arg(args, "fail-under-functions", 100.0)?, + fail_under_regions: parse_f64_arg(args, "fail-under-regions", 100.0)?, fail_under_branches: parse_f64_arg(args, "fail-under-branches", 100.0)?, require_branches: parse_bool_flag(args, "require-branches"), }; @@ -717,6 +728,7 @@ fn report_gate(args: &[String]) -> Result<(), String> { thresholds: CoverageGateReportThresholds { executable_lines: thresholds.fail_under_exec_lines, functions: thresholds.fail_under_functions, + regions: thresholds.fail_under_regions, branches: thresholds.fail_under_branches, branches_required: thresholds.require_branches, }, @@ -753,16 +765,20 @@ fn report_gate(args: &[String]) -> Result<(), String> { if lcov.branches_available { eprintln!( - "{} coverage: executable_lines={:.6} functions={:.6} branches={:.6}", + "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches={:.6}", scope, lcov.executable_percent, summary.functions_percent, + summary.summary_regions_percent, lcov.branch_percent.unwrap_or(0.0) ); } else { eprintln!( - "{} coverage: executable_lines={:.6} functions={:.6} branches=unavailable", - scope, lcov.executable_percent, summary.functions_percent + "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches=unavailable", + scope, + lcov.executable_percent, + summary.functions_percent, + summary.summary_regions_percent ); } @@ -968,6 +984,7 @@ mod tests { let thresholds = CoverageThresholds { fail_under_exec_lines: 100.0, fail_under_functions: 100.0, + fail_under_regions: 100.0, fail_under_branches: 100.0, require_branches: true, }; @@ -1342,6 +1359,7 @@ test_threads = 0 let thresholds = CoverageThresholds { fail_under_exec_lines: 90.0, fail_under_functions: 90.0, + fail_under_regions: 90.0, fail_under_branches: 90.0, require_branches: true, }; @@ -1361,6 +1379,11 @@ test_threads = 0 assert!( gate.fail_reasons .iter() + .any(|reason| reason.contains("regions")) + ); + assert!( + gate.fail_reasons + .iter() .any(|reason| reason.contains("branches")) ); } @@ -1522,6 +1545,7 @@ test_threads = 0 report_gate(&args).expect("report gate success"); let report_raw = fs::read_to_string(&out_path).expect("read report"); assert!(report_raw.contains("\"scope\": \"crate-x\"")); + assert!(report_raw.contains("\"regions\": 100.0")); assert!(report_raw.contains("\"pass\": true")); fs::remove_dir_all(root).expect("remove report gate success root"); } @@ -1551,6 +1575,8 @@ test_threads = 0 "100.0".to_string(), "--fail-under-functions".to_string(), "100.0".to_string(), + "--fail-under-regions".to_string(), + "100.0".to_string(), "--fail-under-branches".to_string(), "100.0".to_string(), ]; diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -655,7 +655,7 @@ crates = ["radroots-a"] .join("target") .join("coverage") .join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", ); root } diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -22,7 +22,7 @@ fn usage() { eprintln!(" cargo xtask sdk coverage required-crates"); eprintln!(" cargo xtask sdk coverage workspace-crates"); eprintln!( - " cargo xtask sdk coverage report --scope <scope> --summary <file> --lcov <file> --out <file>" + " cargo xtask sdk coverage report --scope <scope> --summary <file> --lcov <file> --out <file> [--fail-under-exec-lines <pct>] [--fail-under-functions <pct>] [--fail-under-regions <pct>] [--fail-under-branches <pct>] [--require-branches]" ); } @@ -374,10 +374,12 @@ mod tests { .and_then(|table| table.get("crates")) .and_then(toml::Value::as_array) .expect("required crates array"); - let mut rows = String::from("crate\tstatus\texec\tfunc\tbranch\treport\n"); + let mut rows = String::from("crate\tstatus\texec\tfunc\tbranch\tregion\treport\n"); for crate_name in required_crates { let crate_name = crate_name.as_str().expect("required crate name"); - rows.push_str(&format!("{crate_name}\tpass\t100.0\t100.0\t100.0\tfile\n")); + rows.push_str(&format!( + "{crate_name}\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n" + )); } fs::write(&coverage_refresh_path, rows).expect("write coverage refresh"); diff --git a/scripts/ci/release_preflight.sh b/scripts/ci/release_preflight.sh @@ -13,7 +13,7 @@ trap 'rm -f "$required_file"' EXIT cargo run -q -p xtask -- sdk coverage required-crates > "$required_file" mkdir -p target/coverage -printf "crate\tstatus\texec\tfunc\tbranch\treport\n" > target/coverage/coverage-refresh.tsv +printf "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n" > target/coverage/coverage-refresh.tsv printf "crate\tstatus\n" > target/coverage/coverage-refresh-status.tsv while IFS= read -r crate; do @@ -30,10 +30,11 @@ while IFS= read -r crate; do --out "${out_dir}/gate-report.json" \ --fail-under-exec-lines 100 \ --fail-under-functions 100 \ + --fail-under-regions 100 \ --fail-under-branches 100 \ --require-branches - printf "%s\tpass\t100.0\t100.0\t100.0\t%s\n" "$crate" "${out_dir}/gate-report.json" >> target/coverage/coverage-refresh.tsv + printf "%s\tpass\t100.0\t100.0\t100.0\t100.0\t%s\n" "$crate" "${out_dir}/gate-report.json" >> target/coverage/coverage-refresh.tsv printf "%s\tpass\n" "$crate" >> target/coverage/coverage-refresh-status.tsv done < "$required_file"