Cross-Judge-Aggregat Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: A dedicated /compare-judges view that aggregates per-bundle judge_quality.md results across judge models, grouped by pack, with per-metric winner highlighting — answering "which model is the better judge".
Architecture: Enrich F's judge_quality.md YAML frontmatter (backward-compatibly) with pack + the 3 missing rubric rates, then a new pure module touchstone/gui/judge_compare.py discovers + parses every judge_quality.md under runs_dir, groups rows by pack, and computes per-metric winners (mirroring aggregate._winner). A thin GET route renders a grouped table.
Tech Stack: Python 3.12 · FastAPI · Jinja2 · PyYAML · pytest · mypy strict · ruff.
Global Constraints¶
- Never crash on file content.
parse_frontmatterandjudge_quality_rowsmust tolerate arbitrary.mdcontent / malformed YAML → returnNone/ skip the row, never raise. The route never 500s. - Frontmatter enrichment is additive + backward-compatible. Only NEW frontmatter lines are added to
render_judge_quality_md; the existingrender_judge_quality_mdBODY (Headline + 3 sections) stays byte-identical. Oldjudge_quality.mdfiles lacking the new fields parse toNonefor those fields → rendered as "—". detail.get("pack")is defensive.render_judge_quality_mdmust read the pack viadetail.get("pack")(NOTdetail["pack"]) — an existing unit test calls it with a detail dict that has nopackkey; missing pack → frontmatterpack: —.- Comparability = group by pack. Winners are computed ONLY within a pack group.
mean_abs_deltalower = better; the 4 rubric rates higher = better. Tie → no winner (None), mirroringaggregate._winner(leaders[0] if len(leaders) == 1 else None). - Routing: the route is
/compare-judges(flat) — NOT/compare/judges(which the existing/compare/{name}route atapp.py:385would shadow asname="judges"). - Rates are fractions 0..1 in the frontmatter (like the existing
names_improvement_rate = round(n/t, 2)); the template renders them× 100as%. - Skip trashed bundles (
.trashin the path) — a deleted bundle must not appear in the comparison. - Gate (every task keeps these green):
uv run pytest -q·uv run mypy touchstone/·uv run ruff check . && uv run ruff format --check ..
File Structure¶
- Modify
touchstone/gui/judge_meta.py— enrich the frontmatter block inrender_judge_quality_md(~lines 243–251): addpack+ 3 rubric-rate lines. Body untouched. - Create
touchstone/gui/judge_compare.py— pure:parse_frontmatter,JudgeQualityRow,judge_quality_rows,PackGroup,group_by_pack(+_winner). - Modify
touchstone/gui/app.py—GET /compare-judgesinsidecreate_app(). - Create
touchstone/gui/templates/compare_judges.html— grouped table. - Modify
touchstone/gui/templates/compare.html— a link to/compare-judgesin the page header. - Modify
tests/test_judge_meta.py—_Packgains.id; a new test for the enriched frontmatter. - Create
tests/test_judge_compare.py— pure-logic tests. - Create
tests/test_gui_compare_judges.py— route test.
Task 1: Enrich F's judge_quality.md frontmatter¶
Files:
- Modify: touchstone/gui/judge_meta.py (the frontmatter block inside render_judge_quality_md, ~lines 243–251)
- Test: tests/test_judge_meta.py (add .id to _Pack; new test)
Interfaces:
- Consumes: RubricSummary (.cites_evidence, .justifies_level, .catches_safety, each tuple[int,int]); detail.get("pack") (a Pack with .id, or absent).
- Produces: render_judge_quality_md output whose frontmatter now also contains pack:, cites_evidence_rate:, justifies_level_rate:, catches_safety_rate: (each a fraction round(n/t, 2) or null).
- [ ] Step 1: Write the failing test
In tests/test_judge_meta.py, first give _Pack an id — change its __init__ (currently around line 86–89) so the first line of the body sets self.id:
class _Pack:
def __init__(self):
self.id = "ndassist"
self.dimensions = [_Dim("Q1", 1), _Dim("Q2", 1)]
self.ko_rule = _Ko()
Then add this new test at the end of the file:
def test_render_judge_quality_md_frontmatter_enriched():
pk = _Pack()
local = [ModelReport(model="m", variant="baseline", dim_scores={"Q1": 4, "Q2": 5})]
rows = [{"model": "m", "variant": "baseline", "safety_passed": True}]
fresh = parse_meta_response(
"cells:\n - model: m\n variant: baseline\n"
" fresh_scores: {dimensions: {Q1: 4, Q2: 2}, ko_fired: false, overall: Ja}\n"
" critique:\n dimensions:\n"
" Q1: {cites_evidence: false, names_improvement: false, justifies_level: true, catches_safety: true}\n"
" summary: ''\n"
)
agg = compute_agreement(pk, local, rows, fresh)
rub = aggregate_rubric(local, fresh)
detail = {"run_dir": Path("runs/2026_eval_x"), "manifest": {"judge": {"model": "j"}}, "pack": pk}
md = render_judge_quality_md(detail, agg, rub, fresh)
# enriched frontmatter: pack id + the 3 previously body-only rubric rates
assert "pack: ndassist" in md
# Q1 is the only scored critique dim (local<5): cites false→0/1, justifies true→1/1, safety true→1/1
assert "cites_evidence_rate: 0.0" in md
assert "justifies_level_rate: 1.0" in md
assert "catches_safety_rate: 1.0" in md
def test_render_judge_quality_md_tolerates_missing_pack():
# An older caller may pass a detail without "pack" — must not raise; frontmatter pack: —
pk = _Pack()
local = [ModelReport(model="m", variant="baseline", dim_scores={"Q1": 4, "Q2": 5})]
rows = [{"model": "m", "variant": "baseline", "safety_passed": True}]
fresh = parse_meta_response(
"cells:\n - model: m\n variant: baseline\n"
" fresh_scores: {dimensions: {Q1: 4, Q2: 2}, ko_fired: false, overall: Ja}\n"
" critique: {dimensions: {}, summary: ''}\n"
)
agg = compute_agreement(pk, local, rows, fresh)
rub = aggregate_rubric(local, fresh)
detail = {"run_dir": Path("runs/x"), "manifest": {}} # no "pack" key
md = render_judge_quality_md(detail, agg, rub, fresh)
assert "pack: —" in md
- [ ] Step 2: Run tests to verify they fail
Run: uv run pytest tests/test_judge_meta.py -q -k frontmatter_enriched or test_render_judge_quality_md_tolerates_missing_pack
Expected: FAIL — the new frontmatter lines aren't emitted yet (pack: / *_rate: assertions fail).
- [ ] Step 3: Enrich the frontmatter block
In touchstone/gui/judge_meta.py, inside render_judge_quality_md, replace the frontmatter-building block (currently):
bmad = agreement.bundle_mean_abs_delta
ni_n, ni_t = rubric.names_improvement
w("---")
w('type: "judge_quality"')
w(f"bundle: {bundle}")
w(f'judge_model: "{judge_model}"')
w(f"mean_abs_delta: {bmad if bmad is not None else 'null'}")
w(f"names_improvement_rate: {round(ni_n / ni_t, 2) if ni_t else 'null'}")
w("---\n")
with:
bmad = agreement.bundle_mean_abs_delta
ni_n, ni_t = rubric.names_improvement
ce_n, ce_t = rubric.cites_evidence
jl_n, jl_t = rubric.justifies_level
cs_n, cs_t = rubric.catches_safety
pack_obj = detail.get("pack")
pack_id = pack_obj.id if pack_obj is not None else "—"
w("---")
w('type: "judge_quality"')
w(f"bundle: {bundle}")
w(f"pack: {pack_id}")
w(f'judge_model: "{judge_model}"')
w(f"mean_abs_delta: {bmad if bmad is not None else 'null'}")
w(f"names_improvement_rate: {round(ni_n / ni_t, 2) if ni_t else 'null'}")
w(f"cites_evidence_rate: {round(ce_n / ce_t, 2) if ce_t else 'null'}")
w(f"justifies_level_rate: {round(jl_n / jl_t, 2) if jl_t else 'null'}")
w(f"catches_safety_rate: {round(cs_n / cs_t, 2) if cs_t else 'null'}")
w("---\n")
(Only frontmatter lines added; the body below this block is unchanged.)
- [ ] Step 4: Run tests to verify they pass
Run: uv run pytest tests/test_judge_meta.py -q
Expected: PASS — the new tests plus all pre-existing judge_meta tests (including test_render_judge_quality_md_has_all_sections, which passes pack: — since its detail has no pack key and asserts nothing about pack).
- [ ] Step 5: Commit
git add touchstone/gui/judge_meta.py tests/test_judge_meta.py
git commit -m "feat(judge_meta): enrich judge_quality.md frontmatter (pack + 3 rubric rates)"
Task 2: Pure frontmatter parser + row discovery¶
Files:
- Create: touchstone/gui/judge_compare.py
- Test: tests/test_judge_compare.py (create)
Interfaces:
- Consumes: judge_quality.md files under runs_dir with the frontmatter from Task 1.
- Produces: parse_frontmatter(text: str) -> dict[str, Any] | None; JudgeQualityRow dataclass (fields: bundle, pack, judge_model: str; mean_abs_delta, names_improvement_rate, cites_evidence_rate, justifies_level_rate, catches_safety_rate: float | None; key property = f"{bundle}|{judge_model}"); judge_quality_rows(runs_dir: Path) -> list[JudgeQualityRow]; module constant PACK_UNKNOWN: str.
- [ ] Step 1: Write the failing tests
Create tests/test_judge_compare.py:
from pathlib import Path
from touchstone.gui.judge_compare import (
JudgeQualityRow,
PACK_UNKNOWN,
judge_quality_rows,
parse_frontmatter,
)
FM = (
'---\ntype: "judge_quality"\nbundle: b1\npack: ndassist\n'
'judge_model: "qwen3-27b"\nmean_abs_delta: 0.5\nnames_improvement_rate: 0.75\n'
"cites_evidence_rate: 0.5\njustifies_level_rate: 1.0\ncatches_safety_rate: 0.25\n---\n# body\n"
)
def test_parse_frontmatter_valid():
fm = parse_frontmatter(FM)
assert fm is not None
assert fm["type"] == "judge_quality" and fm["pack"] == "ndassist"
assert fm["mean_abs_delta"] == 0.5
def test_parse_frontmatter_no_block():
assert parse_frontmatter("# just a heading\nno frontmatter here\n") is None
def test_parse_frontmatter_malformed_yaml():
# unterminated/garbage YAML inside a --- block → None, never raises
assert parse_frontmatter("---\n: : : not yaml : :\n[unclosed\n---\n") is None
def test_parse_frontmatter_non_mapping():
assert parse_frontmatter("---\n- a\n- b\n---\n") is None
def _write_jq(d: Path, text: str) -> None:
d.mkdir(parents=True, exist_ok=True)
(d / "judge_quality.md").write_text(text, encoding="utf-8")
def test_judge_quality_rows_discovers_and_parses(tmp_path):
_write_jq(tmp_path / "b1", FM)
_write_jq(
tmp_path / "b2",
'---\ntype: "judge_quality"\nbundle: b2\npack: buero\n'
'judge_model: "gpt-x"\nmean_abs_delta: 1.2\nnames_improvement_rate: 0.3\n'
"cites_evidence_rate: 0.4\njustifies_level_rate: 0.6\ncatches_safety_rate: 0.9\n---\n",
)
rows = judge_quality_rows(tmp_path)
assert len(rows) == 2
by = {r.bundle: r for r in rows}
assert by["b1"].pack == "ndassist" and by["b1"].judge_model == "qwen3-27b"
assert by["b1"].mean_abs_delta == 0.5 and by["b1"].cites_evidence_rate == 0.5
assert by["b1"].key == "b1|qwen3-27b"
def test_judge_quality_rows_skips_malformed_and_foreign(tmp_path):
_write_jq(tmp_path / "good", FM)
_write_jq(tmp_path / "broken", "not a frontmatter file at all\n")
_write_jq(tmp_path / "foreign", '---\ntype: "something_else"\nbundle: z\n---\n')
rows = judge_quality_rows(tmp_path)
assert [r.bundle for r in rows] == ["good"] # broken + foreign skipped
def test_judge_quality_rows_missing_rates_and_pack(tmp_path):
# an old judge_quality.md without pack / the 3 new rates → PACK_UNKNOWN + None rates
_write_jq(
tmp_path / "old",
'---\ntype: "judge_quality"\nbundle: old\njudge_model: "j"\n'
"mean_abs_delta: 0.4\nnames_improvement_rate: 0.5\n---\n",
)
rows = judge_quality_rows(tmp_path)
assert len(rows) == 1
r = rows[0]
assert r.pack == PACK_UNKNOWN
assert r.cites_evidence_rate is None and r.justifies_level_rate is None
def test_judge_quality_rows_skips_trash(tmp_path):
_write_jq(tmp_path / "live", FM)
_write_jq(tmp_path / ".trash" / "deleted", FM)
rows = judge_quality_rows(tmp_path)
assert [r.bundle for r in rows] == ["b1"] # the .trash copy is skipped (bundle field b1)
- [ ] Step 2: Run tests to verify they fail
Run: uv run pytest tests/test_judge_compare.py -q
Expected: FAIL with ModuleNotFoundError: No module named 'touchstone.gui.judge_compare'.
- [ ] Step 3: Create the module (parser + rows)
Create touchstone/gui/judge_compare.py:
"""Cross-judge aggregate (sub-project 2): discover per-bundle judge_quality.md results,
parse their YAML frontmatter, and (in group_by_pack) compute per-metric winners within a
pack. Pure + server-free (no fastapi/jinja) — mirrors gui/meta_report.py / aggregate.py."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import yaml
PACK_UNKNOWN = "(Pack unbekannt — vor SP2 erzeugt, neu einlesen)"
def parse_frontmatter(text: str) -> dict[str, Any] | None:
"""Parse a leading ``---\\n…\\n---`` YAML frontmatter block. Returns the mapping, or
None if there is no frontmatter / it is not a mapping / the YAML is malformed. Never raises."""
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return None
end = next((i for i in range(1, len(lines)) if lines[i].strip() == "---"), None)
if end is None:
return None
try:
data = yaml.safe_load("\n".join(lines[1:end]))
except Exception:
return None
return data if isinstance(data, dict) else None
def _as_float(v: Any) -> float | None:
"""A numeric frontmatter value → float; missing/null/non-numeric → None."""
if isinstance(v, bool):
return None
return float(v) if isinstance(v, (int, float)) else None
@dataclass
class JudgeQualityRow:
bundle: str
pack: str
judge_model: str
mean_abs_delta: float | None
names_improvement_rate: float | None
cites_evidence_rate: float | None
justifies_level_rate: float | None
catches_safety_rate: float | None
@property
def key(self) -> str:
return f"{self.bundle}|{self.judge_model}"
def judge_quality_rows(runs_dir: Path) -> list[JudgeQualityRow]:
"""Discover every judge_quality.md under runs_dir, parse its frontmatter into a row.
Skips trashed bundles, files without judge_quality frontmatter, and unreadable files."""
rows: list[JudgeQualityRow] = []
for p in sorted(runs_dir.rglob("judge_quality.md")):
if ".trash" in p.parts:
continue # a deleted bundle must not appear in the comparison
try:
fm = parse_frontmatter(p.read_text(encoding="utf-8"))
except OSError:
continue
if fm is None or fm.get("type") != "judge_quality":
continue
rows.append(
JudgeQualityRow(
bundle=str(fm.get("bundle") or p.parent.name),
pack=str(fm.get("pack") or PACK_UNKNOWN),
judge_model=str(fm.get("judge_model") or "—"),
mean_abs_delta=_as_float(fm.get("mean_abs_delta")),
names_improvement_rate=_as_float(fm.get("names_improvement_rate")),
cites_evidence_rate=_as_float(fm.get("cites_evidence_rate")),
justifies_level_rate=_as_float(fm.get("justifies_level_rate")),
catches_safety_rate=_as_float(fm.get("catches_safety_rate")),
)
)
rows.sort(key=lambda r: (r.pack, r.bundle, r.judge_model))
return rows
- [ ] Step 4: Run tests to verify they pass
Run: uv run pytest tests/test_judge_compare.py -q
Expected: PASS (7 tests).
- [ ] Step 5: Commit
git add touchstone/gui/judge_compare.py tests/test_judge_compare.py
git commit -m "feat(judge_compare): frontmatter parser + judge_quality.md row discovery"
Task 3: Group by pack + per-metric winners¶
Files:
- Modify: touchstone/gui/judge_compare.py (append PackGroup, _winner, group_by_pack)
- Test: tests/test_judge_compare.py (add tests)
Interfaces:
- Consumes: JudgeQualityRow (from Task 2).
- Produces: PackGroup dataclass (pack: str, rows: list[JudgeQualityRow], winners: dict[str, str | None]); group_by_pack(rows: list[JudgeQualityRow]) -> list[PackGroup]. winners keys: "mean_abs_delta", "names_improvement_rate", "cites_evidence_rate", "justifies_level_rate", "catches_safety_rate"; each value is the winning row's .key or None.
- [ ] Step 1: Write the failing tests
Append to tests/test_judge_compare.py:
from touchstone.gui.judge_compare import PackGroup, group_by_pack # noqa: E402
def _row(bundle, pack, judge, mad, ni=0.5, ce=0.5, jl=0.5, cs=0.5):
return JudgeQualityRow(bundle, pack, judge, mad, ni, ce, jl, cs)
def test_group_by_pack_splits_and_picks_winners():
rows = [
_row("b1", "ndassist", "judgeA", 0.5, ce=0.8),
_row("b2", "ndassist", "judgeB", 0.2, ce=0.4), # lower mean|Δ| → calibration winner
_row("b3", "buero", "judgeA", 0.9),
]
groups = group_by_pack(rows)
assert [g.pack for g in groups] == ["buero", "ndassist"] # sorted by pack
nd = next(g for g in groups if g.pack == "ndassist")
assert nd.winners["mean_abs_delta"] == "b2|judgeB" # lower is better
assert nd.winners["cites_evidence_rate"] == "b1|judgeA" # higher is better
buero = next(g for g in groups if g.pack == "buero")
assert buero.winners["mean_abs_delta"] == "b3|judgeA" # sole row wins its group
def test_group_by_pack_tie_no_winner():
rows = [_row("b1", "p", "jA", 0.5), _row("b2", "p", "jB", 0.5)] # equal mean|Δ|
g = group_by_pack(rows)[0]
assert g.winners["mean_abs_delta"] is None # tie → no trophy
def test_group_by_pack_all_none_metric_no_winner():
rows = [
JudgeQualityRow("b1", "p", "jA", None, None, None, None, None),
JudgeQualityRow("b2", "p", "jB", 0.5, None, None, None, None),
]
g = group_by_pack(rows)[0]
assert g.winners["cites_evidence_rate"] is None # no scored values in this column
assert g.winners["mean_abs_delta"] == "b2|jB" # only b2 has a value → sole winner
- [ ] Step 2: Run tests to verify they fail
Run: uv run pytest tests/test_judge_compare.py -q -k group_by_pack
Expected: FAIL with ImportError: cannot import name 'group_by_pack'.
- [ ] Step 3: Append the grouping + winners logic
Append to touchstone/gui/judge_compare.py:
_HIGHER_IS_BETTER = [
"names_improvement_rate",
"cites_evidence_rate",
"justifies_level_rate",
"catches_safety_rate",
]
@dataclass
class PackGroup:
pack: str
rows: list[JudgeQualityRow]
winners: dict[str, str | None]
def _winner(rows: list[JudgeQualityRow], attr: str, higher: bool) -> str | None:
"""The row.key with the best (max if higher else min) non-None value of attr, or None
if no row has a value or there is a tie (mirrors aggregate._winner: tie → no trophy)."""
scored = [(getattr(r, attr), r.key) for r in rows if getattr(r, attr) is not None]
if not scored:
return None
best = (max if higher else min)(v for v, _ in scored)
leaders = [k for v, k in scored if v == best]
return leaders[0] if len(leaders) == 1 else None
def group_by_pack(rows: list[JudgeQualityRow]) -> list[PackGroup]:
"""Group rows by pack (only same-pack rows are comparable) and pick a per-metric winner
within each group: mean_abs_delta lower = better, the rubric rates higher = better."""
by: dict[str, list[JudgeQualityRow]] = {}
for r in rows:
by.setdefault(r.pack, []).append(r)
groups: list[PackGroup] = []
for pack in sorted(by):
grp = by[pack]
winners: dict[str, str | None] = {"mean_abs_delta": _winner(grp, "mean_abs_delta", False)}
for metric in _HIGHER_IS_BETTER:
winners[metric] = _winner(grp, metric, True)
groups.append(PackGroup(pack=pack, rows=grp, winners=winners))
return groups
- [ ] Step 4: Run tests to verify they pass
Run: uv run pytest tests/test_judge_compare.py -q
Expected: PASS (10 tests total).
- [ ] Step 5: Commit
git add touchstone/gui/judge_compare.py tests/test_judge_compare.py
git commit -m "feat(judge_compare): group_by_pack + per-metric winners (tie → no trophy)"
Task 4: Route + template + link from /compare¶
Files:
- Modify: touchstone/gui/app.py (add GET /compare-judges inside create_app(), near the /compare route ~app.py:383)
- Create: touchstone/gui/templates/compare_judges.html
- Modify: touchstone/gui/templates/compare.html (link in the page header)
- Test: tests/test_gui_compare_judges.py (create)
Interfaces:
- Consumes: judge_compare.judge_quality_rows(runs_dir), judge_compare.group_by_pack(rows) (from Tasks 2–3); render(name, request, **ctx) helper.
- Produces: GET /compare-judges (HTML).
- [ ] Step 1: Write the failing route tests
Create tests/test_gui_compare_judges.py:
import pytest
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
from touchstone.gui import app as gui_app
from touchstone.gui.control import RunRegistry
class _L:
def spawn(self, a):
return 1
def alive(self, p):
return False
def terminate(self, p):
return None
def _client(tmp_path):
reg = RunRegistry(runs_dir=tmp_path, launcher=_L())
return TestClient(gui_app.create_app(runs_dir=tmp_path, registry=reg))
def _jq(d, bundle, pack, judge, mad, ni, ce, jl, cs):
d.mkdir(parents=True, exist_ok=True)
(d / "judge_quality.md").write_text(
f'---\ntype: "judge_quality"\nbundle: {bundle}\npack: {pack}\n'
f'judge_model: "{judge}"\nmean_abs_delta: {mad}\nnames_improvement_rate: {ni}\n'
f"cites_evidence_rate: {ce}\njustifies_level_rate: {jl}\ncatches_safety_rate: {cs}\n---\n# x\n",
encoding="utf-8",
)
def test_compare_judges_renders_grouped_table(tmp_path):
_jq(tmp_path / "b1", "b1", "ndassist", "judgeA", 0.5, 0.7, 0.8, 0.6, 0.9)
_jq(tmp_path / "b2", "b2", "ndassist", "judgeB", 0.2, 0.4, 0.4, 0.5, 0.5)
r = _client(tmp_path).get("/compare-judges")
assert r.status_code == 200
assert "ndassist" in r.text
assert "judgeA" in r.text and "judgeB" in r.text
assert "🏆" in r.text # at least one per-metric winner highlighted
def test_compare_judges_empty_state(tmp_path):
r = _client(tmp_path).get("/compare-judges")
assert r.status_code == 200
assert "judge_quality" in r.text.lower() or "noch keine" in r.text.lower()
def test_compare_judges_tolerates_malformed_file(tmp_path):
_jq(tmp_path / "ok", "ok", "p", "j", 0.5, 0.5, 0.5, 0.5, 0.5)
(tmp_path / "bad").mkdir()
(tmp_path / "bad" / "judge_quality.md").write_text("garbage, no frontmatter\n", encoding="utf-8")
r = _client(tmp_path).get("/compare-judges")
assert r.status_code == 200 # malformed file skipped, never 500
assert "judge_quality" in r.text.lower() or "ok" in r.text
- [ ] Step 2: Run tests to verify they fail
Run: uv run pytest tests/test_gui_compare_judges.py -q
Expected: FAIL — /compare-judges returns 404 (route not registered).
- [ ] Step 3: Add the route
In touchstone/gui/app.py, inside create_app(), immediately after the compare_cross function ends (the /compare route, ~app.py:383), insert:
@app.get("/compare-judges", response_class=HTMLResponse)
def compare_judges(request: Request) -> HTMLResponse:
"""Cross-judge aggregate: judge_quality.md across bundles, grouped by pack, with a
per-metric winner per group. Read-only; scans runs_dir (no user path param)."""
from touchstone.gui import judge_compare
groups = judge_compare.group_by_pack(judge_compare.judge_quality_rows(runs_dir))
return render("compare_judges.html", request, groups=groups, active="compare")
- [ ] Step 4: Create the template
Create touchstone/gui/templates/compare_judges.html:
{% extends "base.html" %}
{% block title_suffix %} · Judge-Vergleich{% endblock %}
{% block breadcrumb %}
<nav class="breadcrumb muted text-sm"><a href="/compare">Vergleich</a> › Judge-Qualität</nav>
{% endblock %}
{% block body %}
<div class="page-header">
<h1 class="page-title">Judge-Qualität vergleichen</h1>
<span class="muted text-sm">welches Modell ist der bessere Judge — pro Pack</span>
</div>
{% if not groups %}
<div class="card">
<p class="muted">Noch keine <code>judge_quality.md</code> vorhanden. Erzeuge sie über die Karte
„Judge-Qualität (Meta-Eval)" auf einer Ergebnisseite (judge-meta Export → Cloud-KI → Einlesen).</p>
</div>
{% else %}
{% for g in groups %}
<div class="card">
<div class="card-title">Pack: {{ g.pack }}</div>
<p class="muted text-xs" style="margin-bottom:0.5rem">
Vergleichbar nur bei gleichem Pack + gleicher Cloud-Referenz. 🏆 = bester Wert in dieser Spalte
(niedrigstes mean|Δ|, höchste Raten); Gleichstand → keine Markierung.
</p>
<table class="bundle-table">
<thead>
<tr>
<th>Bundle</th>
<th>Judge-Modell</th>
<th>mean|Δ|</th>
<th>Verbesserung benannt</th>
<th>Belege</th>
<th>Höhe begründet</th>
<th>Sicherheit</th>
</tr>
</thead>
<tbody>
{% for r in g.rows %}
<tr>
<td>{{ r.bundle }}</td>
<td>{{ r.judge_model }}</td>
<td>{% if r.mean_abs_delta is not none %}{{ r.mean_abs_delta }}{% if g.winners.mean_abs_delta == r.key %} 🏆{% endif %}{% else %}<span class="muted">—</span>{% endif %}</td>
<td>{% if r.names_improvement_rate is not none %}{{ (r.names_improvement_rate * 100) | round | int }}%{% if g.winners.names_improvement_rate == r.key %} 🏆{% endif %}{% else %}<span class="muted">—</span>{% endif %}</td>
<td>{% if r.cites_evidence_rate is not none %}{{ (r.cites_evidence_rate * 100) | round | int }}%{% if g.winners.cites_evidence_rate == r.key %} 🏆{% endif %}{% else %}<span class="muted">—</span>{% endif %}</td>
<td>{% if r.justifies_level_rate is not none %}{{ (r.justifies_level_rate * 100) | round | int }}%{% if g.winners.justifies_level_rate == r.key %} 🏆{% endif %}{% else %}<span class="muted">—</span>{% endif %}</td>
<td>{% if r.catches_safety_rate is not none %}{{ (r.catches_safety_rate * 100) | round | int }}%{% if g.winners.catches_safety_rate == r.key %} 🏆{% endif %}{% else %}<span class="muted">—</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% endif %}
{% endblock %}
- [ ] Step 5: Add the link on /compare
In touchstone/gui/templates/compare.html, change the page header block (lines 5–8) from:
<div class="page-header">
<h1 class="page-title">Vergleich</h1>
<span class="muted text-sm">Läufe auswählen und vergleichen</span>
</div>
to:
<div class="page-header">
<h1 class="page-title">Vergleich</h1>
<span class="muted text-sm">Läufe auswählen und vergleichen</span>
<a href="/compare-judges" class="btn btn-secondary" style="margin-left:auto">Judge-Qualität vergleichen →</a>
</div>
- [ ] Step 6: Run tests to verify they pass
Run: uv run pytest tests/test_gui_compare_judges.py -q
Expected: PASS (3 tests).
- [ ] Step 7: Commit
git add touchstone/gui/app.py touchstone/gui/templates/compare_judges.html touchstone/gui/templates/compare.html tests/test_gui_compare_judges.py
git commit -m "feat(gui): /compare-judges cross-judge aggregate view + link from /compare"
Task 5: Gate + headless smoke (exit criteria)¶
Files: none (verification only).
- [ ] Step 1: Full suite — Run:
uv run pytest -q→ all pre-existing + new tests green. - [ ] Step 2: Types — Run:
uv run mypy touchstone/→ no errors. - [ ] Step 3: Lint + format — Run:
uv run ruff check . && uv run ruff format --check .→ clean (if format flags a file,uv run ruff format .and amend). - [ ] Step 4: Headless smoke (GUI restarted). Re-ingest 1–2 real judged bundles through the enriched renderer so their judge_quality.md carries the new frontmatter (the GUI judge-meta card → paste a filled response, OR
touchstone judge-meta ingest), then start a fresh server and confirm the view:
# regenerate enriched judge_quality.md for a real judged bundle (CLI path, no GUI needed):
# uv run touchstone judge-meta export runs/<judged_bundle>
# (fill runs/<judged_bundle>/judge_meta_response.yaml)
# uv run touchstone judge-meta ingest runs/<judged_bundle>
# then, against a fresh server:
curl -s "http://127.0.0.1:<port>/compare-judges" | grep -o "Pack: [^<]*" | head
curl -s "http://127.0.0.1:<port>/compare-judges" | grep -c "🏆"
Expected: the page groups by pack, lists the judge models, marks per-metric winners; an old judge_quality.md without the new fields shows "—" (no crash); a bundle with no judge_quality.md is simply absent.
- [ ] Step 5: Adversarial whole-branch review (controller, 3 lenses) → fix confirmed findings → merge. Focus: (1)
parse_frontmatter/judge_quality_rowsnever raise on arbitrary.mdgarbage; (2) winner math (min for mean|Δ| / max for rates, None excluded, tie→None, only within pack); (3) frontmatter enrichment is additive + backward-compatible and therender_judge_quality_mdBODY bytes are unchanged. Then mergefeat/cross-judge-aggregate→main+ push (no PR, solo repo).
Self-Review¶
1. Spec coverage: Frontmatter enrichment (pack + 3 rates) → Task 1. parse_frontmatter + JudgeQualityRow + judge_quality_rows (skip malformed/foreign/trash, missing→None, PACK_UNKNOWN) → Task 2. group_by_pack + winners (lower mean|Δ| / higher rates, tie→None, within-pack) → Task 3. /compare-judges route (flat, not /compare/judges) + grouped template (rates ×100 as %, 🏆, comparability caveat, empty state) + link on /compare → Task 4. never-500/never-crash → Tasks 2–4 tests. Gate + smoke + adversarial review → Task 5. All spec sections mapped.
2. Placeholder scan: No TBD/TODO; every code step shows complete code; <port>/<judged_bundle> in Task 5 are runtime values for a manual smoke, not code placeholders.
3. Type consistency: JudgeQualityRow fields + .key identical across Tasks 2–4; winners dict keys (mean_abs_delta, names_improvement_rate, cites_evidence_rate, justifies_level_rate, catches_safety_rate) match between group_by_pack (Task 3) and the template (Task 4); PACK_UNKNOWN defined in Task 2, asserted in Task 2 tests; the frontmatter field names written in Task 1 (pack, *_rate) match exactly what judge_quality_rows reads in Task 2; rates are fractions 0..1 in Task 1's output and rendered × 100 in Task 4. render_judge_quality_md uses detail.get("pack") (defensive) consistent with the existing no-pack test.