diff --git a/runner/harness/card.py b/runner/harness/card.py
index e57d517..21418dc 100644
--- a/runner/harness/card.py
+++ b/runner/harness/card.py
@@ -141,6 +141,43 @@ def _stage_rows(stages: list[dict]) -> str:
return "\n".join(rows) or '
| no stages |
'
+# Friendly rung labels for the skip rows.
+RUNG_LABEL = {
+ "install": "install",
+ "upgrade": "upgrade",
+ "backup_restore": "backup/restore",
+ "functional": "functional",
+ "integration": "integration",
+ "recipe_local": "recipe-local",
+}
+SKIP_GREEN = "#57ab5a" # muted green — an intentional skip reads like a pass (but labelled, never inflating)
+
+
+def _skip_rows(skips: dict) -> str:
+ """Render SKIPPED rungs as stage-like rows. An intentional (declared) skip looks like a pass row
+ but its status says 'INTENTIONAL SKIP' (muted green) with the declared reason on the line below;
+ an unintentional skip is amber 'UNINTENTIONAL SKIP' with a prompt to add a test or declare it."""
+ rows = []
+ for rung, reason in (skips.get("intentional") or {}).items():
+ rows.append(
+ f'| ⊘'
+ f'{html.escape(RUNG_LABEL.get(rung, rung))} | '
+ f'intentional skip |
'
+ )
+ rows.append(f' | {html.escape(reason)} |
')
+ for rung in skips.get("unintentional") or []:
+ rows.append(
+ f'| ⊘'
+ f'{html.escape(RUNG_LABEL.get(rung, rung))} | '
+ f'unintentional skip |
'
+ )
+ rows.append(
+ ' | not declared in EXPECTED_NA — add the '
+ "missing test/label, or declare the skip with a reason |
"
+ )
+ return "\n".join(rows)
+
+
def render_card_html(data: dict, screenshot_rel: str | None = "screenshot.png") -> str:
"""Build the summary-card HTML from a results.json dict. `screenshot_rel` is the relative path to
the screenshot PNG (same dir as the card) — omitted from the card if None / absent.
@@ -151,15 +188,8 @@ def render_card_html(data: dict, screenshot_rel: str | None = "screenshot.png")
version = html.escape(str(data.get("version") or data.get("ref") or ""))
level = int(data.get("level", 0))
cap_reason = str(data.get("level_cap_reason") or "")
- # Annotate the cap line by whether the capping rung was an intentional skip (declared, with its
- # reason) or an unintentional one (skipped but not declared).
- capped = data.get("level_cap_rung")
- sk = data.get("skips", {}) or {}
- if capped and capped in (sk.get("intentional") or {}):
- cap_reason += f" · intentional: {sk['intentional'][capped]}"
- elif capped and capped in (sk.get("unintentional") or []):
- cap_reason += " · unintentional skip (no EXPECTED_NA — add a test or declare it)"
cap = html.escape(cap_reason)
+ sk = data.get("skips", {}) or {}
color = level_color(level)
flags = data.get("flags", {}) or {}
flag_bits = []
@@ -175,7 +205,7 @@ def render_card_html(data: dict, screenshot_rel: str | None = "screenshot.png")
if show_shot
else 'no screenshot
'
)
- rows = _stage_rows(data.get("stages", []))
+ rows = _stage_rows(data.get("stages", [])) + "\n" + _skip_rows(sk)
return f"""