Coverage gate
The coverage gate enforces minimum line / branch / function coverage
percentages for the doctor_frankentui crate. It reads a threshold
table from thresholds.toml, runs cargo llvm-cov, and fails with a
human-readable report when any metric falls below its floor.
Source: scripts/doctor_frankentui_coverage.sh +
crates/doctor_frankentui/coverage/thresholds.toml +
crates/doctor_frankentui/coverage/README.md.
Running the gate
./scripts/doctor_frankentui_coverage.sh
# or, with a custom output directory
./scripts/doctor_frankentui_coverage.sh /tmp/doctor_frankentui_coverage_gateDefault output directory: target/doctor_frankentui_coverage/.
Under it you get:
coverage_summary.json— rawcargo llvm-cov --branch --summary-only --json.coverage_gate_report.json— threshold evaluation.coverage_gate_report.txt— human-readable report.
Exit code is non-zero iff any threshold fails.
Prerequisites
cargo— Rust toolchain.cargo-llvm-cov— install withcargo install cargo-llvm-cov.python3— 3.11+ shipstomllibin the stdlib. On 3.9/3.10, installtomli(pip install tomli); the script detects this and errors with a clear message otherwise.
The threshold table
crates/doctor_frankentui/coverage/thresholds.toml has three kinds of
entries.
Totals
Crate-wide floors for lines, branches, functions.
[total]
lines = 89.0
branches = 69.0
functions = 86.0Groups
Logical groupings of files. Two canonical groups:
[group.orchestration]— command-heavy modules with high branch complexity (capture.rs,doctor.rs,report.rs,seed.rs,suite.rs). Floors are looser because orchestration has more unreachable error paths.[group.foundations]— parsers, formats, helpers (cli.rs,error.rs,keyseq.rs,main.rs,profile.rs,runmeta.rs,tape.rs,util.rs). Expected to stay highly covered.
[group.orchestration]
files = [
"crates/doctor_frankentui/src/capture.rs",
"crates/doctor_frankentui/src/doctor.rs",
"crates/doctor_frankentui/src/report.rs",
"crates/doctor_frankentui/src/seed.rs",
"crates/doctor_frankentui/src/suite.rs",
]
lines = 88.0
branches = 66.0
functions = 81.0Per-module floors
Named [group.module_<name>]. These exist so that a group average
can’t mask a single module’s regression. Example:
[group.module_capture]
files = ["crates/doctor_frankentui/src/capture.rs"]
lines = 78.0
branches = 69.0
functions = 75.0
[group.module_cli]
files = ["crates/doctor_frankentui/src/cli.rs"]
lines = 99.0
branches = 50.0
functions = 100.0Every module in the crate has a floor; they are tight for the ones that should stay near 100% and looser for inherently branchy code.
Reading coverage_gate_report.json
{
"schema_version": "coverage-gate-v1",
"passed": true,
"totals": {
"lines": { "actual": 92.1, "threshold": 89.0, "verdict": "pass" },
"branches": { "actual": 72.4, "threshold": 69.0, "verdict": "pass" },
"functions": { "actual": 88.3, "threshold": 86.0, "verdict": "pass" }
},
"groups": {
"orchestration": {
"lines": { "actual": 90.1, "threshold": 88.0, "verdict": "pass" },
"branches": { "actual": 67.4, "threshold": 66.0, "verdict": "pass" },
"functions": { "actual": 82.7, "threshold": 81.0, "verdict": "pass" }
},
"module_cli": {
"lines": { "actual": 99.1, "threshold": 99.0, "verdict": "pass" },
"branches": { "actual": 52.3, "threshold": 50.0, "verdict": "pass" },
"functions": { "actual": 100.0, "threshold": 100.0, "verdict": "pass" }
}
}
}passed— the overall gate verdict.totals,groups— per-scope breakdowns.verdictis"pass"or"fail"per metric.
On failure the txt report prints the failing groups with per-metric
deltas:
[coverage] FAIL group.module_capture: lines 76.2 < 78.0 (delta -1.8)
[coverage] FAIL group.module_report: branches 78.9 < 81.0 (delta -2.1)Typical debug loop
Run the gate locally
./scripts/doctor_frankentui_coverage.shRead the .txt report. Identify the failing scope (group or module)
and metric (lines / branches / functions).
Generate an HTML report to drill in
cargo llvm-cov -p doctor_frankentui --all-targets --branch --html
open target/llvm-cov/html/index.htmlNavigate to the failing file. Red-highlighted lines / branches are uncovered.
Add tests or remove dead code
Whichever is appropriate. The no-mock policy still applies — coverage earned by mocking counts as cheating.
Re-run the gate
./scripts/doctor_frankentui_coverage.shIterate until passed: true.
Tightening a threshold
When you add a test that materially lifts coverage, the right move is often to tighten the floor so a future regression trips the gate.
Establish the new baseline
./scripts/doctor_frankentui_coverage.sh
cat target/doctor_frankentui_coverage/coverage_gate_report.json | jq .Note the actuals for the affected groups.
Bump the threshold table
Edit crates/doctor_frankentui/coverage/thresholds.toml. Round down
by ~0.5 percentage points — the actual value has run-to-run variance.
Rerun to confirm the new floor holds
./scripts/doctor_frankentui_coverage.shDeliberately regress to confirm the gate trips
Comment out a test, rerun, confirm the gate fails. Revert. This is the “deliberate regression” check from the coverage playbook .
Aligning with the wider coverage matrix
The crate-level gate lives in parallel with the workspace-wide
coverage matrix (docs/testing/coverage-matrix.md). The two are
independent but coordinated:
- Workspace-level LCOV gate: per-crate thresholds across the whole
repo, enforced against
lcov.info. doctor_frankentuigate: tighter thresholds for a single crate because it is on the hot path for CI certification.
See the coverage playbook for the workspace gate.
Pitfalls
Don’t lower thresholds without documentation. If a PR genuinely must reduce a floor (deprecated module, branch moved to a different file), call it out in the PR description. The threshold table is a log of what we consider “covered enough”.
Branch coverage is sensitive to refactoring. Moving an if into
a match or vice versa can move branch counts without changing
behaviour. If branch coverage drops after a pure refactor, the fix
is usually a couple of targeted tests for the new branch shape, not
a threshold change.
Python version mismatch is the #1 setup failure. If
tomllib/tomli is missing, the script exits early. Install
Python 3.11+ or pip install tomli.