Judge-Meta GUI-Oberfläche 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: Surface sub-project F's judge-meta round trip in the web GUI as a per-bundle "Judge-Qualität (Meta-Eval)" card: stream-only export (request .md + empty response .yaml), textarea paste-ingest → judge_quality.md + an inline summary.
Architecture: Three thin routes in create_app() that lazy-import the existing pure logic in touchstone/gui/judge_meta.py (unchanged). Two GET routes generate + stream the export artifacts on the fly (mirrors E's /export-meta-report); one POST route validates a pasted YAML, computes agreement + rubric, writes judge_quality.md, and returns an inline summary rendered by a new Jinja partial (mirrors the pack-editor's never-500 {ok:false} pattern). A new Alpine component drives the textarea POST. No /compare touch; F's logic and outputs are untouched.
Tech Stack: Python 3.12 · FastAPI · Jinja2 · Alpine.js · pytest (TestClient) · mypy strict · ruff.
Global Constraints¶
- Never-500 on bad input. A parse/schema error from a pasted YAML is a normal
{"ok": false, "errors": [...]}JSON result (HTTP 200), never an HTTP 500 — mirrors/packs/validate. - Run-dir confinement before disk access. Every route building a path from the
{name}segment doesrd = (runs_dir / name).resolve()thenif not rd.is_relative_to(runs_dir.resolve()) or not rd.is_dir(): raise HTTPException(status_code=404)BEFORE touching disk. - Judged-gate.
bundle_detail(rd) is None→ 404;not detail.get("reports")→ 400 with message"Bundle ohne Judge-Bewertung — erst touchstone judge". Applies to all three routes. - Stream-only export. The GET routes write nothing to the bundle. Only the POST ingest writes
judge_quality.md. - F logic frozen.
touchstone/gui/judge_meta.py,bundles.py,report_md.pyare consumed unchanged — no edits. No new artifact added to either export allowlist (judge_quality.mdis already in both). - Lazy import
from touchstone.gui import judge_metainside each handler (mirrorsmeta_reportatapp.py:269/286). - Gate (every task's tests must keep these green):
uv run pytest -q·uv run mypy touchstone/·uv run ruff check . && uv run ruff format --check ..
File Structure¶
- Modify
touchstone/gui/app.py— add 3 routes +_MAX_META_YAMLconstant insidecreate_app()(the two GET routes go right afterexport_meta_reportends at:317, beforeresult()at:319; the POST route + constant go in the same block). - Create
touchstone/gui/templates/macros/_judge_meta_summary.html— inline summary partial, rendered fromAgreementResult+RubricSummary. - Modify
touchstone/gui/templates/result.html— new card before the Export card (:477), gated{% if detail and detail.reports %}; load the new JS in the body block. - Create
touchstone/gui/static/judge_meta.js— AlpinejudgeMeta(name)component driving the ingest POST. - Create
tests/test_gui_judge_meta_surface.py— route + template tests (self-contained fixtures).
Task 1: GET export routes (request .md + template .yaml)¶
Files:
- Modify: touchstone/gui/app.py (add two GET routes inside create_app(), after line 317)
- Test: tests/test_gui_judge_meta_surface.py (create)
Interfaces:
- Consumes: bundles.bundle_detail(rd: Path) -> dict | None (keys pack, reports); judge_meta.render_request_md(detail: dict) -> str; judge_meta.empty_response_template(pack, cells: list[tuple[str,str]]) -> str. detail["reports"] is a list[ModelReport] with .model / .variant.
- Produces: GET /export-judge-meta-request/{name} (→ text/markdown attachment judge_meta_request.md); GET /export-judge-meta-template/{name} (→ application/x-yaml attachment judge_meta_response.yaml).
- [ ] Step 1: Write the failing tests + shared fixtures
Create tests/test_gui_judge_meta_surface.py:
import json
from pathlib import Path
import pytest
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
from touchstone.gui import app as gui_app
from touchstone.gui.control import RunRegistry
from touchstone.judge import write_reports_jsonl
from touchstone.pack import load_pack
from touchstone.results import ModelReport
PACK = "packs/ndassist.yaml"
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 _judged_bundle(d: Path):
"""A minimal judged bundle (bundle.json + responses + scores + reports.jsonl)."""
pk = load_pack(PACK)
first = next(p for _, p in pk.all_prompts())
d.mkdir(parents=True)
(d / "bundle.json").write_text(
json.dumps(
{
"pack_id": pk.id,
"pack_path": PACK,
"host": {"chip": "M5", "ram_gb": "64 GB"},
"date": "2026-06-24",
"judge": {"model": "qwen3-27b"},
}
),
encoding="utf-8",
)
base = {
"pack_id": pk.id, "pack_version": 1, "machine": "t", "model": "m", "quant": "q4",
"engine": "lm-studio", "engine_version": "0", "variant": "baseline", "category": "A",
"prompt_id": first.id, "repeat": 0, "response_text": "A.", "content_empty": False,
"ttft_s": 0.2, "decode_tps": 30.0, "prefill_tps": 90.0, "e2e_s": 1.5,
"prompt_tokens": 100, "completion_tokens": 50, "is_cold_start": False,
"power_source": "ac", "peak_rss_mb": 0.0, "sys_used_mb": 20000.0,
"mem_pressure_max": "normal", "throttled": False, "ok": True, "error": "",
"seed": 42, "t_start": 0.0, "t_end": 1.5, "reasoning_chars": 0,
}
(d / "responses.jsonl").write_text(json.dumps(base) + "\n", encoding="utf-8")
hdr = "model,variant,metric_type,metric,weight,score"
rows = [hdr] + [f"m,baseline,dimension,{dim.id},{dim.weight},4" for dim in pk.dimensions]
(d / "scores.csv").write_text("\n".join(rows) + "\n", encoding="utf-8")
write_reports_jsonl(
d / "reports.jsonl",
[
ModelReport(
model="m",
variant="baseline",
dim_scores={dim.id: 4 for dim in pk.dimensions},
dim_rationales={pk.dimensions[0].id: "gut"},
)
],
)
return pk
def _valid_response_yaml(pk) -> str:
dims = "{" + ", ".join(f"{dim.id}: 4" for dim in pk.dimensions) + "}"
crit = "\n".join(
f" {dim.id}: {{cites_evidence: true, names_improvement: true, "
f"justifies_level: true, catches_safety: true}}"
for dim in pk.dimensions
)
return (
"cells:\n - model: m\n variant: baseline\n"
f" fresh_scores: {{dimensions: {dims}, ko_fired: false, overall: Ja}}\n"
f" critique:\n dimensions:\n{crit}\n summary: 'ok'\n"
"recommendations:\n - 'Mehr Belege zitieren.'\n"
)
def test_export_request_streams_md_for_judged(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
r = _client(tmp_path).get(f"/export-judge-meta-request/{d.name}")
assert r.status_code == 200
assert r.headers["content-type"].startswith("text/markdown")
assert "judge_meta_request.md" in r.headers["content-disposition"]
assert "Teil A" in r.text and "Teil B" in r.text
def test_export_request_refuses_unjudged(tmp_path):
pk = load_pack(PACK)
d = tmp_path / "2026_eval_raw"
d.mkdir()
(d / "bundle.json").write_text(
json.dumps({"pack_id": pk.id, "pack_path": PACK, "host": {}, "date": "x"}),
encoding="utf-8",
)
(d / "responses.jsonl").write_text("", encoding="utf-8") # no reports.jsonl → unjudged
r = _client(tmp_path).get(f"/export-judge-meta-request/{d.name}")
assert r.status_code == 400
def test_export_request_traversal_404(tmp_path):
_judged_bundle(tmp_path / "2026_eval_nd")
r = _client(tmp_path).get("/export-judge-meta-request/..%2f..%2fetc")
assert r.status_code == 404
def test_export_template_streams_yaml_round_trips(tmp_path):
from touchstone.gui import judge_meta
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
r = _client(tmp_path).get(f"/export-judge-meta-template/{d.name}")
assert r.status_code == 200
assert r.headers["content-type"].startswith("application/x-yaml")
assert "judge_meta_response.yaml" in r.headers["content-disposition"]
parsed = judge_meta.parse_meta_response(r.text) # must round-trip
assert any(c.model == "m" and c.variant == "baseline" for c in parsed.cells)
def test_export_template_refuses_unjudged(tmp_path):
pk = load_pack(PACK)
d = tmp_path / "2026_eval_raw"
d.mkdir()
(d / "bundle.json").write_text(
json.dumps({"pack_id": pk.id, "pack_path": PACK, "host": {}, "date": "x"}),
encoding="utf-8",
)
(d / "responses.jsonl").write_text("", encoding="utf-8")
r = _client(tmp_path).get(f"/export-judge-meta-template/{d.name}")
assert r.status_code == 400
- [ ] Step 2: Run tests to verify they fail
Run: uv run pytest tests/test_gui_judge_meta_surface.py -q
Expected: FAIL — routes return 404 (not registered yet); test_export_request_traversal_404 may pass incidentally.
- [ ] Step 3: Add the two GET routes
In touchstone/gui/app.py, immediately after the export_meta_report function ends (after line 317, before @app.get("/result/{name}", ...) at line 319), insert:
@app.get("/export-judge-meta-request/{name}")
def export_judge_meta_request(name: str) -> Any:
"""Stream the judge-quality request markdown (Teil A blank fresh-scoring + Teil B the
local judge's rationales) for a judged bundle — to hand to an external cloud AI.
Stream-only: nothing is written to the bundle (the filled response returns via ingest)."""
from touchstone.gui import judge_meta
rd = (runs_dir / name).resolve()
if not rd.is_relative_to(runs_dir.resolve()) or not rd.is_dir():
raise HTTPException(status_code=404)
detail = bundles.bundle_detail(rd)
if detail is None:
raise HTTPException(status_code=404)
if not detail.get("reports"):
raise HTTPException(
status_code=400, detail="Bundle ohne Judge-Bewertung — erst touchstone judge"
)
md = judge_meta.render_request_md(detail)
return Response(
md,
media_type="text/markdown; charset=utf-8",
headers={"Content-Disposition": 'attachment; filename="judge_meta_request.md"'},
)
@app.get("/export-judge-meta-template/{name}")
def export_judge_meta_template(name: str) -> Any:
"""Stream the empty judge_meta_response.yaml skeleton (one entry per cell × pack
dimension) the cloud AI fills. Stream-only."""
from touchstone.gui import judge_meta
rd = (runs_dir / name).resolve()
if not rd.is_relative_to(runs_dir.resolve()) or not rd.is_dir():
raise HTTPException(status_code=404)
detail = bundles.bundle_detail(rd)
if detail is None:
raise HTTPException(status_code=404)
if not detail.get("reports"):
raise HTTPException(
status_code=400, detail="Bundle ohne Judge-Bewertung — erst touchstone judge"
)
cells = sorted({(r.model, r.variant) for r in detail["reports"]})
body = judge_meta.empty_response_template(detail["pack"], cells)
return Response(
body,
media_type="application/x-yaml",
headers={"Content-Disposition": 'attachment; filename="judge_meta_response.yaml"'},
)
- [ ] Step 4: Run tests to verify they pass
Run: uv run pytest tests/test_gui_judge_meta_surface.py -q
Expected: PASS (5 tests).
- [ ] Step 5: Commit
git add touchstone/gui/app.py tests/test_gui_judge_meta_surface.py
git commit -m "feat(gui): judge-meta export routes (request md + template yaml, stream-only)"
Task 2: POST ingest route + inline-summary partial¶
Files:
- Modify: touchstone/gui/app.py (add _MAX_META_YAML + POST route inside create_app(), after the Task 1 routes)
- Create: touchstone/gui/templates/macros/_judge_meta_summary.html
- Test: tests/test_gui_judge_meta_surface.py (add tests)
Interfaces:
- Consumes: judge_meta.parse_meta_response(text) -> MetaResponse (raises ValueError/pydantic ValidationError on bad input); judge_meta.compute_agreement(pack, reports, master_rows, fresh) -> AgreementResult; judge_meta.aggregate_rubric(reports, fresh) -> RubricSummary; judge_meta.render_judge_quality_md(detail, agreement, rubric, response) -> str. AgreementResult has .cells: list[CellAgreement] (each .model, .variant, .dims: list[DimAgreement] with .outlier: bool, .local_quality_pct, .cloud_quality_pct, .quality_delta, .ko_concordant) and .bundle_mean_abs_delta: float | None. RubricSummary has .cites_evidence, .names_improvement, .justifies_level, .catches_safety, each a tuple[int, int].
- Produces: POST /judge-meta-ingest/{name} (yaml_text: str = Form(...)) → {"ok": false, "errors": [...]} | {"ok": true, "summary_html": str, "download_url": str}.
- [ ] Step 1: Write the failing tests
Append to tests/test_gui_judge_meta_surface.py:
def test_ingest_valid_writes_and_returns_summary(tmp_path):
d = tmp_path / "2026_eval_nd"
pk = _judged_bundle(d)
r = _client(tmp_path).post(
f"/judge-meta-ingest/{d.name}", data={"yaml_text": _valid_response_yaml(pk)}
)
assert r.status_code == 200
body = r.json()
assert body["ok"] is True
assert body["download_url"] == f"/export/{d.name}/judge_quality.md"
assert "Begründungs-Qualität" in body["summary_html"]
assert (d / "judge_quality.md").exists()
assert "## Headline" in (d / "judge_quality.md").read_text(encoding="utf-8")
def test_ingest_malformed_yaml_returns_errors_not_500(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
# a cell missing required fresh_scores/critique → pydantic ValidationError
r = _client(tmp_path).post(
f"/judge-meta-ingest/{d.name}",
data={"yaml_text": "cells:\n - model: m\n variant: baseline\n"},
)
assert r.status_code == 200
body = r.json()
assert body["ok"] is False
assert body["errors"]
def test_ingest_non_mapping_yaml_returns_errors(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
r = _client(tmp_path).post(
f"/judge-meta-ingest/{d.name}", data={"yaml_text": "- just\n- a\n- list\n"}
)
assert r.status_code == 200
assert r.json()["ok"] is False
def test_ingest_unjudged_400(tmp_path):
pk = load_pack(PACK)
d = tmp_path / "2026_eval_raw"
d.mkdir()
(d / "bundle.json").write_text(
json.dumps({"pack_id": pk.id, "pack_path": PACK, "host": {}, "date": "x"}),
encoding="utf-8",
)
(d / "responses.jsonl").write_text("", encoding="utf-8")
r = _client(tmp_path).post(f"/judge-meta-ingest/{d.name}", data={"yaml_text": "cells: []\n"})
assert r.status_code == 400
def test_ingest_oversize_400(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
r = _client(tmp_path).post(
f"/judge-meta-ingest/{d.name}", data={"yaml_text": "x" * 1_000_001}
)
assert r.status_code == 400
def test_ingest_traversal_404(tmp_path):
_judged_bundle(tmp_path / "2026_eval_nd")
r = _client(tmp_path).post(
"/judge-meta-ingest/..%2f..%2fetc", data={"yaml_text": "cells: []\n"}
)
assert r.status_code == 404
- [ ] Step 2: Run tests to verify they fail
Run: uv run pytest tests/test_gui_judge_meta_surface.py -q -k ingest
Expected: FAIL — route not registered (404 for all).
- [ ] Step 3: Create the inline-summary partial
Create touchstone/gui/templates/macros/_judge_meta_summary.html:
{# Inline judge-quality summary rendered from AgreementResult + RubricSummary (no MD→HTML).
Shown in the result page's Judge-Qualität card after a successful ingest. #}
{% set ns = namespace(outliers=0) %}
{% for c in agreement.cells %}{% for d in c.dims if d.outlier %}{% set ns.outliers = ns.outliers + 1 %}{% endfor %}{% endfor %}
<div class="text-sm" style="margin-bottom:0.5rem">
<strong>mean|Δ| zum Referenz-Judge:</strong>
{{ agreement.bundle_mean_abs_delta if agreement.bundle_mean_abs_delta is not none else "—" }}
· <strong>{{ ns.outliers }}</strong> Ausreißer 🚩
</div>
<table class="text-xs" style="width:100%; border-collapse:collapse">
<thead>
<tr style="text-align:left; border-bottom:1px solid var(--border)">
<th>Zelle</th><th>Quality% lokal</th><th>Cloud</th><th>Δ</th><th>K.-o.</th>
</tr>
</thead>
<tbody>
{% for c in agreement.cells %}
<tr>
<td>{{ c.model }} · {{ c.variant }}</td>
<td>{{ c.local_quality_pct if c.local_quality_pct is not none else "—" }}</td>
<td>{{ c.cloud_quality_pct if c.cloud_quality_pct is not none else "—" }}</td>
<td>{{ c.quality_delta if c.quality_delta is not none else "—" }}</td>
<td>{% if c.ko_concordant %}✓{% else %}✗{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="text-xs muted" style="margin-top:0.5rem">
Begründungs-Qualität —
Belege: {{ rubric.cites_evidence[0] }}/{{ rubric.cites_evidence[1] }} ·
Verbesserung benannt (<5): {{ rubric.names_improvement[0] }}/{{ rubric.names_improvement[1] }} ·
Höhe begründet: {{ rubric.justifies_level[0] }}/{{ rubric.justifies_level[1] }} ·
Sicherheit: {{ rubric.catches_safety[0] }}/{{ rubric.catches_safety[1] }}
</div>
- [ ] Step 4: Add the POST ingest route
In touchstone/gui/app.py, immediately after the export_judge_meta_template route from Task 1, insert:
_MAX_META_YAML = 1_000_000 # a response YAML over ~1 MB is abuse, not a use case
@app.post("/judge-meta-ingest/{name}")
def judge_meta_ingest(name: str, yaml_text: str = Form(...)) -> dict[str, Any]:
"""Validate + ingest a pasted judge_meta_response.yaml: compute agreement + rubric, write
judge_quality.md into the bundle, return an inline summary. Never 500s — a parse/schema
error is a normal {ok:false} result (pack-editor pattern)."""
from touchstone.gui import judge_meta
rd = (runs_dir / name).resolve()
if not rd.is_relative_to(runs_dir.resolve()) or not rd.is_dir():
raise HTTPException(status_code=404)
if len(yaml_text) > _MAX_META_YAML:
raise HTTPException(status_code=400, detail="response YAML too large")
detail = bundles.bundle_detail(rd)
if detail is None:
raise HTTPException(status_code=404)
if not detail.get("reports"):
raise HTTPException(
status_code=400, detail="Bundle ohne Judge-Bewertung — erst touchstone judge"
)
try:
meta = judge_meta.parse_meta_response(yaml_text)
except Exception as exc:
return {"ok": False, "errors": [str(exc)]}
agreement = judge_meta.compute_agreement(
detail["pack"], detail["reports"], detail["master_rows"], meta
)
rubric = judge_meta.aggregate_rubric(detail["reports"], meta)
md = judge_meta.render_judge_quality_md(detail, agreement, rubric, meta)
(rd / "judge_quality.md").write_text(md, encoding="utf-8")
summary_html = _templates.get_template("macros/_judge_meta_summary.html").render(
agreement=agreement, rubric=rubric
)
return {
"ok": True,
"summary_html": summary_html,
"download_url": f"/export/{name}/judge_quality.md",
}
- [ ] Step 5: Run tests to verify they pass
Run: uv run pytest tests/test_gui_judge_meta_surface.py -q
Expected: PASS (11 tests).
- [ ] Step 6: Commit
git add touchstone/gui/app.py touchstone/gui/templates/macros/_judge_meta_summary.html tests/test_gui_judge_meta_surface.py
git commit -m "feat(gui): judge-meta ingest route + inline-summary partial (never-500 paste)"
Task 3: Result-page card + Alpine component¶
Files:
- Modify: touchstone/gui/templates/result.html (load JS in body block; add card before Export card at :477)
- Create: touchstone/gui/static/judge_meta.js
- Test: tests/test_gui_judge_meta_surface.py (add template tests)
Interfaces:
- Consumes: the three routes from Tasks 1–2; detail.reports (gate); run_dir.name; (run_dir / "judge_quality.md").exists().
- Produces: rendered card HTML on /result/{name} for judged bundles only.
- [ ] Step 1: Write the failing template tests
Append to tests/test_gui_judge_meta_surface.py:
def test_result_page_shows_judge_meta_card_when_judged(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
r = _client(tmp_path).get(f"/result/{d.name}")
assert r.status_code == 200
assert "Judge-Qualität (Meta-Eval)" in r.text
assert f"/export-judge-meta-request/{d.name}" in r.text
assert f"/export-judge-meta-template/{d.name}" in r.text
def test_result_page_hides_judge_meta_card_when_unjudged(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
(d / "reports.jsonl").unlink() # strip the judging → eval-only bundle
r = _client(tmp_path).get(f"/result/{d.name}")
assert r.status_code == 200
assert "Judge-Qualität (Meta-Eval)" not in r.text
def test_result_page_shows_download_when_quality_exists(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
(d / "judge_quality.md").write_text("# Judge-Qualität\n", encoding="utf-8")
r = _client(tmp_path).get(f"/result/{d.name}")
assert f"/export/{d.name}/judge_quality.md" in r.text
- [ ] Step 2: Run tests to verify they fail
Run: uv run pytest tests/test_gui_judge_meta_surface.py -q -k result_page
Expected: FAIL — card markup not present yet.
- [ ] Step 3: Create the Alpine component
Create touchstone/gui/static/judge_meta.js:
// touchstone/gui/static/judge_meta.js
// Alpine component for the per-bundle judge-quality meta-eval card. A YAML textarea → POST
// /judge-meta-ingest/{name} → server validates (parse_meta_response), writes judge_quality.md,
// returns an inline summary. Never alerts; a parse/schema error is a normal {ok:false} result.
// Build-free; registered on alpine:init, loaded non-deferred.
"use strict";
document.addEventListener("alpine:init", () => {
Alpine.data("judgeMeta", (name) => ({
name: name,
yamlText: "",
ok: null, // null = not yet submitted; true/false after a POST
errors: [],
summaryHtml: "",
downloadUrl: "",
busy: false,
async ingest() {
this.busy = true;
this.ok = null;
this.errors = [];
try {
const res = await fetch(`/judge-meta-ingest/${encodeURIComponent(this.name)}`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ yaml_text: this.yamlText }),
});
if (!res.ok) {
// 400/404 (oversize / unjudged / traversal) — surface the server detail.
this.ok = false;
const d = await res.json().catch(() => ({}));
this.errors = [d.detail || `Fehler (${res.status})`];
return;
}
const data = await res.json();
this.ok = data.ok;
this.errors = data.errors || [];
this.summaryHtml = data.summary_html || "";
this.downloadUrl = data.download_url || "";
} catch (e) {
this.ok = false;
this.errors = ["Auswertung fehlgeschlagen (Netz)"];
} finally {
this.busy = false;
}
},
}));
});
- [ ] Step 4: Load the JS in result.html
In touchstone/gui/templates/result.html, change the top of the body block (lines 7-8) from:
to:
{% block body %}
<script src="/static/sparkline.js" defer></script>
<!-- non-deferred so judgeMeta is registered before the deferred Alpine fires alpine:init -->
<script src="/static/judge_meta.js"></script>
- [ ] Step 5: Add the card before the Export card
In touchstone/gui/templates/result.html, immediately before the {# ── Export ── #} comment at line 477, insert:
{# ── Judge-Qualität (Meta-Eval) ────────────────────────────────────────────── #}
{% if detail and detail.reports %}
<div class="card" x-data="judgeMeta('{{ run_dir.name }}')">
<div class="card-title">Judge-Qualität (Meta-Eval)</div>
<p class="muted text-sm" style="margin-bottom:0.5rem">
Lässt eine externe Cloud-KI die Qualität des lokalen Judges prüfen (Kalibrierung +
Begründungs-Güte). Teil A (eigene frische Bewertung) <strong>zuerst</strong> ausfüllen, dann Teil B.
</p>
<div class="flex gap-2" style="margin-bottom:0.75rem; flex-wrap:wrap">
<a href="/export-judge-meta-request/{{ run_dir.name }}" class="btn btn-primary"
title="Anfrage-Markdown für die Cloud-KI (Teil A blank + Teil B lokale Begründungen).">Anfrage (.md)</a>
<a href="/export-judge-meta-template/{{ run_dir.name }}" class="btn btn-secondary"
title="Leere Antwort-Vorlage (YAML) zum Ausfüllen durch die Cloud-KI.">Antwort-Vorlage (.yaml)</a>
{% if (run_dir / "judge_quality.md").exists() %}
<a href="/export/{{ run_dir.name }}/judge_quality.md" class="btn btn-secondary"
title="Bereits ausgewertet — Neu-Einlesen überschreibt.">judge_quality.md</a>
{% endif %}
</div>
<label class="text-sm" style="display:block; margin-bottom:0.25rem">
Ausgefüllte Antwort (YAML) einfügen:
</label>
<textarea x-model="yamlText" rows="8" spellcheck="false"
style="width:100%; font-family:monospace; font-size:0.8rem; white-space:pre; overflow:auto"
placeholder="cells: - model: …"></textarea>
<div class="flex gap-2 items-center" style="margin-top:0.5rem">
<button type="button" class="btn btn-primary" @click="ingest()" :disabled="busy"
x-text="busy ? 'Werte aus…' : 'Auswerten'"></button>
<span class="text-xs" style="color:var(--ja)" x-show="ok === true" x-cloak>✓ judge_quality.md geschrieben</span>
</div>
<template x-if="ok === false">
<ul class="text-xs" style="color:var(--nein); margin:0.5rem 0 0 1rem">
<template x-for="e in errors" :key="e"><li x-text="e"></li></template>
</ul>
</template>
<div x-show="ok === true" x-cloak style="margin-top:0.75rem" x-html="summaryHtml"></div>
<div x-show="ok === true" x-cloak style="margin-top:0.5rem">
<a class="btn btn-secondary" :href="downloadUrl">judge_quality.md herunterladen</a>
</div>
</div>
{% endif %}
- [ ] Step 6: Run tests to verify they pass
Run: uv run pytest tests/test_gui_judge_meta_surface.py -q
Expected: PASS (14 tests). (Note: judge_meta.js execution is not unit-tested — no JS harness in this repo, mirroring pack_editor.js; it is covered by the Task 4 headless smoke.)
- [ ] Step 7: Commit
git add touchstone/gui/templates/result.html touchstone/gui/static/judge_meta.js tests/test_gui_judge_meta_surface.py
git commit -m "feat(gui): judge-meta card on result page (textarea paste-ingest + inline summary)"
Task 4: Full gate + headless smoke (exit criteria)¶
Files: none (verification only).
- [ ] Step 1: Full test suite
Run: uv run pytest -q
Expected: PASS — all pre-existing tests + the 14 new ones green.
- [ ] Step 2: Type check
Run: uv run mypy touchstone/
Expected: no errors (strict).
- [ ] Step 3: Lint + format
Run: uv run ruff check . && uv run ruff format --check .
Expected: both clean. (If ruff format --check . reports a file, run uv run ruff format . and amend the relevant commit.)
- [ ] Step 4: Headless smoke (GUI restarted)
Start a fresh GUI server against a judged bundle's runs dir, then verify each route end-to-end with curl (the running server must be the new code — restart it, per the project's "GUI nach Änderung neu starten" lesson):
# in one shell: start the server (note the bound port from output / use a fixed one)
uv run touchstone gui & # or: uv run python -m touchstone gui
# then, against a real judged bundle <name> under runs/:
curl -s "http://127.0.0.1:<port>/export-judge-meta-request/<name>" | head -5 # MD with "Teil A"
curl -s "http://127.0.0.1:<port>/export-judge-meta-template/<name>" | head -5 # YAML skeleton
# fill the template, then:
curl -s -X POST "http://127.0.0.1:<port>/judge-meta-ingest/<name>" \
--data-urlencode "yaml_text@filled_response.yaml" | python -m json.tool # {ok:true, summary_html, download_url}
# confirm the card renders + the textarea/Auswerten flow works in a browser on /result/<name>
Expected: the request streams MD beginning with the two-part instruction; the template streams a valid YAML skeleton; the POST returns {"ok": true, ...} and writes judge_quality.md; the browser card shows the inline summary + download button.
- [ ] Step 5: Adversarial whole-branch review (controller, 3 lenses) → fix confirmed findings → merge
Review the whole branch focusing on: (1) confinement/traversal at all three routes (resolve + is_relative_to before any disk access); (2) the never-500 path (malformed/non-mapping YAML → {ok:false}, not 500); (3) the judged-gate holds everywhere (no render/write on an unjudged bundle, card hidden in template). Verify each finding independently before fixing. Then merge feat/judge-meta-gui → main + push (no PR, solo repo).
Self-Review¶
1. Spec coverage — every spec section maps to a task:
- Heimat / card before Export, gated on detail.reports → Task 3 (Steps 5, and the hide-test).
- Stream-only export (request + template) → Task 1.
- Textarea paste-ingest, never-500, writes judge_quality.md, inline summary → Task 2 + Task 3.
- /export/{name}/judge_quality.md reused for download → Task 2 (download_url) + Task 3 (existing-file button); allowlist unchanged (Global Constraints).
- Fehlerbehandlung (unjudged 400, traversal 404, malformed {ok:false}, oversize 400, CSRF auto) → Tasks 1–2 tests.
- Testing (route + template + gate + smoke + adversarial) → Tasks 1–4.
2. Placeholder scan — no "TBD/TODO/handle edge cases"; every code step shows complete code; <port>/<name> in Step 4 Task 4 are runtime values for a manual smoke, not code placeholders.
3. Type consistency — route names (export_judge_meta_request, export_judge_meta_template, judge_meta_ingest) and URLs are identical across tasks; _MAX_META_YAML = 1_000_000 matches the oversize test's 1_000_001; summary_html/download_url/ok/errors keys match between the POST route, the JS component, and the tests; partial consumes agreement/rubric exactly as the route renders them; AgreementResult/RubricSummary/CellAgreement field names match judge_meta.py (bundle_mean_abs_delta, local_quality_pct, cloud_quality_pct, quality_delta, ko_concordant, cites_evidence/names_improvement/justifies_level/catches_safety tuples).