From 0b81f8c55c21fa1a251fbc2b3270b271bf09815c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alexander=20K=C3=B6lnberger?=
<159939812+ProfRandom92@users.noreply.github.com>
Date: Tue, 19 May 2026 04:37:55 -0700
Subject: [PATCH] Add deterministic SVG degradation curve renderer
---
docs/benchmarks/layered_admissibility.md | 10 +-
docs/media/layered_admissibility_curve.svg | 36 ++++++
scripts/render_curve_svg.py | 21 ++++
src/visualization/__init__.py | 1 +
src/visualization/svg_curve_renderer.py | 131 +++++++++++++++++++++
tests/test_svg_curve_renderer.py | 66 +++++++++++
6 files changed, 263 insertions(+), 2 deletions(-)
create mode 100644 docs/media/layered_admissibility_curve.svg
create mode 100644 scripts/render_curve_svg.py
create mode 100644 src/visualization/__init__.py
create mode 100644 src/visualization/svg_curve_renderer.py
create mode 100644 tests/test_svg_curve_renderer.py
diff --git a/docs/benchmarks/layered_admissibility.md b/docs/benchmarks/layered_admissibility.md
index 30bb1f9..e6d098c 100644
--- a/docs/benchmarks/layered_admissibility.md
+++ b/docs/benchmarks/layered_admissibility.md
@@ -27,8 +27,14 @@ Deterministically compare admissibility outcomes across fixture bundles using Co
- no fuzzy matching
- no semantic equivalence
+## Visualization
+
+
+
+This SVG is a deterministic benchmark artifact generated directly from `artifacts/layered_admissibility_results.json` via the hand-written renderer (`src/visualization/svg_curve_renderer.py`). Rendering is pure SVG text generation with fixed canvas geometry, stable ordering, and fixed float precision (three decimals), so output is CI-friendly and reproducible with no stochastic rendering.
+
## Future
- add more fixture families
-- add progressive degradation levels
-- add SVG curve visualization later
+- extend deterministic benchmark artifacts
+- keep visualization static and reproducible
diff --git a/docs/media/layered_admissibility_curve.svg b/docs/media/layered_admissibility_curve.svg
new file mode 100644
index 0000000..ee88b66
--- /dev/null
+++ b/docs/media/layered_admissibility_curve.svg
@@ -0,0 +1,36 @@
+
diff --git a/scripts/render_curve_svg.py b/scripts/render_curve_svg.py
new file mode 100644
index 0000000..032f128
--- /dev/null
+++ b/scripts/render_curve_svg.py
@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+import sys
+
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
+if str(PROJECT_ROOT) not in sys.path:
+ sys.path.insert(0, str(PROJECT_ROOT))
+
+from src.visualization.svg_curve_renderer import SVGCurveRenderer
+
+INPUT_PATH = Path("artifacts/layered_admissibility_results.json")
+OUTPUT_PATH = Path("docs/media/layered_admissibility_curve.svg")
+
+
+if __name__ == "__main__":
+ payload = json.loads(INPUT_PATH.read_text(encoding="utf-8"))
+ svg = SVGCurveRenderer().render(payload)
+ OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
+ OUTPUT_PATH.write_text(svg, encoding="utf-8")
diff --git a/src/visualization/__init__.py b/src/visualization/__init__.py
new file mode 100644
index 0000000..703e279
--- /dev/null
+++ b/src/visualization/__init__.py
@@ -0,0 +1 @@
+"""Deterministic visualization helpers."""
diff --git a/src/visualization/svg_curve_renderer.py b/src/visualization/svg_curve_renderer.py
new file mode 100644
index 0000000..ece999a
--- /dev/null
+++ b/src/visualization/svg_curve_renderer.py
@@ -0,0 +1,131 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from html import escape
+
+
+@dataclass(frozen=True, slots=True)
+class _PointLayout:
+ fixture_id: str
+ score: float
+ x: float
+ y: float
+ failure_labels: tuple[str, ...]
+
+
+class SVGCurveRenderer:
+ WIDTH = 1000
+ HEIGHT = 520
+ MARGIN_LEFT = 90
+ MARGIN_RIGHT = 40
+ MARGIN_TOP = 70
+ MARGIN_BOTTOM = 140
+
+ TITLE = "Layered Admissibility Degradation Curve"
+ X_LABEL = "Fixture progression"
+ Y_LABEL = "overall_admissibility_score"
+
+ X_TICKS: tuple[tuple[str, str], ...] = (
+ ("coding_workflow_pr_review_v1", "positive"),
+ ("coding_workflow_pr_review_mild_v1", "mild"),
+ ("coding_workflow_pr_review_moderate_v1", "moderate"),
+ ("coding_workflow_pr_review_degraded_v1", "severe"),
+ )
+
+ LEGEND_ITEMS: tuple[str, ...] = ("structural", "relational", "operational", "governance")
+
+ FAILURE_ANNOTATION_ORDER: tuple[str, ...] = (
+ "RECOVERY_PATH_INVALID",
+ "CAUSAL_DEPENDENCY_LOSS",
+ "POLICY_ORDER_BROKEN",
+ "INVARIANT_VIOLATION",
+ )
+
+ def _fmt(self, value: float) -> str:
+ return f"{value:.3f}"
+
+ def _layout_points(self, curve_json: dict) -> tuple[_PointLayout, ...]:
+ points_by_fixture = {point["fixture_id"]: point for point in curve_json["points"]}
+ plot_width = self.WIDTH - self.MARGIN_LEFT - self.MARGIN_RIGHT
+ plot_height = self.HEIGHT - self.MARGIN_TOP - self.MARGIN_BOTTOM
+
+ layouts: list[_PointLayout] = []
+ for index, (fixture_id, _) in enumerate(self.X_TICKS):
+ point = points_by_fixture[fixture_id]
+ score = float(point["overall_admissibility_score"])
+ x = self.MARGIN_LEFT + (plot_width * index / (len(self.X_TICKS) - 1))
+ y = self.MARGIN_TOP + ((1.0 - score) * plot_height)
+ layouts.append(
+ _PointLayout(
+ fixture_id=fixture_id,
+ score=score,
+ x=x,
+ y=y,
+ failure_labels=tuple(sorted(point["failure_labels"])),
+ )
+ )
+ return tuple(layouts)
+
+ def render(self, curve_json: dict) -> str:
+ layouts = self._layout_points(curve_json)
+ plot_bottom = self.HEIGHT - self.MARGIN_BOTTOM
+ plot_right = self.WIDTH - self.MARGIN_RIGHT
+
+ polyline_points = " ".join(f"{self._fmt(p.x)},{self._fmt(p.y)}" for p in layouts)
+ elements: list[str] = [
+ f'")
+ return "\n".join(elements) + "\n"
diff --git a/tests/test_svg_curve_renderer.py b/tests/test_svg_curve_renderer.py
new file mode 100644
index 0000000..f647864
--- /dev/null
+++ b/tests/test_svg_curve_renderer.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import json
+import re
+from pathlib import Path
+
+from src.visualization.svg_curve_renderer import SVGCurveRenderer
+
+INPUT_PATH = Path("artifacts/layered_admissibility_results.json")
+SVG_PATH = Path("docs/media/layered_admissibility_curve.svg")
+
+
+def _render() -> str:
+ payload = json.loads(INPUT_PATH.read_text(encoding="utf-8"))
+ return SVGCurveRenderer().render(payload)
+
+
+def test_svg_render_is_deterministic() -> None:
+ assert _render() == _render()
+
+
+def test_svg_root_exists() -> None:
+ output = _render()
+ assert output.startswith('