diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a53295..bb6af3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ ## [Unreleased] +### Fixed + +- **REQ-110 / REQ-111 — coverage "totals" no longer masquerade as artifact + counts.** The dashboard overview rendered `{covered} / {total} artifacts + covered` and the `coverage --format json` `overall` object exposed + `total`/`covered`, but both aggregate *per-rule* denominators — an artifact + satisfying N traceability rules is counted N times — which is a different + cardinality from the distinct-artifact `total` the `stats` command reports + under the same key. The numbers are unchanged (per the relabel decision); + the label is now honest: the HTML reads "coverage checks" and the JSON + `overall` exposes `checks_covered` / `checks_total` (the ambiguous + `total`/`covered` keys are removed from the `overall` object — a JSON + consumer change). Per-rule `entries[]` keep `covered`/`total`, which are + correct at rule scope. ## [0.14.0] — 2026-05-30 Theme: **agent-actionable validation + self-contained export**. Two diff --git a/rivet-cli/src/main.rs b/rivet-cli/src/main.rs index a12c761..e247088 100644 --- a/rivet-cli/src/main.rs +++ b/rivet-cli/src/main.rs @@ -6099,17 +6099,21 @@ fn cmd_coverage( }) }) .collect(); - let total: usize = report.entries.iter().map(|e| e.total).sum(); - let covered: usize = report.entries.iter().map(|e| e.covered).sum(); + // REQ-111: these aggregate per-RULE denominators (an artifact satisfying + // N rules is counted N times) — NOT the distinct-artifact `total` that + // `stats` JSON reports. Emit under `checks_*` keys so the same key name + // `total` never carries two different cardinalities across commands. + let checks_total: usize = report.entries.iter().map(|e| e.total).sum(); + let checks_covered: usize = report.entries.iter().map(|e| e.covered).sum(); let external_boundary: usize = report.entries.iter().map(|e| e.external_boundary).sum(); let overall_pct = (report.overall_coverage() * 10.0).round() / 10.0; let mut output = serde_json::json!({ "command": "coverage", "rules": rules_json, "overall": { - "covered": covered, + "checks_covered": checks_covered, "external_boundary": external_boundary, - "total": total, + "checks_total": checks_total, "percentage": overall_pct, }, }); diff --git a/rivet-cli/src/render/stats.rs b/rivet-cli/src/render/stats.rs index 7e85283..7c28026 100644 --- a/rivet-cli/src/render/stats.rs +++ b/rivet-cli/src/render/stats.rs @@ -234,8 +234,12 @@ pub(crate) fn render_stats(ctx: &RenderContext) -> String { } else { "#c62828" }; - let total_covered: usize = cov_report.entries.iter().map(|e| e.covered).sum(); - let total_items: usize = cov_report.entries.iter().map(|e| e.total).sum(); + // REQ-110: these are per-RULE check sums (an artifact in N rules counts + // N times), NOT distinct artifacts. The store-artifact total lives on the + // "Artifacts" stat card. Label them "coverage checks" so the two + // different "totals" are not conflated. + let checks_covered: usize = cov_report.entries.iter().map(|e| e.covered).sum(); + let checks_total: usize = cov_report.entries.iter().map(|e| e.total).sum(); let cov_delta = bl.map_or(String::new(), |s| { delta_pct_badge(overall, s.coverage.overall) }); @@ -249,7 +253,7 @@ pub(crate) fn render_stats(ctx: &RenderContext) -> String {
\ \