Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions rivet-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
Expand Down
10 changes: 7 additions & 3 deletions rivet-cli/src/render/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});
Expand All @@ -249,7 +253,7 @@ pub(crate) fn render_stats(ctx: &RenderContext) -> String {
<div class=\"status-bar-fill\" style=\"background:{cov_color};width:{overall:.1}%\"></div>\
</div>\
<div style=\"color:var(--text-secondary);font-size:.8rem;margin-top:.35rem\">\
{total_covered} / {total_items} artifacts covered across {} rules\
{checks_covered} / {checks_total} coverage checks across {} rules\
</div>\
</div>\
</div>\
Expand Down
29 changes: 29 additions & 0 deletions rivet-cli/tests/cli_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,35 @@ fn coverage_json() {
parsed.get("rules").and_then(|v| v.as_array()).is_some(),
"coverage JSON must contain 'rules' array"
);

// REQ-111: the overall aggregate sums per-rule denominators (an artifact in
// N rules counts N times), a different cardinality from `stats` JSON's
// distinct-artifact `total`. It must be exposed under disambiguated
// `checks_*` keys and must NOT reuse the bare `total`/`covered` names that
// would collide semantically with the stats command.
let overall = &parsed["overall"];
assert!(
overall
.get("checks_total")
.and_then(|v| v.as_u64())
.is_some(),
"coverage overall must expose 'checks_total'"
);
assert!(
overall
.get("checks_covered")
.and_then(|v| v.as_u64())
.is_some(),
"coverage overall must expose 'checks_covered'"
);
assert!(
overall.get("total").is_none(),
"coverage overall must NOT use the ambiguous key 'total' (REQ-111)"
);
assert!(
overall.get("covered").is_none(),
"coverage overall must NOT use the ambiguous key 'covered' (REQ-111)"
);
}

// ── rivet matrix ───────────────────────────────────────────────────────
Expand Down
Loading