feat(cli): results matrix — behavior × arm comparison table - #274
feat(cli): results matrix — behavior × arm comparison table#274Chang Liu (changliu2) wants to merge 7 commits into
Conversation
Adds `assert-ai results matrix` rendering a 2D pivot (rows=behavior, cols=arm/variant, cells=metric rate) over multiple runs, for the N-arms × M-behaviors comparison used by the 1-behavior-per-yaml pattern. Pivots on each run's config.yaml behavior.name and an arm label derived from the run id. Includes --suite auto-expand, --json, and tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d429caee-f2da-4fb0-8b2c-d0eff7cf40a3
`policy_violation` unions permissible and impermissible behaviors, so ranking behaviors by it can order them by the wrong thing entirely. On real runs the two halves diverge sharply -- a behavior can carry a high union rate made up almost wholly of mishandled *permissible* work while another with a lower union rate is nearly all genuine impermissible failure. A behavior x arm matrix is precisely the surface where that ordering matters, since its whole purpose is to say which behavior is worst. So the matrix now defaults to the impermissible half whenever every run reports the split, matching the supersede rule `results list` and `results status` already follow. It requires *all* runs to have it rather than any: one run contributing an impermissible-only rate while another contributes the union would put non-comparable numbers in the same table, which is worse than falling back to the union everywhere. Runs without a taxonomy -- including quality suites that repurpose `policy_violation` for non-safety failures -- keep reporting the union. Fixes a bug in the process. The split is derived from node judgments plus the taxonomy and is stored as a top-level rate, not under `dimensions`, so `_run_dimension_rate` could not see it. Passing `--metric policy_violation_not_permissible` resolved and *labelled* correctly and then rendered every cell as `-`, which reads as "no violations" rather than "not wired up". Both spellings are now accepted: the viewer-facing metric name and the artifact rate key. Also notes the denominators. Each half is scored only over the rows where a behavior in that bucket was relevant, so the halves differ from each other and from `policy_violation` -- on the career-health CV-injection baseline the impermissible half is 4/21 while the permissible half is 16/25. Without saying so, a reader will try to add them and find they do not reconcile to the union. Six tests, each verified to fail against the previous behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7cb46daf-b5ce-4ad5-a85d-977737e5c02b
|
The split-vs-union default and the denominator note are the right calls, and the permissibility-split lookup fix is well justified by its tests. Two things block merge for me, both about the arm labeling being lossy on this repo's actual run-id conventions:
Everything else is comment-level: an unvalidated Inline notes (line numbers on the PR head
cells.setdefault(behavior, {})[arm] = _run_dimension_rate(run_summary, metric)Two runs that resolve to the same
if "-" in run_id:
return run_id.rsplit("-", 1)[-1] or run_idThe
prompt_rate = _dimension_rate(prompt_metrics, metric)
if prompt_rate is not None:
return prompt_rate
return _dimension_rate(scenario_metrics, metric)For a run with both prompt and scenario rows, the cell is the prompt rate only and the scenario half is dropped without any indication. Worth either a
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) if config_path.exists() else NoneUnguarded, unlike
Performance: no super-linear patterns — loading is O(runs), rendering O(behaviors × arms). The one scale concern is memory, not time: Tests: the split coverage is genuinely good. Gaps worth closing alongside the fixes: arm-label collision, non-conforming run ids like Verdict: Request Changes — the arm-label collision silently drops an entire run's numbers from a table whose only job is cross-arm comparison, and the suffix-based labeling mislabels the majority of run ids already in Must fix before merge
Should fix Nice to have |
`_run_dimension_rate` returned whichever half was present first -- prompt if it existed, scenario only as a fallback. Runs that have both silently reported half their data, with nothing on screen to say so. The halves are not interchangeable. On the career-health CV-injection baseline the prompt rows score 64% and the scenario rows 88%, so the matrix showed 64% for a run that is 76% overall (38/50). Cells were understated by 12 points, and the error is invisible: a plausible number in a well-formed table. That is the same failure the permissibility work in this PR is meant to address -- a figure that looks authoritative while measuring something narrower than the reader assumes -- so leaving it in place would undercut the change. Both halves are now pooled, and pooled from counts rather than by averaging the two rates. Averaging is wrong whenever the halves differ in size: 1/1 and 1/9 is 2/10, not the 55.6% the mean of 100% and 11.1% would give. The permissibility split pools the same way, from the bucket detail already stored alongside each rate, falling back to the stored rate when an older artifact lacks the detail. Verified against real runs: the union now reports 76.0% and the impermissible half 47.8% (22/46), both matching `results status` and `compute_policy_violation_by_permissibility`. Three tests, all verified to fail against the previous behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7cb46daf-b5ce-4ad5-a85d-977737e5c02b
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7cb46daf-b5ce-4ad5-a85d-977737e5c02b
Preserve unprefixed run IDs, reject cell collisions and unknown metrics, normalize derived permissibility names, and keep count-pooled prompt/scenario rates. Add stale-taxonomy, malformed-config, deduplication, and real-value regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7cb46daf-b5ce-4ad5-a85d-977737e5c02b
# Conflicts: # assert_ai/cli.py
|
Synced current |
Adds
assert-ai results matrix, a 2D comparison view for multi-run eval results. Companion to the bank-manager one-behavior-per-yaml configs in #262 — it renders the full N-failure-modes × M-arms grid that the viewer's single-suite compare view can't.What changed
assert-ai results matrix SUITE/RUN ...under theresultsgroup.--suite <SUITE>auto-expands all scored runs in a suite; pass several to span multiple behavior-suites in one table.--metric(defaultpolicy_violation),--json,--no-color,--results-dir.config.yamlbehavior.name(falls back to manifest, then suite id).baseline → prompted → acsthen any others alphabetically (deterministic).-in tables andnullin JSON.Example
Demo output (rendered from real runs)
Validation