Skip to content

Commit f2329f0

Browse files
authored
Merge pull request #651 from pulseengine/feat/req-244-overview-badge
feat(serve): overview flags per-artifact lifecycle gaps with a badge (REQ-244 pt2, #622)
2 parents d248526 + 837aaf6 commit f2329f0

3 files changed

Lines changed: 53 additions & 3 deletions

File tree

artifacts/requirements.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7634,7 +7634,7 @@ artifacts:
76347634
- id: REQ-244
76357635
type: requirement
76367636
title: rivet serve overview surfaces per-artifact missing coverage
7637-
status: implemented
7637+
status: verified
76387638
description: "The `rivet serve` validation detail view shows what an artifact is missing, but the overview does not surface it. Expose the missing-coverage signal in the overview so gaps are visible without drilling into each artifact. #622."
76397639
provenance:
76407640
created-by: ai-assisted

rivet-cli/src/render/artifacts.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,14 +260,35 @@ pub(crate) fn render_artifacts_list(ctx: &RenderContext, params: &ViewParams) ->
260260
html.push_str("<th>Links</th><th data-col=\"tags\">Tags</th>");
261261
html.push_str("</tr></thead><tbody>");
262262

263+
// #622 (REQ-244): per-artifact lifecycle gaps, so the overview flags what's
264+
// missing to complete each artifact's trace — not only the per-artifact
265+
// validation view. Computed once over the store (same pass the artifacts API
266+
// uses); only the page's artifacts are looked up below.
267+
let gap_artifacts: Vec<rivet_core::model::Artifact> = store.iter().cloned().collect();
268+
let gaps: std::collections::HashMap<String, Vec<String>> =
269+
rivet_core::lifecycle::check_lifecycle_completeness(&gap_artifacts, ctx.schema, ctx.graph)
270+
.into_iter()
271+
.map(|g| (g.artifact_id, g.missing))
272+
.collect();
273+
263274
for a in page_artifacts {
264275
let status = a.status.as_deref().unwrap_or("-");
265-
let status_badge = match status {
276+
let mut status_cell = match status {
266277
"approved" => format!("<span class=\"badge badge-ok\">{status}</span>"),
267278
"draft" => format!("<span class=\"badge badge-warn\">{status}</span>"),
268279
"obsolete" => format!("<span class=\"badge badge-error\">{status}</span>"),
269280
_ => format!("<span class=\"badge badge-info\">{status}</span>"),
270281
};
282+
// Gap indicator: warn badge listing the missing downstream types,
283+
// appended to the status cell so it reads at a glance.
284+
if let Some(missing) = gaps.get(&a.id).filter(|m| !m.is_empty()) {
285+
status_cell.push_str(&format!(
286+
" <span class=\"badge badge-warn\" title=\"missing: {}\">\u{26a0} {} gap{}</span>",
287+
html_escape(&missing.join(", ")),
288+
missing.len(),
289+
if missing.len() == 1 { "" } else { "s" },
290+
));
291+
}
271292
let tags_csv = a.tags.join(",");
272293
let tags_display = if a.tags.is_empty() {
273294
String::from("-")
@@ -293,7 +314,7 @@ pub(crate) fn render_artifacts_list(ctx: &RenderContext, params: &ViewParams) ->
293314
<td data-tags=\"{}\">{}</td></tr>",
294315
badge_for_type(&a.artifact_type),
295316
html_escape(&a.title),
296-
status_badge,
317+
status_cell,
297318
a.links.len(),
298319
html_escape(&tags_csv),
299320
tags_display,

rivet-cli/tests/serve_integration.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,35 @@ fn api_artifacts_expose_missing_gaps() {
603603
child.wait().ok();
604604
}
605605

606+
/// #622 (REQ-244 pt2): the artifacts OVERVIEW page renders a lifecycle-gap
607+
/// badge (server-rendered HTML — no client JS), so gaps are visible at a
608+
/// glance, matching the per-artifact validation view. rivet's own corpus has
609+
/// requirements without a full downstream trace, so the requirement listing
610+
/// must contain the badge.
611+
///
612+
/// The badge is per-row, so it only appears on the page that actually holds a
613+
/// gapped artifact. rivet's gaps sit on `requirement` artifacts, which sort
614+
/// after the ~500-row page cap on the unfiltered list — so we filter to
615+
/// `types=requirement` to land them on page 1 (matching how a user would
616+
/// narrow the view to inspect requirement completeness).
617+
///
618+
/// rivet: verifies REQ-244
619+
#[test]
620+
fn overview_renders_lifecycle_gap_badge() {
621+
let (mut child, port) = start_server();
622+
623+
let (status, body, _headers) = fetch(port, "/artifacts?types=requirement&per_page=500", false);
624+
assert_eq!(status, 200);
625+
assert!(
626+
body.contains("gap</span>") || body.contains("gaps</span>"),
627+
"the artifacts overview must render a lifecycle-gap badge; \
628+
none found in the server-rendered HTML"
629+
);
630+
631+
child.kill().ok();
632+
child.wait().ok();
633+
}
634+
606635
#[test]
607636
fn api_artifacts_filter_by_type() {
608637
let (mut child, port) = start_server();

0 commit comments

Comments
 (0)