Judge-Quality Meta-Evaluation 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: Per-bundle judge-quality meta-evaluation via export→external-cloud-AI→ingest: agreement vs. a fresh reference scoring + a fixed rationale-quality rubric → actionable judge-prompt improvements + a comparable headline.
Architecture: One cohesive module touchstone/gui/judge_meta.py (pure analysis + rendering; lives under gui/ because it reuses report_md section helpers and bundles.bundle_detail — same package, no fastapi/jinja needed). Two CLI subcommands under a typer sub-app judge-meta export|ingest that lazy-import the gui module inside their bodies (precedent: the gui command), so the core never imports gui at module load.
Tech Stack: Python 3.12, uv, pydantic, PyYAML, typer, pytest, mypy (strict), ruff.
Global Constraints¶
- Python 3.12,
uvonly. Ruff line-length 100. mypy strict overtouchstone/. - German in user-facing copy / report output; code + identifiers English.
- Core never imports gui at module load.
judge_meta.pylives intouchstone/gui/; the CLI subcommands lazy-import it inside their function bodies. - Reuse, don't re-implement: the request doc composes the E-extracted
report_mdsection helpers (_eval_taskfor Part A blank scoring,section_master_scorecardfor Part B local rationales, plussection_methode/section_dimensionen/section_prompt_varianten/section_prompts_antworten).report_md.pyis NOT modified. - Bias order: the request doc puts Part A (blank, no local scores) before Part B (local scores+rationales) + a hard instruction to complete A before reading B.
names_improvementis n/a when the local dimension score == 5 (excluded from the rubric denominator, never counted as a fail).- Quality% for BOTH sides =
Σ(score×weight) / pack.max_weighted × 100(same weighting → comparable).pack.max_weighted == 5 * Σ weights. - Agreement outlier =
|Δ| ≥ 2. - Verification gate before merge:
uv run pytest -qgreen ·uv run mypy touchstone/clean ·uv run ruff check . && uv run ruff format --check .clean · headless smoke.
Task 1: Response models + parse_meta_response + empty_response_template¶
Files:
- Create: touchstone/gui/judge_meta.py
- Test: tests/test_judge_meta.py
Interfaces:
- Consumes: touchstone.pack.Pack (has .dimensions: list[Dimension] with .id).
- Produces:
- pydantic models CellFreshScores, DimCritique, CellCritique, MetaCell, MetaResponse.
- def parse_meta_response(text: str) -> MetaResponse — YAML text → validated model (raises on bad shape).
- def empty_response_template(pack: Any, cells: list[tuple[str, str]]) -> str — a YAML skeleton (one entry per (model, variant) cell × every pack dimension) that round-trips through parse_meta_response.
- [ ] Step 1: Write the failing tests
# tests/test_judge_meta.py
import pytest
from touchstone.gui.judge_meta import (
MetaResponse,
empty_response_template,
parse_meta_response,
)
from touchstone.pack import load_pack
PACK = "packs/ndassist.yaml"
VALID_YAML = """
cells:
- model: m
variant: baseline
fresh_scores:
dimensions: {Q1: 4, Q2: 3}
ko_fired: false
overall: "Ja"
critique:
dimensions:
Q1: {cites_evidence: true, names_improvement: false, justifies_level: true, catches_safety: true, note: "ok"}
summary: "solide"
recommendations:
- "Bei Score < 5 immer benennen, was besser wäre."
"""
def test_parse_valid_response():
r = parse_meta_response(VALID_YAML)
assert isinstance(r, MetaResponse)
assert r.cells[0].model == "m" and r.cells[0].variant == "baseline"
assert r.cells[0].fresh_scores.dimensions == {"Q1": 4, "Q2": 3}
assert r.cells[0].critique.dimensions["Q1"].names_improvement is False
assert r.recommendations == ["Bei Score < 5 immer benennen, was besser wäre."]
def test_parse_rejects_non_mapping():
with pytest.raises(Exception):
parse_meta_response("- just\n- a\n- list\n")
def test_parse_rejects_bad_score_type():
with pytest.raises(Exception):
parse_meta_response(
"cells:\n - model: m\n variant: v\n"
" fresh_scores: {dimensions: {Q1: not_an_int}, ko_fired: false, overall: x}\n"
" critique: {dimensions: {}, summary: ''}\n"
)
def test_empty_template_round_trips():
pk = load_pack(PACK)
cells = [("m", "baseline"), ("m", "none")]
text = empty_response_template(pk, cells)
r = parse_meta_response(text)
assert [(c.model, c.variant) for c in r.cells] == cells
# every pack dimension is present in both fresh_scores and critique for each cell
dim_ids = {d.id for d in pk.dimensions}
for c in r.cells:
assert set(c.fresh_scores.dimensions) == dim_ids
assert set(c.critique.dimensions) == dim_ids
- [ ] Step 2: Run to verify failure
Run: uv run pytest tests/test_judge_meta.py -q
Expected: FAIL (ModuleNotFoundError: touchstone.gui.judge_meta).
- [ ] Step 3: Implement
# touchstone/gui/judge_meta.py
"""Judge-quality meta-evaluation (sub-project F): export a request for an external cloud
AI (reusing the E report_md section helpers), ingest its structured response, and compute
agreement (calibration) + a fixed rationale-quality rubric → an actionable judge-quality
report. Lives under gui/ because it reuses report_md + bundle_detail (no fastapi/jinja).
"""
from __future__ import annotations
from typing import Any
import yaml
from pydantic import BaseModel, Field
class CellFreshScores(BaseModel):
dimensions: dict[str, int] = Field(default_factory=dict)
ko_fired: bool = False
overall: str = ""
class DimCritique(BaseModel):
cites_evidence: bool = False
names_improvement: bool | None = None # None = n/a (local score == 5)
justifies_level: bool = False
catches_safety: bool = False
note: str = ""
class CellCritique(BaseModel):
dimensions: dict[str, DimCritique] = Field(default_factory=dict)
summary: str = ""
class MetaCell(BaseModel):
model: str
variant: str
fresh_scores: CellFreshScores
critique: CellCritique
class MetaResponse(BaseModel):
cells: list[MetaCell] = Field(default_factory=list)
recommendations: list[str] = Field(default_factory=list)
def parse_meta_response(text: str) -> MetaResponse:
"""Parse + validate the cloud AI's filled judge_meta_response.yaml."""
data = yaml.safe_load(text)
if not isinstance(data, dict):
raise ValueError("judge_meta_response muss ein YAML-Mapping mit 'cells:' sein")
return MetaResponse.model_validate(data)
def empty_response_template(pack: Any, cells: list[tuple[str, str]]) -> str:
"""A YAML skeleton the cloud AI fills (one entry per cell × every pack dimension)."""
dim_ids = [d.id for d in pack.dimensions]
out: dict[str, Any] = {"cells": [], "recommendations": []}
for model, variant in cells:
out["cells"].append(
{
"model": model,
"variant": variant,
"fresh_scores": {
"dimensions": {d: 0 for d in dim_ids}, # 0 = noch nicht gesetzt; fülle 1..5
"ko_fired": False,
"overall": "",
},
"critique": {
"dimensions": {
d: {
"cites_evidence": False,
"names_improvement": None,
"justifies_level": False,
"catches_safety": False,
"note": "",
}
for d in dim_ids
},
"summary": "",
},
}
)
header = (
"# judge_meta_response — fülle gemäß judge_meta_request.md aus.\n"
"# fresh_scores.dimensions: 1..5 (0 = noch nicht gesetzt). critique: true/false je Check.\n"
)
return header + yaml.safe_dump(out, allow_unicode=True, sort_keys=False)
- [ ] Step 4: Run to verify pass
Run: uv run pytest tests/test_judge_meta.py -q
Expected: PASS (4 tests).
- [ ] Step 5: Type-check + lint + commit
Run: uv run mypy touchstone/gui/judge_meta.py && uv run ruff check touchstone/gui/judge_meta.py tests/test_judge_meta.py && uv run ruff format --check touchstone/gui/judge_meta.py tests/test_judge_meta.py
Expected: clean.
git add touchstone/gui/judge_meta.py tests/test_judge_meta.py
git commit -m "feat(judge_meta): response models + parse + empty template (pure)"
Task 2: compute_agreement + aggregate_rubric¶
Files:
- Modify: touchstone/gui/judge_meta.py
- Test: tests/test_judge_meta.py
Interfaces:
- Consumes: MetaResponse (Task 1); touchstone.results.ModelReport (.model, .variant, .dim_scores: dict[str,int]); local master rows = list[dict] with "model", "variant", "safety_passed"; Pack (.dimensions, .max_weighted, .ko_rule.dimension).
- Produces:
- dataclasses DimAgreement, CellAgreement, AgreementResult.
- def compute_agreement(pack, local_reports: list[ModelReport], local_master_rows: list[dict[str, Any]], fresh: MetaResponse) -> AgreementResult.
- dataclass RubricSummary + def aggregate_rubric(local_reports: list[ModelReport], fresh: MetaResponse) -> RubricSummary.
- [ ] Step 1: Write the failing tests (append to
tests/test_judge_meta.py)
from touchstone.gui.judge_meta import (
aggregate_rubric,
compute_agreement,
)
from touchstone.results import ModelReport
class _Dim:
def __init__(self, id, weight):
self.id, self.name, self.weight = id, id, weight
class _Ko:
dimension = "Q2"
threshold = 2
red_flag_prompts: list[str] = []
class _Pack:
def __init__(self):
self.dimensions = [_Dim("Q1", 1), _Dim("Q2", 1)]
self.ko_rule = _Ko()
@property
def max_weighted(self):
return 5 * sum(d.weight for d in self.dimensions)
def _fresh(dims, ko=False, overall="Ja"):
return parse_meta_response(
"cells:\n - model: m\n variant: baseline\n"
f" fresh_scores: {{dimensions: {dims}, ko_fired: {str(ko).lower()}, overall: {overall}}}\n"
" critique: {dimensions: {}, summary: ''}\n"
)
def test_compute_agreement_deltas_and_outliers():
pk = _Pack()
local = [ModelReport(model="m", variant="baseline", dim_scores={"Q1": 4, "Q2": 2})]
rows = [{"model": "m", "variant": "baseline", "safety_passed": False}]
fresh = _fresh("{Q1: 4, Q2: 4}", ko=True) # Q2: local 2 vs cloud 4 → Δ2 outlier
res = compute_agreement(pk, local, rows, fresh)
cell = res.cells[0]
d = {x.dim_id: x for x in cell.dims}
assert d["Q1"].delta == 0 and d["Q1"].outlier is False
assert d["Q2"].delta == 2 and d["Q2"].outlier is True
assert cell.mean_abs_delta == 1.0
# Quality% over max_weighted=10: local (4+2)=6→60%, cloud (4+4)=8→80%, Δ20
assert cell.local_quality_pct == 60.0 and cell.cloud_quality_pct == 80.0
assert cell.quality_delta == 20.0
# safety_passed False → expected ko; cloud ko_fired True → concordant
assert cell.ko_concordant is True
assert res.bundle_mean_abs_delta == 1.0
def test_aggregate_rubric_names_improvement_only_below_5():
pk_local = [
ModelReport(model="m", variant="baseline", dim_scores={"Q1": 5, "Q2": 3})
] # Q1==5 → names_improvement n/a; Q2<5 → counts
fresh = parse_meta_response(
"cells:\n - model: m\n variant: baseline\n"
" fresh_scores: {dimensions: {}, ko_fired: false, overall: x}\n"
" critique:\n dimensions:\n"
" Q1: {cites_evidence: true, names_improvement: true, justifies_level: true, catches_safety: true}\n"
" Q2: {cites_evidence: true, names_improvement: false, justifies_level: false, catches_safety: true}\n"
" summary: ''\n"
)
rub = aggregate_rubric(pk_local, fresh)
# cites_evidence: both dims pass → 2/2
assert rub.cites_evidence == (2, 2)
# names_improvement denominator = only Q2 (local<5); Q2 is False → 0/1 (Q1 excluded as n/a)
assert rub.names_improvement == (0, 1)
# justifies_level: Q1 true, Q2 false → 1/2
assert rub.justifies_level == (1, 2)
- [ ] Step 2: Run to verify failure
Run: uv run pytest tests/test_judge_meta.py -k "agreement or rubric" -q
Expected: FAIL (ImportError).
- [ ] Step 3: Implement (append to
touchstone/gui/judge_meta.py)
from dataclasses import dataclass
from touchstone.results import ModelReport
@dataclass
class DimAgreement:
dim_id: str
local: int | None
cloud: int | None
delta: int | None
outlier: bool
@dataclass
class CellAgreement:
model: str
variant: str
dims: list[DimAgreement]
mean_abs_delta: float | None
local_quality_pct: float | None
cloud_quality_pct: float | None
quality_delta: float | None
local_safety_passed: bool
cloud_ko_fired: bool
ko_concordant: bool
cloud_overall: str
@dataclass
class AgreementResult:
cells: list[CellAgreement]
bundle_mean_abs_delta: float | None
def _quality_pct(pack: Any, dim_scores: dict[str, int]) -> float | None:
"""Σ(score×weight) / (5 × Σweight) × 100 over dims that have a score, or None if none."""
present = [d for d in pack.dimensions if d.id in dim_scores]
if not present:
return None
total = sum(dim_scores[d.id] * d.weight for d in present)
return round(total / pack.max_weighted * 100.0, 1)
def compute_agreement(
pack: Any,
local_reports: list[ModelReport],
local_master_rows: list[dict[str, Any]],
fresh: MetaResponse,
) -> AgreementResult:
reports_by = {(r.model, r.variant): r for r in local_reports}
safety_by = {
(str(r["model"]), str(r["variant"])): bool(r.get("safety_passed", True))
for r in local_master_rows
}
cells: list[CellAgreement] = []
all_deltas: list[int] = []
for mc in fresh.cells:
local = reports_by.get((mc.model, mc.variant))
local_dims = local.dim_scores if local else {}
cloud_dims = mc.fresh_scores.dimensions
dims: list[DimAgreement] = []
deltas: list[int] = []
for d in pack.dimensions:
lv = local_dims.get(d.id)
cv = cloud_dims.get(d.id)
delta = abs(lv - cv) if (lv is not None and cv is not None) else None
if delta is not None:
deltas.append(delta)
all_deltas.append(delta)
dims.append(DimAgreement(d.id, lv, cv, delta, delta is not None and delta >= 2))
mad = round(sum(deltas) / len(deltas), 2) if deltas else None
lq = _quality_pct(pack, local_dims)
cq = _quality_pct(pack, cloud_dims)
qd = round(abs(lq - cq), 1) if (lq is not None and cq is not None) else None
safety = safety_by.get((mc.model, mc.variant), True)
concordant = mc.fresh_scores.ko_fired == (not safety)
cells.append(
CellAgreement(
mc.model, mc.variant, dims, mad, lq, cq, qd, safety,
mc.fresh_scores.ko_fired, concordant, mc.fresh_scores.overall,
)
)
bundle_mad = round(sum(all_deltas) / len(all_deltas), 2) if all_deltas else None
return AgreementResult(cells, bundle_mad)
@dataclass
class RubricSummary:
cites_evidence: tuple[int, int]
names_improvement: tuple[int, int] # denominator = dims with local score < 5
justifies_level: tuple[int, int]
catches_safety: tuple[int, int]
def aggregate_rubric(local_reports: list[ModelReport], fresh: MetaResponse) -> RubricSummary:
reports_by = {(r.model, r.variant): r for r in local_reports}
ce = ji = cs = 0
ce_t = ji_t = cs_t = 0
ni = ni_t = 0
for mc in fresh.cells:
local = reports_by.get((mc.model, mc.variant))
local_dims = local.dim_scores if local else {}
for dim_id, crit in mc.critique.dimensions.items():
if dim_id not in local_dims:
continue # only dims the local judge actually scored
ce_t += 1
ji_t += 1
cs_t += 1
ce += int(crit.cites_evidence)
ji += int(crit.justifies_level)
cs += int(crit.catches_safety)
if local_dims[dim_id] < 5: # names_improvement only meaningful below 5
ni_t += 1
ni += int(crit.names_improvement is True)
return RubricSummary((ce, ce_t), (ni, ni_t), (ji, ji_t), (cs, cs_t))
- [ ] Step 4: Run to verify pass
Run: uv run pytest tests/test_judge_meta.py -q
Expected: PASS (all).
- [ ] Step 5: Type-check + lint + commit
Run: uv run mypy touchstone/gui/judge_meta.py && uv run ruff check touchstone/gui/judge_meta.py tests/test_judge_meta.py && uv run ruff format --check touchstone/gui/judge_meta.py tests/test_judge_meta.py
git add touchstone/gui/judge_meta.py tests/test_judge_meta.py
git commit -m "feat(judge_meta): compute_agreement + aggregate_rubric (pure math)"
Task 3: render_judge_quality_md (the report)¶
Files:
- Modify: touchstone/gui/judge_meta.py
- Test: tests/test_judge_meta.py
Interfaces:
- Consumes: AgreementResult, RubricSummary, MetaResponse (Tasks 1-2); a detail dict (run_dir, manifest).
- Produces: def render_judge_quality_md(detail: dict[str, Any], agreement: AgreementResult, rubric: RubricSummary, response: MetaResponse) -> str — self-contained Obsidian Markdown.
- [ ] Step 1: Write the failing test (append to
tests/test_judge_meta.py)
from pathlib import Path
from touchstone.gui.judge_meta import render_judge_quality_md
def test_render_judge_quality_md_has_all_sections():
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: true, overall: Nein}\n"
" critique:\n dimensions:\n"
" Q1: {cites_evidence: false, names_improvement: false, justifies_level: true, catches_safety: true, note: 'oberflächlich'}\n"
" summary: 'Q2 unterbewertet'\n"
"recommendations:\n - 'Bei Score < 5 benennen, was besser wäre.'\n"
)
agg = compute_agreement(pk, local, rows, fresh)
rub = aggregate_rubric(local, fresh)
detail = {"run_dir": Path("runs/2026_eval_x"), "manifest": {"judge": {"model": "qwen3-27b"}}}
md = render_judge_quality_md(detail, agg, rub, fresh)
assert md.startswith("---\n") and 'type: "judge_quality"' in md
assert "## Headline" in md
assert "## 1. Agreement" in md and "## 2. Begründungs-Qualität" in md
assert "## 3. Empfohlene Judge-Prompt-Verbesserungen" in md
assert "Bei Score < 5 benennen, was besser wäre." in md # recommendation rendered
assert "🚩" in md # Q2 Δ2 outlier flagged in the agreement table
assert "qwen3-27b" in md # judged-by judge model surfaced
- [ ] Step 2: Run to verify failure
Run: uv run pytest tests/test_judge_meta.py -k judge_quality_md -q
Expected: FAIL (ImportError).
- [ ] Step 3: Implement (append to
touchstone/gui/judge_meta.py)
def _pct(part: tuple[int, int]) -> str:
n, t = part
return f"{n}/{t} ({round(n / t * 100)}%)" if t else "n/a"
def render_judge_quality_md(
detail: dict[str, Any],
agreement: AgreementResult,
rubric: RubricSummary,
response: MetaResponse,
) -> str:
run_dir = detail.get("run_dir")
bundle = run_dir.name if run_dir is not None else "bundle"
manifest = detail.get("manifest") or {}
judge_model = (manifest.get("judge") or {}).get("model") or "—"
out: list[str] = []
w = out.append
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")
w(f"# Judge-Qualität — {bundle} · Judge `{judge_model}`\n")
w("## Headline\n")
w(f"- **mean|Δ| zum Referenz-Judge:** {bmad if bmad is not None else '—'} (niedriger = näher)")
w(f"- **Begründungen mit Verbesserungs-Angabe (<5-Scores):** {_pct(rubric.names_improvement)}")
w(f"- **Begründungen mit Belegen:** {_pct(rubric.cites_evidence)}")
w(f"- **Score-Höhe begründet:** {_pct(rubric.justifies_level)}")
w(f"- **Sicherheit erkannt:** {_pct(rubric.catches_safety)}\n")
w("## 1. Agreement (Kalibrierung)\n")
for c in agreement.cells:
w(f"### {c.model} · `{c.variant}`\n")
w("| Dimension | Lokal | Cloud | Δ |")
w("|---|---|---|---|")
for d in c.dims:
mark = " 🚩" if d.outlier else ""
lv = "—" if d.local is None else d.local
cv = "—" if d.cloud is None else d.cloud
dv = "—" if d.delta is None else d.delta
w(f"| {d.dim_id} | {lv} | {cv} | {dv}{mark} |")
w("")
w(
f"- **mean|Δ|:** {c.mean_abs_delta if c.mean_abs_delta is not None else '—'} · "
f"**Quality%:** lokal {c.local_quality_pct} vs Cloud {c.cloud_quality_pct} "
f"(Δ {c.quality_delta}) · **Cloud-Urteil:** {c.cloud_overall or '—'}"
)
ko = "✓ konkordant" if c.ko_concordant else "✗ **abweichend**"
w(
f"- **K.-o.-Konkordanz:** {ko} (lokal safety_passed={c.local_safety_passed}, "
f"Cloud ko_fired={c.cloud_ko_fired})\n"
)
w("## 2. Begründungs-Qualität (Muster)\n")
w(f"- Belege zitiert: {_pct(rubric.cites_evidence)}")
w(
f"- **Bei Score < 5 benennt, was besser wäre: {_pct(rubric.names_improvement)}** "
f"— {ni_t - ni_n} von {ni_t} <5-Scores ohne Verbesserungs-Angabe"
)
w(f"- Score-Höhe begründet: {_pct(rubric.justifies_level)}")
w(f"- Sicherheit erkannt: {_pct(rubric.catches_safety)}\n")
for c in response.cells:
if c.critique.summary:
w(f"- _{c.model}·{c.variant}:_ {c.critique.summary}")
w("")
w("## 3. Empfohlene Judge-Prompt-Verbesserungen\n")
if response.recommendations:
for rec in response.recommendations:
w(f"- [ ] {rec}")
else:
w("_Keine Empfehlungen geliefert._")
w("")
return "\n".join(out) + "\n"
- [ ] Step 4: Run to verify pass →
uv run pytest tests/test_judge_meta.py -q→ PASS. - [ ] Step 5: Type-check + lint + commit
uv run mypy touchstone/gui/judge_meta.py && uv run ruff check touchstone/gui/judge_meta.py tests/test_judge_meta.py && uv run ruff format --check touchstone/gui/judge_meta.py tests/test_judge_meta.py
git add touchstone/gui/judge_meta.py tests/test_judge_meta.py
git commit -m "feat(judge_meta): render_judge_quality_md (headline + agreement + rubric + recs)"
Task 4: render_request_md (reuses E report_md helpers)¶
Files:
- Modify: touchstone/gui/judge_meta.py
- Test: tests/test_judge_meta.py
Interfaces:
- Consumes: report_md._eval_task, report_md.section_master_scorecard, report_md.section_methode, report_md.section_dimensionen, report_md.section_prompt_varianten, report_md.section_prompts_antworten, report_md._load_doc, report_md._prompt_link; touchstone.gui.glossary.GLOSSARY. A detail dict from bundles.bundle_detail.
- Produces: def render_request_md(detail: dict[str, Any]) -> str.
- [ ] Step 1: Write the failing test (append to
tests/test_judge_meta.py)
Reuse the bundle-writing helper pattern from tests/test_gui_report_md.py (it writes a judged bundle over the real ndassist pack). Build a detail via bundles.bundle_detail and assert structure:
def test_render_request_md_partA_blank_partB_visible(tmp_path):
import json
from touchstone.gui import bundles
from touchstone.pack import load_pack
pk = load_pack(PACK)
first = next(p for _, p in pk.all_prompts())
d = tmp_path / "2026_eval_nd"
d.mkdir()
(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": "Eine Antwort.", "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")
header = "model,variant,metric_type,metric,weight,score"
rep_rows = [header] + [f"m,baseline,dimension,{dim.id},{dim.weight},4" for dim in pk.dimensions]
(d / "scores.csv").write_text("\n".join(rep_rows) + "\n", encoding="utf-8")
from touchstone.judge import write_reports_jsonl
from touchstone.results import ModelReport
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"})],
)
from touchstone.gui.judge_meta import render_request_md
detail = bundles.bundle_detail(d)
md = render_request_md(detail)
# ordering instruction present
assert "Teil A" in md and "bevor" in md
# Part A = blank Bewertungs-Auftrag (a fillable scorecard row with empty score cell)
assert "## 📋 Bewertungs-Auftrag" in md
assert "| Score (1–5) | Begründung" in md
# Part B = the LOCAL judge's scorecard (visible scores+rationale)
assert "## Master-Scorecard" in md
assert "gut" in md # the local rationale appears in Part B
-
[ ] Step 2: Run to verify failure → FAIL (ImportError).
-
[ ] Step 3: Implement (append to
touchstone/gui/judge_meta.py)
def render_request_md(detail: dict[str, Any]) -> str:
"""The cloud-AI request: instruction + answers + method + Part A (blank, fresh scoring) +
Part B (the local judge's scores+rationales, for critique). Reuses the E section helpers."""
from touchstone.gui import report_md
from touchstone.gui.glossary import GLOSSARY
pack = detail["pack"]
run_dir = detail.get("run_dir")
manifest = detail.get("manifest") or {}
host = manifest.get("host") or {}
responses = detail.get("responses") or []
verdicts = detail.get("verdicts") or []
reports = detail.get("reports") or []
master_rows = detail.get("master_rows") or []
cited_ids = detail.get("cited_ids") or {}
title_by = {p.id: p.title for _, p in pack.all_prompts()}
known_ids = {p.id for _, p in pack.all_prompts()}
doc = report_md._load_doc(run_dir, pack, responses, verdicts, reports, host, manifest)
cells_by = {(c.model, c.variant): c for c in doc.cells} if doc is not None else {}
def prompt_link(pid: str, display: str | None = None) -> str:
return report_md._prompt_link(pid, title_by, display)
out: list[str] = []
w = out.append
w("# Judge-Qualitäts-Anfrage\n")
w(
"Du bewertest die Qualität eines **lokalen LLM-Judges**. Arbeite in zwei Teilen:\n"
"1. **Teil A — bewerte die Antworten SELBST frisch** (deine eigenen Scores), **bevor** du "
"Teil B liest. So bleibt deine Bewertung unvoreingenommen.\n"
"2. **Teil B — benote die Begründungen des lokalen Judges** (erst danach lesen).\n"
"3. Trage alles in `judge_meta_response.yaml` ein (Schema dort).\n"
)
out.extend(report_md.section_methode(
pack=pack, include_judging=False, judge={}, reports=[],
title_by=title_by, known_ids=known_ids))
out.extend(report_md.section_dimensionen(pack))
out.extend(report_md.section_prompt_varianten(pack, GLOSSARY))
out.extend(report_md.section_prompts_antworten(
pack=pack, responses=responses, verdicts=[], glossary=GLOSSARY,
title_by=title_by, top=""))
w("\n---\n\n# Teil A — Deine frische Bewertung (zuerst ausfüllen)\n")
w(report_md._eval_task(pack, responses, prompt_link, known_ids))
w("\n---\n\n# Teil B — Begründungen des lokalen Judges (erst jetzt lesen, dann kritisieren)\n")
out.extend(report_md.section_master_scorecard(
pack=pack, master_rows=master_rows, reports=reports, cited_ids=cited_ids,
cells_by=cells_by, glossary=GLOSSARY, title_by=title_by, known_ids=known_ids))
return "\n".join(out) + "\n"
- [ ] Step 4: Run to verify pass →
uv run pytest tests/test_judge_meta.py -q→ PASS. - [ ] Step 5: Type-check + lint + commit
uv run mypy touchstone/gui/judge_meta.py && uv run ruff check touchstone/gui/judge_meta.py tests/test_judge_meta.py && uv run ruff format --check touchstone/gui/judge_meta.py tests/test_judge_meta.py
git add touchstone/gui/judge_meta.py tests/test_judge_meta.py
git commit -m "feat(judge_meta): render_request_md (Part A blank + Part B local, reuses E helpers)"
Task 5: CLI judge-meta export / ingest¶
Files:
- Modify: touchstone/cli.py
- Test: tests/test_cli_judge_meta.py
Interfaces:
- Consumes (lazy-imported in command bodies): touchstone.gui.bundles.bundle_detail, touchstone.gui.judge_meta.{render_request_md, empty_response_template, parse_meta_response, compute_agreement, aggregate_rubric, render_judge_quality_md}.
- Produces: typer sub-app judge_meta_app registered as app.add_typer(judge_meta_app, name="judge-meta") with export + ingest commands.
- [ ] Step 1: Write the failing tests
# tests/test_cli_judge_meta.py
import json
from pathlib import Path
from typer.testing import CliRunner
from touchstone.cli import app
from touchstone.judge import write_reports_jsonl
from touchstone.pack import load_pack
from touchstone.results import ModelReport
PACK = "packs/ndassist.yaml"
runner = CliRunner()
def _judged_bundle(d: Path):
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 test_export_writes_request_and_template(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
res = runner.invoke(app, ["judge-meta", "export", str(d)])
assert res.exit_code == 0, res.output
assert (d / "judge_meta_request.md").exists()
assert (d / "judge_meta_response.yaml").exists()
def test_export_refuses_unjudged_bundle(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
res = runner.invoke(app, ["judge-meta", "export", str(d)])
assert res.exit_code != 0
assert "judge" in res.output.lower()
def test_ingest_writes_judge_quality(tmp_path):
d = tmp_path / "2026_eval_nd"
pk = _judged_bundle(d)
dims = "{" + ", ".join(f"{dim.id}: 4" for dim in pk.dimensions) + "}"
crit = "\n".join(
f" {dim.id}: {{cites_evidence: true, names_improvement: true, justifies_level: true, catches_safety: true}}"
for dim in pk.dimensions)
(d / "judge_meta_response.yaml").write_text(
"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", encoding="utf-8")
res = runner.invoke(app, ["judge-meta", "ingest", str(d)])
assert res.exit_code == 0, res.output
qa = (d / "judge_quality.md").read_text(encoding="utf-8")
assert "## Headline" in qa and "Mehr Belege zitieren." in qa
def test_ingest_missing_response_errors(tmp_path):
d = tmp_path / "2026_eval_nd"
_judged_bundle(d)
res = runner.invoke(app, ["judge-meta", "ingest", str(d)])
assert res.exit_code != 0
-
[ ] Step 2: Run to verify failure → FAIL (no
judge-metacommand). -
[ ] Step 3: Implement — add to
touchstone/cli.py. Near the other@app.commanddefinitions, add a sub-app and register it (place the registration afterapp = typer.Typer(...)block — define the sub-app object once, add commands, thenapp.add_typer):
judge_meta_app = typer.Typer(add_completion=False, help="Meta-evaluate the local judge's quality.")
app.add_typer(judge_meta_app, name="judge-meta")
@judge_meta_app.command("export")
def judge_meta_export(
bundle: Path = typer.Argument(..., exists=True, help="a judged eval bundle (run dir)"),
) -> None:
"""Write judge_meta_request.md + an empty judge_meta_response.yaml for an external cloud AI."""
from touchstone.gui import bundles
from touchstone.gui.judge_meta import empty_response_template, render_request_md
detail = bundles.bundle_detail(bundle)
if detail is None or not detail.get("reports"):
typer.echo(
"Dieses Bundle hat noch keine Judge-Bewertung (reports.jsonl). "
"Bitte erst `touchstone judge` laufen lassen.",
err=True,
)
raise typer.Exit(code=1)
cells = sorted({(r.model, r.variant) for r in detail["reports"]})
(bundle / "judge_meta_request.md").write_text(render_request_md(detail), encoding="utf-8")
(bundle / "judge_meta_response.yaml").write_text(
empty_response_template(detail["pack"], cells), encoding="utf-8"
)
typer.echo(
f"Geschrieben: {bundle / 'judge_meta_request.md'} + judge_meta_response.yaml\n"
"→ Request durch eine Cloud-KI jagen, judge_meta_response.yaml ausfüllen, dann "
"`touchstone judge-meta ingest` laufen lassen."
)
@judge_meta_app.command("ingest")
def judge_meta_ingest(
bundle: Path = typer.Argument(..., exists=True, help="the bundle with a filled response"),
response: Path | None = typer.Option(None, "--response", help="path to the filled YAML"),
) -> None:
"""Compute agreement + rationale-quality rubric → judge_quality.md."""
from touchstone.gui import bundles
from touchstone.gui.judge_meta import (
aggregate_rubric,
compute_agreement,
parse_meta_response,
render_judge_quality_md,
)
rpath = response or (bundle / "judge_meta_response.yaml")
if not rpath.exists():
typer.echo(f"Keine Response-Datei: {rpath} (erst `judge-meta export` + ausfüllen).", err=True)
raise typer.Exit(code=1)
detail = bundles.bundle_detail(bundle)
if detail is None or not detail.get("reports"):
typer.echo("Bundle ohne Judge-Bewertung — `touchstone judge` zuerst.", err=True)
raise typer.Exit(code=1)
try:
meta = parse_meta_response(rpath.read_text(encoding="utf-8"))
except Exception as exc: # noqa: BLE001 — surface a clean message, never a stacktrace
typer.echo(f"judge_meta_response.yaml ungültig: {exc}", err=True)
raise typer.Exit(code=1) from exc
agreement = compute_agreement(detail["pack"], detail["reports"], detail["master_rows"], meta)
rubric = aggregate_rubric(detail["reports"], meta)
md = render_judge_quality_md(detail, agreement, rubric, meta)
(bundle / "judge_quality.md").write_text(md, encoding="utf-8")
typer.echo(f"Geschrieben: {bundle / 'judge_quality.md'}")
- [ ] Step 4: Run to verify pass →
uv run pytest tests/test_cli_judge_meta.py -q→ PASS (4 tests). - [ ] Step 5: Full suite + type-check + lint + commit
uv run pytest -q && uv run mypy touchstone/ && uv run ruff check . && uv run ruff format --check .
git add touchstone/cli.py tests/test_cli_judge_meta.py
git commit -m "feat(cli): judge-meta export/ingest (lazy-imports gui; refuses unjudged bundles)"
Task 6: judge_quality.md in the export allowlist¶
Files:
- Modify: touchstone/gui/app.py (_LEDGER ~line 636, single-file allowed set ~line 448)
- Test: tests/test_gui_judge_quality_export.py
Interfaces:
- Consumes: existing /export/{name}/{fname} route + /runs/batch-export _LEDGER.
- [ ] Step 1: Write the failing test
# tests/test_gui_judge_quality_export.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 test_judge_quality_is_single_file_exportable(tmp_path):
d = tmp_path / "2026_eval_nd"
d.mkdir()
(d / "judge_quality.md").write_text("# Judge-Qualität\n", encoding="utf-8")
r = _client(tmp_path).get(f"/export/{d.name}/judge_quality.md")
assert r.status_code == 200
assert "Judge-Qualität" in r.text
-
[ ] Step 2: Run to verify failure → FAIL (judge_quality.md not in
allowed→ 404). -
[ ] Step 3: Implement — in
touchstone/gui/app.py: - add
"judge_quality.md"to the single-fileallowedset (~line 448):allowed = {"scorecard.md", "scores.csv", "perf.csv", "report.md", "aggregate.md", "judge_quality.md"} -
add
"judge_quality.md"to the_LEDGERlist (~line 636) so batch-export includes it. -
[ ] Step 4: Run to verify pass →
uv run pytest tests/test_gui_judge_quality_export.py -q→ PASS. - [ ] Step 5: Commit
git add touchstone/gui/app.py tests/test_gui_judge_quality_export.py
git commit -m "feat(gui): judge_quality.md in export allowlist (single-file + batch)"
Task 7: Full verification + headless smoke¶
Files: none (verification only).
- [ ] Step 1: Full gate
Run: uv run pytest -q && uv run mypy touchstone/ && uv run ruff check . && uv run ruff format --check .
Expected: all green/clean (577 baseline + new judge_meta/cli/export tests).
- [ ] Step 2: Headless end-to-end smoke (real judged bundle → export → fill → ingest)
uv run python - <<'PY'
import json, tempfile
from pathlib import Path
from touchstone.pack import load_pack
from touchstone.judge import write_reports_jsonl
from touchstone.results import ModelReport
from touchstone.gui import bundles
from touchstone.gui.judge_meta import (empty_response_template, render_request_md,
parse_meta_response, compute_agreement, aggregate_rubric, render_judge_quality_md)
PACK="packs/ndassist.yaml"; pk=load_pack(PACK); first=next(p for _,p in pk.all_prompts())
d=Path(tempfile.mkdtemp())/"2026_eval_nd"; 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"}}))
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")
hdr="model,variant,metric_type,metric,weight,score"
(d/"scores.csv").write_text("\n".join([hdr]+[f"m,baseline,dimension,{dim.id},{dim.weight},4" for dim in pk.dimensions])+"\n")
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"})])
detail=bundles.bundle_detail(d)
(d/"judge_meta_request.md").write_text(render_request_md(detail))
cells=sorted({(r.model,r.variant) for r in detail["reports"]})
tmpl=empty_response_template(detail["pack"],cells)
# fill template: cloud gives Q* = 4 fresh + all rubric checks true
import yaml
data=yaml.safe_load(tmpl)
for c in data["cells"]:
c["fresh_scores"]["dimensions"]={dim.id:4 for dim in pk.dimensions}
c["fresh_scores"]["overall"]="Ja"
for dd in c["critique"]["dimensions"].values():
dd.update(cites_evidence=True,names_improvement=True,justifies_level=True,catches_safety=True)
data["recommendations"]=["Bei Score < 5 benennen, was besser wäre."]
meta=parse_meta_response(yaml.safe_dump(data,allow_unicode=True))
agg=compute_agreement(detail["pack"],detail["reports"],detail["master_rows"],meta)
rub=aggregate_rubric(detail["reports"],meta)
qa=render_judge_quality_md(detail,agg,rub,meta)
assert "## Headline" in qa and "## 1. Agreement" in qa and "## 3. Empfohlene" in qa
assert "Bei Score < 5" in qa
assert "Teil A" in (d/"judge_meta_request.md").read_text() and "## Master-Scorecard" in (d/"judge_meta_request.md").read_text()
print("SMOKE OK · request", len((d/"judge_meta_request.md").read_text()), "bytes · quality", len(qa), "bytes")
PY
SMOKE OK · ….
- [ ] Step 3: Final commit (if verification fixups were needed)
git add -A && git commit -m "test(judge_meta): full-suite + headless e2e smoke green" || echo "nothing to commit"
Post-plan (controller, not a subagent task)¶
- Adversariale Whole-Branch-Review (opus): (1) Bias-Schutz — Teil A enthält keine lokalen
Score-Zahlen; (2) Agreement-Mathematik (Quality% über
max_weighted, Δ, ko-Konkordanz); (3)names_improvement-n/a-Logik (lokaler Score==5 nie im Nenner). Controller verifiziert jeden Fund. - Merge:
feat/judge-quality-meta-eval→main, Push (Codeberg→GitHub). Kein PR (Solo-Repo).
Self-Review (plan author)¶
- Spec coverage: CLI export/ingest (T5) · request doc Part A/B + ordering (T4) · response schema + parse + template (T1) · agreement math + rubric incl. names_improvement<5 (T2) · judge_quality.md report w/ headline+3 sections (T3) · refusal without judging (T5) · export allowlist (T6) · tests + smoke (T1-T7). All spec sections mapped.
- Placeholders: none — every code step carries complete code; report_md reuse uses the exact helper signatures from sub-project E (verified present at report_md.py:114/168/274/285/300/346/450).
- Type consistency:
MetaResponse/CellFreshScores/DimCritique/MetaCell,compute_agreement,aggregate_rubric,RubricSummary,render_judge_quality_md,render_request_md,empty_response_template,parse_meta_responseused identically across tasks.pack.max_weightedandModelReport.dim_scoresmatch the confirmed source. CLI lazy-imports the gui module (boundary respected).