From d733e2c4ca3210fb09d458d0aaff8e5c89d2cfdc Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Tue, 9 Jun 2026 02:26:44 +0000 Subject: [PATCH] feat(card): badge differentiates expected vs unexpected skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The level badge gains a third segment derived from level_cap_intent: - amber 'gap?' when the climb was capped by an UNDECLARED gap-sensitive N/A (backup_restore / functional) — a likely-missing test (unexpected skip) - muted 'expected' when capped by a DECLARED intentional N/A (reviewed, nothing to fix) - nothing extra for a clean cap, a full climb, or a real failure. Font-safe text labels (no emoji) so the SVG renders headless/anywhere. Badge never inflates — it only annotates the cap the level already reflects. Co-Authored-By: Claude Opus 4.8 --- runner/harness/card.py | 42 +++++++++++++++++++++++++++++++++++++---- runner/run_recipe_ci.py | 8 +++++++- tests/unit/test_card.py | 13 +++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/runner/harness/card.py b/runner/harness/card.py index 36b717f..1cee8a6 100644 --- a/runner/harness/card.py +++ b/runner/harness/card.py @@ -79,10 +79,44 @@ def render_badge_svg(label: str, message: str, color: str) -> str: ) -def level_badge_svg(level: int, cap_reason: str = "") -> str: - """Per-recipe/-run LEVEL badge: 'cc-ci | level N'. Colour by level (R6).""" - msg = f"level {int(level)}" - return render_badge_svg("cc-ci", msg, level_color(level)) +# Third-segment colours for the level badge: amber = an UNEXPECTED skip (undeclared gap-sensitive +# N/A — likely missing coverage) capped the climb; muted = an EXPECTED skip (declared intentional +# N/A — reviewed, nothing to fix). Font-safe text labels (no emoji) so the SVG renders anywhere. +GAP_COLOR = "#d29922" +EXPECT_COLOR = "#6e7681" + + +def level_badge_svg(level: int, cap_reason: str = "", cap_intent: str = "") -> str: + """Per-recipe/-run LEVEL badge: 'cc-ci | level N' coloured by level (R6), with a THIRD segment + that differentiates *why* the climb stopped when an N/A capped it: + - undeclared gap-sensitive N/A (an UNEXPECTED skip — likely missing coverage): amber 'gap?'. + - declared intentional N/A (an EXPECTED skip — reviewed, nothing to fix): muted 'expected'. + - clean cap / full climb / a real failure: no third segment (the level + card carry it). + Derived from `cap_intent` (results.level_cap_intent) so the badge never inflates — it only + annotates the cap the level already reflects.""" + label, msg = "cc-ci", f"level {int(level)}" + lw, mw = _text_width(label), _text_width(msg) + third: tuple[str, str] | None = None + if cap_intent.startswith("undeclared"): + third = ("gap?", GAP_COLOR) + elif cap_intent.startswith("intentional"): + third = ("expected", EXPECT_COLOR) + if third is None: + return render_badge_svg(label, msg, level_color(level)) + txt, tcolor = third + tw = _text_width(txt) + w = lw + mw + tw + return ( + f'' + f'' + f'' + f'' + f'' + f'{html.escape(label)}' + f'{html.escape(msg)}' + f'{html.escape(txt)}' + ) def _stage_rows(stages: list[dict]) -> str: diff --git a/runner/run_recipe_ci.py b/runner/run_recipe_ci.py index becc9c4..b10966a 100644 --- a/runner/run_recipe_ci.py +++ b/runner/run_recipe_ci.py @@ -1296,7 +1296,13 @@ def main() -> int: f.write(card_mod.render_card_html(data, screenshot_rel=data.get("screenshot"))) png = card_mod.render_card_png(html_path, os.path.join(run_artifact_dir, "summary.png")) with open(os.path.join(run_artifact_dir, "badge.svg"), "w", encoding="utf-8") as f: - f.write(card_mod.level_badge_svg(data["level"], data.get("level_cap_reason", ""))) + f.write( + card_mod.level_badge_svg( + data["level"], + data.get("level_cap_reason", ""), + data.get("level_cap_intent", ""), + ) + ) print( f"summary card {'rendered ' + png if png else '(PNG render unavailable)'} + " f"badge.svg written into {run_artifact_dir}", diff --git a/tests/unit/test_card.py b/tests/unit/test_card.py index cdafc02..1abce04 100644 --- a/tests/unit/test_card.py +++ b/tests/unit/test_card.py @@ -51,6 +51,19 @@ def test_badge_svg_wellformed(): assert svg.startswith("") assert "level 4" in svg assert C.level_color(4) in svg + # plain cap (no intent) → two-box badge, no third segment + assert "expected" not in svg and "gap?" not in svg + + +def test_badge_svg_differentiates_expected_vs_unexpected_skip(): + # declared intentional N/A capped the climb → muted "expected" third segment + exp = C.level_badge_svg(2, "L3 backup/restore N/A", "intentional · no persistent data") + assert "level 2" in exp and "expected" in exp and C.EXPECT_COLOR in exp + assert "gap?" not in exp + # undeclared gap-sensitive N/A → amber "gap?" third segment (an UNEXPECTED skip) + gap = C.level_badge_svg(2, "L3 backup/restore N/A", "undeclared N/A — possible coverage gap") + assert "level 2" in gap and "gap?" in gap and C.GAP_COLOR in gap + assert "expected" not in gap def test_card_html_reports_level_verbatim():