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''
+ )
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("