feat(report): emit a machine-readable roster summary (rates, gap, ranks, Kendall tau) - #46
Merged
Merged
Conversation
leakgauge report rendered a static leaderboard and printed a reorder table, but nothing tied the roster together in a form that could be committed, cited or diffed between runs. Add suite.roster_summary(), which reads the loaded summaries and returns, per model: hijack-ASR, leakage-verified ASR and utility-under-attack (each with its bootstrap CI as loaded), the hijack-leakage gap, and the rank under each ordering; plus roster-level Kendall tau and the count of models whose rank shifts. format_roster_markdown() renders the same object as a committable table. Both are wired to leakgauge report via --summary-json / --summary-md. Nothing is synthesised. Rates are passed through as loaded rather than recomputed, and a metric a run did not produce stays null (markdown: an em dash) instead of collapsing to 0.0, so 'not measured' and 'measured as zero' stay distinguishable. Ranks and tau are null for a single-model roster. The gap is the one computed value, rounded to 6 places so float subtraction noise does not show up as a diff between identical runs. The crossing count reuses the existing logic rather than reimplementing it: _crossings moves from leaderboard.py to scoring.py as the public crossings(), beside the RankReorder it operates on, and leaderboard imports it from there. Closes bamdadd#37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #37.
What it produces
Run against the three committed roster results:
roster.md:Kendall τ (hijack-rate vs leakage-rate): 1.000 — 0 of 3 model(s) change rank when scored on verified leakage.
roster.jsoncarries the same content unrounded, keyed for machine consumption:{ "schema_version": 1, "n_models": 3, "kendall_tau": 1.0, "rank_crossings": 0, "models": [ { "model": "openai:gpt-4o", "n_cases": 37, "hijack_asr": { "point": 0.081…, "lo": 0.027…, "hi": 0.146… }, "leakage_asr": { "point": 0.076…, "lo": 0.027…, "hi": 0.135… }, "utility_under_attack": { "point": 0.657…, "lo": 0.520…, "hi": 0.788… }, "gap": 0.004505, "hijack_rank": 1, "leakage_rank": 1 } ] }(That table is real output from the committed
results/, not an illustration.)Implementation
suite.roster_summary(summaries, reorder)builds the object;suite.format_roster_markdown(roster)renders the same object as the table, so the two artifacts cannot drift.leakgauge reportgains--summary-json PATHand--summary-md PATH. Both are optional and create their parent directory; with neither flag the command behaves exactly as before.No fabricated rows
{point, lo, hi}intact — nothing is recomputed, so the summary cannot disagree with the source files.utility_under_attackisNoneunless the run supplied a utility check, and staysNonethrough the JSON round trip / renders as—in markdown. It never becomes0.0: "not measured" and "measured as zero" are different claims, and on a robustness metric conflating them flatters the model.kendall_tau,rank_crossingsand both rank columns arenullfor a single-model roster, since a reorder is undefined below two models.gapis the only value computed here, and it is rounded to 6 places. Unrounded, float subtraction gives0.0045045045045045on one run and0.004504504504504505on another for identical inputs, which would show up as a spurious diff — and diffability is the point of the artifact.Reusing
_crossingsThe issue asks to reuse the existing
_crossingslogic. Rather than import a private name across modules, it moves fromleaderboard.pytoscoring.pyas the publiccrossings(), next to theRankReorderit operates on;leaderboard.pyimports it from there and its single call site is updated. No behaviour change, andsuite.pygets it without depending on the renderer.Tests
Six in
tests/test_suite.py._fake_summarygrew an optionalutilityargument (defaultNone, so existing callers are untouched and still exercise the absent-metric path).test_roster_summary_matches_the_computed_report— three models where A hijacks most but leaks least, so the orderings disagree and the gap is positive for one model and negative for another. Asserts every rate dict is identical to the inputaggregate,n_casesmatches,gapequals the difference, both ranks matchreorder,kendall_taumatches,rank_crossings == crossings(reorder) == 2, and row order equalsreorder.models_by_hijack.test_roster_markdown_matches_the_roster_summary— exactly one row per model (no invented rows), CIs present in the row rather than just the point estimate, signed gap, τ and the crossing sentence.test_roster_summary_leaves_an_unmeasured_metric_blank_not_zero— absent utility survives ajson.dumps/loadsround trip asnull, the markdown shows—, and0.000appears nowhere in the table.test_roster_summary_single_model_has_no_ranks— τ, crossings and ranks allnull; markdown says it needs ≥2 summaries.test_report_writes_the_roster_summary_artifacts— end to end throughmain(["report", …]), including creating a missing parent directory; the written JSON is compared againstroster_summary(...)recomputed from the same files, and the markdown againstformat_roster_markdown(...).test_report_without_summary_flags_writes_nothing— the no-flags path is unchanged and leaves no files behind.Docs
README gains a short
leakgauge reportflag block under the CLI section explaining what the two artifacts contain and thenull/—rule.Checks
ruff check .,mypy src,pytest -qall pass (414 passed, 4 xfailed).ruff format --check src testsis clean.ruff format --check .flagsCONTRIBUTING.mdanddocs/CONTRACTS.mdon a clean tree here only because my local ruff 0.16.1 formats fenced code blocks in Markdown and theuv.lockversion does not; neither file is touched by this PR.