CoolFace
Apppublic

Nomearod/agentbench

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
make_plots.py740 linesDownload Raw Back to scripts
1"""Generate (and freshness-check) the README's statistics plots.2 3A figure is a measurement too: an unpinned plot is an unpinned number. Every4plot here is generated from the SAME pinned source the README cells read --5``docs/_generated/stats_report.md`` -- and carries a ``source-hash`` of the6exact values it drew. ``check`` recomputes that hash from the current report and7fails loudly if a committed SVG drifted, the same discipline8``scripts/check_readme_stats.py`` applies to the table cells.9 10Usage (matplotlib only needed for ``generate``, lazy-imported there so ``check``11and the tests run without the ``[plots]`` extra)::12 13    python scripts/make_plots.py generate   # rebuild SVGs from the report14    python scripts/make_plots.py check       # assert committed SVGs are fresh15"""16 17import argparse18import hashlib19import json20import re21import sys22from pathlib import Path23 24ROOT = Path(__file__).resolve().parents[1]25REPORT = ROOT / "docs" / "_generated" / "stats_report.md"26# The judge-unfolding demonstration lives in the hand-maintained design doc, not27# the auto-generated report, so its one plot pins to this file instead.28JUDGE_DESIGN = ROOT / "docs" / "judge-design.md"29PLOTS_DIR = ROOT / "docs" / "_generated" / "plots"30# The 1280x640 GitHub social-preview card. It lives outside PLOTS_DIR because it31# is uploaded in repo settings rather than embedded in the README, so the32# freshness check (which walks PLOTS_DIR) does not gate it; it still carries the33# same source-hash pin and regenerates with `make plots`.34ASSETS_DIR = ROOT / "assets"35 36# Same KEY = value block the README markers pin to (scripts/check_readme_stats.py).37VALUE_RE = re.compile(r"^- ([a-z0-9_]+) = (.+)$", re.MULTILINE)38# Exactly 16 hex chars: source_hash() returns sha256()[:16], and bounding the39# length stops the match from running into a PNG tEXt chunk's trailing CRC byte40# when that byte happens to be an ASCII hex digit (seen on paired_diff).41HASH_RE = re.compile(r"source-hash:\s*([0-9a-f]{16})")42 43# (value-key fragment, display label, framework) in table-column order.44_CONFIGS = (45    ("custom_openai", "Custom OpenAI", "custom"),46    ("custom_anthropic", "Custom Anthropic", "custom"),47    ("langchain_openai", "LangChain OpenAI", "langchain"),48    ("langchain_anthropic", "LangChain Anthropic", "langchain"),49)50_METRICS = (("p_at_5", "P@5"), ("r_at_5", "R@5"))51 52 53def read_values(report_text: str) -> dict[str, str]:54    """Parse the report's README-values block into {KEY: value}."""55    return dict(VALUE_RE.findall(report_text))56 57 58def _ci(raw: str) -> tuple[float, float]:59    lo, hi = raw.strip().strip("[]").split(",")60    return float(lo), float(hi)61 62 63def forest_rows(values: dict[str, str], corpus: str = "fastapi") -> list[dict]:64    """The eight (config x metric) headline points with their 95 percent CIs."""65    rows = []66    for key, label, framework in _CONFIGS:67        for metric, mlabel in _METRICS:68            lo, hi = _ci(values[f"{corpus}_{key}_{metric}_ci"])69            rows.append(70                {71                    "config": key,72                    "label": label,73                    "framework": framework,74                    "metric": metric,75                    "metric_label": mlabel,76                    "mean": float(values[f"{corpus}_{key}_{metric}_mean"]),77                    "lo": lo,78                    "hi": hi,79                }80            )81    return rows82 83 84def significant_points(values: dict[str, str], corpus: str = "fastapi") -> set:85    """The (config, metric) points belonging to a 95 percent significant pair."""86    raw = values.get(f"{corpus}_significant_pairs_95", "none")87    points = set()88    for clause in raw.split(";"):89        m = re.match(r"\s*(\w+) vs (\w+) (p_at_5|r_at_5)\s*$", clause)90        if m:91            a, b, metric = m.groups()92            points.update({(a, metric), (b, metric)})93    return points94 95 96def forest_source(values: dict[str, str], corpus: str = "fastapi") -> dict:97    """The exact value set the forest plot draws -- hashed for provenance."""98    return {99        "rows": forest_rows(values, corpus),100        "significant": sorted(f"{c}:{m}" for c, m in significant_points(values, corpus)),101        "mde": values.get(f"{corpus}_mde_p_at_5_80"),102    }103 104 105# (custom-key, langchain-key, display label) for the framework difference plot.106_PAIRS = (107    ("custom_openai", "langchain_openai", "Custom OpenAI - LC OpenAI"),108    ("custom_anthropic", "langchain_anthropic", "Custom Anthropic - LC Anthropic"),109    ("custom_openai", "langchain_anthropic", "Custom OpenAI - LC Anthropic"),110    ("custom_anthropic", "langchain_openai", "Custom Anthropic - LC OpenAI"),111)112 113 114def significant_pairs(values: dict[str, str], corpus: str = "fastapi") -> set:115    """The (custom, langchain, metric) tuples flagged significant at 95 percent."""116    raw = values.get(f"{corpus}_significant_pairs_95", "none")117    out = set()118    for clause in raw.split(";"):119        m = re.match(r"\s*(\w+) vs (\w+) (p_at_5|r_at_5)\s*$", clause)120        if m:121            out.add((m.group(1), m.group(2), m.group(3)))122    return out123 124 125def paired_rows(values: dict[str, str], corpus: str = "fastapi") -> list[dict]:126    """One row per framework comparison: the mean paired difference with its127    nested 90/95 percent CIs and the pinned TOST verdict. The 90 percent CI reads128    against the +/-margin band (equivalence), the 95 percent against zero."""129    sig = significant_pairs(values, corpus)130    rows = []131    for custom, lc, label in _PAIRS:132        for metric, mlabel in _METRICS:133            stem = f"{corpus}_{custom}_vs_{lc}_{metric}"134            if f"{stem}_diff" not in values:135                continue136            rows.append(137                {138                    "label": label,139                    "metric": metric,140                    "metric_label": mlabel,141                    "diff": float(values[f"{stem}_diff"]),142                    "ci90": _ci(values[f"{stem}_ci90"]),143                    "ci95": _ci(values[f"{stem}_ci95"]),144                    "tost": values.get(f"{stem}_tost", ""),145                    "significant": (custom, lc, metric) in sig,146                    "same_provider": custom.rsplit("_", 1)[1] == lc.rsplit("_", 1)[1],147                }148            )149    return rows150 151 152def paired_source(values: dict[str, str], corpus: str = "fastapi") -> dict:153    """The exact value set the paired-difference plot draws -- hashed for provenance."""154    return {"rows": paired_rows(values, corpus)}155 156 157# (corpus-key, display label) for the variance-decomposition contrast.158_VAR_CORPORA = (("fastapi", "FastAPI"), ("k8s", "Kubernetes"))159 160 161def variance_rows(values: dict[str, str]) -> list[dict]:162    """Per corpus: the P@5 variance split into between-question (stable difficulty)163    and within-question (epoch noise a single run hides), plus the ICC."""164    rows = []165    for key, label in _VAR_CORPORA:166        if f"{key}_icc_p_at_5" not in values:167            continue168        between = float(values[f"{key}_between_question_var_p_at_5"])169        within = float(values[f"{key}_within_question_var_p_at_5"])170        total = between + within171        rows.append(172            {173                "label": label,174                "between": between,175                "within": within,176                "icc": float(values[f"{key}_icc_p_at_5"]),177                "within_frac": within / total if total else 0.0,178            }179        )180    return rows181 182 183def variance_source(values: dict[str, str]) -> dict:184    """The exact value set the ICC-contrast plot draws -- hashed for provenance."""185    return {"rows": variance_rows(values)}186 187 188def mde_source(values: dict[str, str], corpus: str = "fastapi") -> dict:189    """The P@5 minimum detectable effect (80 percent power) and the four190    framework-pair |gaps| measured against it -- the detectability/power view.191    ``detectable`` is the resolution criterion |gap| >= MDE (NOT 95 percent192    significance); the two coincide on current data but are different questions."""193    mde = float(values[f"{corpus}_mde_p_at_5_80"])194    gaps = [195        {196            "label": r["label"],197            "abs_diff": abs(r["diff"]),198            "detectable": abs(r["diff"]) >= mde,199            "same_provider": r["same_provider"],200        }201        for r in paired_rows(values, corpus)202        if r["metric"] == "p_at_5"203    ]204    return {"mde": mde, "gaps": gaps}205 206 207def unfolding_source() -> dict:208    """Parse the judge-unfolding table from judge-design.md section 1.9. Pinned to209    that hand-maintained doc (NOT the auto report); editing the table fails the210    freshness check. Returns observed/truth pass-rate and the two corrected211    estimators (regularized D'Agostini, naive matrix inversion) with their CIs."""212    text = JUDGE_DESIGN.read_text()213 214    def row(label: str) -> tuple[float, float, float]:215        # point and bounds all sign-tolerant: the matrix-inversion estimator is the216        # unstable one and a negative point/bound is a valid table value, not a parse error.217        m = re.search(rf"{re.escape(label)} \| (-?[\d.]+), 95% CI \[(-?[\d.]+), (-?[\d.]+)\]", text)218        if not m:219            raise ValueError(f"judge-design.md section 1.9: row {label!r} not found/parseable")220        return float(m.group(1)), float(m.group(2)), float(m.group(3))221 222    def scalar(label: str) -> float:223        m = re.search(rf"{re.escape(label)} \| [\d/]+ = (-?[\d.]+)", text)224        if not m:225            raise ValueError(f"judge-design.md section 1.9: row {label!r} not found")226        return float(m.group(1))227 228    dago = row("Corrected, D'Agostini")229    matinv = row("Corrected, matrix inversion")230    # Pin the KNOWN-TRUTH row, not just observed: the figure's whole claim is that231    # the corrected CI contains a separately-known truth, so a drift in that row232    # must invalidate the plot even though the truth line currently equals observed.233    return {234        "observed": scalar("Observed (jury) pass-rate"),235        "known_true": scalar("Known true pass-rate"),236        "dagostini": {"point": dago[0], "lo": dago[1], "hi": dago[2]},237        "matrix_inversion": {"point": matinv[0], "lo": matinv[1], "hi": matinv[2]},238    }239 240 241def source_hash(obj) -> str:242    return hashlib.sha256(json.dumps(obj, sort_keys=True).encode()).hexdigest()[:16]243 244 245def embedded_hash(svg_text: str) -> str | None:246    m = HASH_RE.search(svg_text)247    return m.group(1) if m else None248 249 250# name -> source-set builder. One entry per committed figure. PNG (not SVG):251# GitHub's Markdown sanitizer does not reliably render relative-path SVGs inline,252# so the figure ships as PNG and the source-hash lives in a PNG tEXt chunk.253EXPECTED_PLOTS = {254    "forest_fastapi.png": lambda v: forest_source(v, "fastapi"),255    "paired_diff_fastapi.png": lambda v: paired_source(v, "fastapi"),256    "icc_contrast.png": variance_source,257    "mde_resolution.png": lambda v: mde_source(v, "fastapi"),258    "unfolding_shift.png": lambda v: unfolding_source(),259}260 261 262def check(report_text: str, plots_dir: Path) -> list[str]:263    """One failure string per missing or stale plot (empty == all fresh)."""264    values = read_values(report_text)265    failures = []266    for name, builder in EXPECTED_PLOTS.items():267        svg = plots_dir / name268        if not svg.exists():269            failures.append(f"{name} missing; run `make plots`")270            continue271        want = source_hash(builder(values))272        # latin-1 maps every byte, so this never raises on a binary PNG and keeps273        # the ASCII source-hash substring intact (a PNG tEXt chunk is plain text).274        got = embedded_hash(svg.read_bytes().decode("latin-1", "ignore"))275        if got != want:276            failures.append(277                f"{name} stale: embedded source-hash {got} != report {want}; run `make plots`"278            )279    return failures280 281 282def _save_with_hash(fig, out_path: Path, h: str, dpi: int = 200) -> None:283    """Write the figure and embed ``source-hash:<h>`` -- an XML comment for SVG,284    a PNG tEXt chunk for raster. ``check`` reads it back to detect drift."""285    import matplotlib.pyplot as plt286 287    if out_path.suffix == ".svg":288        fig.savefig(out_path, metadata={"Date": None})289        plt.close(fig)290        svg = out_path.read_text().replace("</svg>", f"<!-- source-hash: {h} -->\n</svg>", 1)291        out_path.write_text(svg)292    else:  # raster (png): the hash rides in a PNG tEXt chunk via savefig metadata293        fig.savefig(out_path, dpi=dpi, metadata={"Description": f"source-hash:{h}"})294        plt.close(fig)295 296 297def _render_forest(298    values: dict[str, str],299    out_path: Path,300    figsize: tuple[float, float] = (7.6, 4.4),301    title: str = "FastAPI retrieval: framework comparison (overlapping CIs)",302    dpi: int = 200,303) -> None:304    import matplotlib305 306    matplotlib.use("Agg")307    matplotlib.rcParams["svg.hashsalt"] = "agent-bench"  # deterministic element ids308    import matplotlib.pyplot as plt309    from matplotlib.lines import Line2D310 311    rows = forest_rows(values)312    sig = significant_points(values)313    colors = {"custom": "#2b6cb0", "langchain": "#dd6b20"}314 315    # P@5 group on top, R@5 below; four configs each in table order. y descends so316    # the first config sits highest in its group, with a one-row gap between groups.317    groups = [[r for r in rows if r["metric"] == metric] for metric, _ in _METRICS]318    fig, ax = plt.subplots(figsize=figsize)319    y = float(len(rows) + 1)320    group_tops = []321    for group in groups:322        group_tops.append(y)323        for r in group:324            highlighted = (r["config"], r["metric"]) in sig325            fw = r["framework"]326            ax.errorbar(327                r["mean"],328                y,329                xerr=[[r["mean"] - r["lo"]], [r["hi"] - r["mean"]]],330                fmt="o",331                color=colors[fw],332                ecolor=colors[fw],333                elinewidth=2,334                capsize=4,335                markersize=10 if highlighted else 7,336                markeredgecolor="#b7791f" if highlighted else "white",337                markeredgewidth=2.2 if highlighted else 0.8,338                zorder=3 if highlighted else 2,339            )340            ax.text(r["lo"] - 0.012, y, r["label"], ha="right", va="center", fontsize=8.5)341            y -= 1342        y -= 1  # gap between metric groups343 344    ax.set_yticks([])345    ax.set_xlim(0.40, 1.02)346    ax.set_xlabel("score (95% CI, cluster bootstrap)")347    for (_, mlabel), top in zip(_METRICS, group_tops):348        ax.text(0.40, top + 0.5, mlabel, fontsize=11, fontweight="bold", va="bottom")349    ax.set_title(title, fontsize=11)350 351    handles = [352        Line2D(353            [0],354            [0],355            marker="o",356            color="w",357            markerfacecolor=colors["custom"],358            markersize=8,359            label="custom",360        ),361        Line2D(362            [0],363            [0],364            marker="o",365            color="w",366            markerfacecolor=colors["langchain"],367            markersize=8,368            label="LangChain",369        ),370        Line2D(371            [0],372            [0],373            marker="o",374            color="w",375            markerfacecolor="#cbd5e0",376            markeredgecolor="#b7791f",377            markeredgewidth=2,378            markersize=8,379            label="only significant pair (95%)",380        ),381    ]382    ax.legend(383        handles=handles,384        loc="upper center",385        bbox_to_anchor=(0.5, -0.12),386        ncol=3,387        fontsize=8,388        frameon=False,389    )390    ax.spines[["top", "right"]].set_visible(False)391    fig.tight_layout()392    _save_with_hash(fig, out_path, source_hash(forest_source(values)), dpi=dpi)393 394 395def _render_paired(values: dict[str, str], out_path: Path) -> None:396    import matplotlib397 398    matplotlib.use("Agg")399    matplotlib.rcParams["svg.hashsalt"] = "agent-bench"400    import matplotlib.pyplot as plt401    from matplotlib.lines import Line2D402    from matplotlib.patches import Patch403 404    rows = paired_rows(values)405    margin = 0.10406    sig_color, base_color, tie_color = "#b7791f", "#2b6cb0", "#718096"407 408    # group by metric (P@5 top, R@5 below); same-provider before cross within each409    def order(r: dict) -> tuple:410        return (0 if r["same_provider"] else 1, r["label"])411 412    groups = [413        (mlabel, sorted((r for r in rows if r["metric"] == metric), key=order))414        for metric, mlabel in _METRICS415    ]416 417    fig, ax = plt.subplots(figsize=(8.4, 5.6))418    ax.axvspan(-margin, margin, color="#e2e8f0", zorder=0)  # equivalence band, for the 90% bar419    ax.axvline(0.0, color="#1a202c", lw=1.3, zorder=1)  # zero rule, for the 95% caps420 421    y = float(sum(len(g) for _, g in groups) + len(groups))422    group_tops = []423    for mlabel, group in groups:424        group_tops.append((mlabel, y))425        for r in group:426            color = sig_color if r["significant"] else base_color427            lo95, hi95 = r["ci95"]428            lo90, hi90 = r["ci90"]429            if lo95 == hi95 == 0.0:  # identical recall: a 0-width bar would read as a render bug430                ax.plot(0.0, y, marker="D", color=tie_color, markersize=8, zorder=6)431                ax.text(0.024, y, "exact tie (Δ=0)", va="center", fontsize=7.5, color=tie_color)432            else:433                ax.plot([lo95, hi95], [y, y], color=color, lw=1.3, zorder=3)  # 95% thin434                for x in (lo95, hi95):  # 95% caps435                    ax.plot([x, x], [y - 0.14, y + 0.14], color=color, lw=1.3, zorder=3)436                ax.plot([lo90, hi90], [y, y], color=color, lw=5.5, solid_capstyle="butt", zorder=4)437                ax.plot(438                    r["diff"],439                    y,440                    marker="o",441                    color=color,442                    markersize=9 if r["significant"] else 6,443                    markeredgecolor="white",444                    markeredgewidth=0.8,445                    zorder=6,446                )447            ax.text(-0.215, y, r["label"], ha="right", va="center", fontsize=8)448            y -= 1449        y -= 1450 451    ax.set_yticks([])452    ax.set_xlim(-0.22, 0.40)453    # Headroom above the top group so the title clears the bold "P@5" label.454    ax.set_ylim(1.2, max(t for _, t in group_tops) + 1.6)455    ax.set_xlabel("paired difference: custom − LangChain  (per-question, cluster bootstrap)")456    for mlabel, top in group_tops:457        ax.text(-0.215, top + 0.5, mlabel, fontsize=11, fontweight="bold", va="bottom")458    ax.set_title(459        "Framework difference (paired): equivalence vs the ±0.10 band, significance vs zero",460        fontsize=10.5,461        pad=12,462    )463 464    handles = [465        Line2D([0], [0], color=base_color, lw=5.5, label="90% CI — equivalence (vs ±0.10 band)"),466        Line2D([0], [0], color=base_color, lw=1.3, label="95% CI — significance (vs zero)"),467        Line2D(468            [0],469            [0],470            marker="o",471            color="w",472            markerfacecolor=sig_color,473            markersize=8,474            label="significant pair (95%)",475        ),476        Patch(facecolor="#e2e8f0", label="±0.10 TOST margin"),477    ]478    ax.legend(479        handles=handles,480        loc="upper center",481        bbox_to_anchor=(0.5, -0.11),482        ncol=2,483        fontsize=8,484        frameon=False,485    )486    ax.spines[["top", "right", "left"]].set_visible(False)487    fig.tight_layout()488    _save_with_hash(fig, out_path, source_hash(paired_source(values)))489 490 491def _render_icc(values: dict[str, str], out_path: Path) -> None:492    import matplotlib493 494    matplotlib.use("Agg")495    matplotlib.rcParams["svg.hashsalt"] = "agent-bench"496    import matplotlib.pyplot as plt497    from matplotlib.patches import Patch498 499    rows = variance_rows(values)500    between_color, within_color = "#cbd5e0", "#dd6b20"501 502    fig, ax = plt.subplots(figsize=(8.2, 2.9))503    ys = list(range(len(rows)))[::-1]  # first corpus on top504    for r, y in zip(rows, ys):505        bf = 1.0 - r["within_frac"]  # between-question fraction == ICC506        ax.barh(y, bf, height=0.5, color=between_color)507        ax.barh(y, r["within_frac"], left=bf, height=0.5, color=within_color)508        ax.text(1.015, y, f"ICC {r['icc']:.2f}", va="center", fontsize=10, fontweight="bold")509        ax.text(510            0.0,511            y - 0.42,512            f"within-question (epoch noise): {r['within_frac'] * 100:.1f}% of P@5 variance",513            va="top",514            ha="left",515            fontsize=8,516            color="#4a5568",517        )518    ax.set_yticks(ys)519    ax.set_yticklabels([r["label"] for r in rows], fontsize=12, fontweight="bold")520    ax.set_xlim(0, 1.0)521    ax.set_xticks([0, 0.25, 0.5, 0.75, 1.0])522    ax.set_xticklabels(["0", "25%", "50%", "75%", "100%"])523    ax.set_ylim(-0.85, max(ys) + 0.75)524    ax.set_xlabel("share of P@5 variance")525    ax.set_title(526        "A single run hides a distribution — and how much depends on the corpus",527        fontsize=11,528        pad=10,529    )530    ax.legend(531        handles=[532            Patch(facecolor=between_color, label="between-question (stable difficulty)"),533            Patch(facecolor=within_color, label="within-question (epoch noise, hidden by one run)"),534        ],535        loc="upper center",536        bbox_to_anchor=(0.5, -0.32),537        ncol=2,538        fontsize=8,539        frameon=False,540    )541    ax.spines[["top", "right", "left"]].set_visible(False)542    fig.tight_layout()543    _save_with_hash(fig, out_path, source_hash(variance_source(values)))544 545 546def _render_mde(values: dict[str, str], out_path: Path) -> None:547    import matplotlib548 549    matplotlib.use("Agg")550    matplotlib.rcParams["svg.hashsalt"] = "agent-bench"551    import matplotlib.pyplot as plt552 553    src = mde_source(values)554    mde = src["mde"]555    sig_color, base_color = "#b7791f", "#2b6cb0"556 557    fig, ax = plt.subplots(figsize=(8.2, 2.4))558    ax.axvspan(0, mde, color="#e2e8f0", zorder=0)  # below-resolution zone559    ax.axvline(mde, color="#718096", lw=1.3, ls="--", zorder=1)560    ax.text(561        mde + 0.004,562        0.82,563        f"resolution floor\nMDE {mde:.3f} (80% power)",564        fontsize=8.5,565        color="#4a5568",566        va="top",567    )568    detectable = [g for g in src["gaps"] if g["detectable"]]569    below = len(src["gaps"]) - len(detectable)570    for g in src["gaps"]:571        color = sig_color if g["detectable"] else base_color  # past the floor, not 95% significance572        ax.plot(573            g["abs_diff"],574            0,575            marker="o",576            markersize=11,577            color=color,578            markeredgecolor="white",579            markeredgewidth=0.8,580            zorder=3,581        )582    if len(detectable) == 1:583        g = detectable[0]584        prov = "cross-provider" if not g["same_provider"] else "same-provider"585        ax.annotate(586            f"{g['label']}  +{g['abs_diff']:.3f}\n({prov}: the one gap above the floor)",587            (g["abs_diff"], 0),588            xytext=(g["abs_diff"], -0.9),589            ha="center",590            fontsize=8,591            color=sig_color,592            arrowprops=dict(arrowstyle="-", color=sig_color, lw=0.8),593        )594    else:  # 0 -> no annotation; >1 -> label each gap that clears the floor595        for g in detectable:596            ax.annotate(597                g["label"],598                (g["abs_diff"], 0),599                xytext=(g["abs_diff"], -0.9),600                ha="center",601                fontsize=7.5,602                color=sig_color,603                arrowprops=dict(arrowstyle="-", color=sig_color, lw=0.8),604            )605    ax.text(606        mde / 2,607        -0.9,608        f"{below} of {len(src['gaps'])} P@5 gaps fall below\nthe floor (within the noise)",609        ha="center",610        va="center",611        fontsize=8,612        color=base_color,613    )614    ax.set_ylim(-1.5, 1.4)615    ax.set_yticks([])616    ax.set_xlim(0, 0.20)617    ax.set_xlabel("|P@5 difference|, custom vs LangChain")618    ax.set_title("What the benchmark can resolve at this sample size", fontsize=11)619    ax.spines[["top", "right", "left"]].set_visible(False)620    fig.tight_layout()621    _save_with_hash(fig, out_path, source_hash(mde_source(values)))622 623 624def _render_unfolding(out_path: Path) -> None:625    import matplotlib626 627    matplotlib.use("Agg")628    matplotlib.rcParams["svg.hashsalt"] = "agent-bench"629    import matplotlib.pyplot as plt630 631    src = unfolding_source()632    obs, true = src["observed"], src["known_true"]633    dago, matinv = src["dagostini"], src["matrix_inversion"]634    reg_color, naive_color = "#2b6cb0", "#a0aec0"635 636    fig, ax = plt.subplots(figsize=(8.4, 3.0))637    # The reference line is the KNOWN TRUTH (what the corrected CI must contain),638    # drawn from the pinned known_true row, not observed. They coincide in 1.9639    # (the canary confusion is the identity), so say so only when they actually do.640    ax.axvline(true, color="#1a202c", lw=1.3, zorder=1)641    same = abs(obs - true) < 1e-9642    label = f"observed = known truth {true:.3f}" if same else f"known truth {true:.3f}"643    ax.text(true + 0.008, 1.62, label, fontsize=8.5, va="bottom")644 645    # D'Agostini (regularized): point + wide CI, entirely inside [0,1]646    ax.plot(647        [dago["lo"], dago["hi"]], [1, 1], color=reg_color, lw=4, solid_capstyle="butt", zorder=3648    )649    for x in (dago["lo"], dago["hi"]):650        ax.plot([x, x], [0.88, 1.12], color=reg_color, lw=2, zorder=3)651    ax.plot(dago["point"], 1, "o", ms=10, color=reg_color, mec="white", mew=0.8, zorder=4)652    ax.text(653        dago["hi"] + 0.012,654        1,655        f"{dago['point']:.3f}  [{dago['lo']:.3f}, {dago['hi']:.3f}]",656        va="center",657        fontsize=8,658        color=reg_color,659    )660 661    # matrix inversion (naive): CI leaves [0,1] -> bar across with off-axis arrows662    ax.plot([0.0, 1.0], [0, 0], color=naive_color, lw=4, solid_capstyle="butt", zorder=2)663    ax.annotate(664        "",665        xy=(-0.025, 0),666        xytext=(0.05, 0),667        arrowprops=dict(arrowstyle="->", color=naive_color, lw=2.5),668    )669    ax.annotate(670        "",671        xy=(1.025, 0),672        xytext=(0.95, 0),673        arrowprops=dict(arrowstyle="->", color=naive_color, lw=2.5),674    )675    ax.plot(matinv["point"], 0, "o", ms=10, color=naive_color, mec="white", mew=0.8, zorder=4)676    ax.text(677        0.5,678        -0.36,679        f"95% CI [{matinv['lo']:.3f}, {matinv['hi']:.3f}] — leaves [0,1]: unidentified at n≈20",680        ha="center",681        va="top",682        fontsize=8,683        color="#718096",684    )685 686    ax.set_yticks([0, 1])687    ax.set_yticklabels(["matrix inversion\n(naive)", "D'Agostini\n(regularized)"], fontsize=9)688    ax.set_ylim(-0.75, 1.9)689    ax.set_xlim(-0.04, 1.04)690    ax.set_xticks([0, 0.25, 0.5, 0.75, 1.0])691    ax.set_xlabel("completeness pass-rate (corrected through the judge confusion matrix)")692    ax.set_title(693        "Judge unfolding: the correction moves the rate and widens the honest uncertainty",694        fontsize=10.5,695        pad=8,696    )697    ax.spines[["top", "right", "left"]].set_visible(False)698    fig.tight_layout()699    _save_with_hash(fig, out_path, source_hash(unfolding_source()))700 701 702def generate() -> None:703    PLOTS_DIR.mkdir(parents=True, exist_ok=True)704    values = read_values(REPORT.read_text())705    _render_forest(values, PLOTS_DIR / "forest_fastapi.png")706    _render_paired(values, PLOTS_DIR / "paired_diff_fastapi.png")707    _render_icc(values, PLOTS_DIR / "icc_contrast.png")708    _render_mde(values, PLOTS_DIR / "mde_resolution.png")709    _render_unfolding(PLOTS_DIR / "unfolding_shift.png")710    ASSETS_DIR.mkdir(parents=True, exist_ok=True)711    # 8.0x4.0 in at 160 dpi = exactly 1280x640 px, close to the README forest's712    # committed 7.6x4.4 proportions so the layout survives the aspect change.713    _render_forest(714        values,715        ASSETS_DIR / "social_preview.png",716        figsize=(8.0, 4.0),717        title="agent-bench · FastAPI retrieval: framework comparison (overlapping CIs)",718        dpi=160,719    )720    print(f"wrote {len(EXPECTED_PLOTS)} plot(s) to {PLOTS_DIR} + social card to {ASSETS_DIR}")721 722 723def main() -> int:724    parser = argparse.ArgumentParser(description=__doc__)725    parser.add_argument("mode", choices=("generate", "check"), nargs="?", default="generate")726    args = parser.parse_args()727    if args.mode == "generate":728        generate()729        return 0730    failures = check(REPORT.read_text(), PLOTS_DIR)731    for line in failures:732        print(f"FAIL: {line}")733    if not failures:734        print(f"OK: all {len(EXPECTED_PLOTS)} plot(s) fresh against the report")735    return 1 if failures else 0736 737 738if __name__ == "__main__":739    sys.exit(main())740