feat(cli): results matrix — behavior × arm comparison table - #274
feat(cli): results matrix — behavior × arm comparison table#274Chang Liu (changliu2) wants to merge 9 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 |
Jake Present (jakepresent)
left a comment
There was a problem hiding this comment.
Two correctness blockers remain on exact head 79c3525:
-
A one-sided permissibility taxonomy makes the default matrix render an all-null result. The default-selection logic marks a run as split-capable when either side of the permissibility split has data, then always selects
policy_violation_not_permissible. For an all-permissible taxonomy, every run legitimately has data only in the permissible bucket. I reproduced the resulting successful JSON response:{ "metric": "policy_violation_not_permissible", "cells": {"allowed_behavior": {"baseline": null, "prompted": null}} }The same text output is a complete row of
-, which looks like missing data despite real scored rows. This conflicts with the command's stated fallback topolicy_violationwhen the preferred metric is unavailable. Select the impermissible default only when that exact metric has a usable denominator in every run; otherwise fall back to the union. Please add an all-permissible regression. -
A regenerated taxonomy can silently reverse historical split results.
_load_run_summary()recomputes every run's split using the suite's currenttaxonomy.json.compute_policy_violation_by_permissibility()trustsnode_indexbeforenode_name, even when the current category at that index has a different name. I created historical rows where index 0 wasmust neverand index 1 wasallowed, then reordered those same categories in the current suite taxonomy. A real impermissible baseline violation was reported as0.0rather than1.0, and the command exited successfully. The existing stale-taxonomy test misses this because it changes indices to values outside the current range; ordinary reordering reuses valid indices and produces wrong-but-plausible numbers. Require the index and name to agree, fall back to a stable name match when possible, and fall back to the union when the historical taxonomy cannot be established. Add a reordered-taxonomy regression.
Smaller product-surface gap: this adds a public CLI command with nuanced defaults and collision behavior, but current docs/cli/commands.md, docs/guides/results.md, and CHANGELOG.md do not mention it. Please document the command and its metric/denominator semantics before release.
The prior arm-label and overwrite blockers are fixed: unprefixed run IDs are preserved, duplicate behavior/arm cells fail with both source runs named, unknown metrics fail, prompt and scenario counts are pooled correctly, and malformed configs fall back cleanly. Verification on a synthetic merge with current main: focused results tests 36 passed; full suite 1459 passed, 26 skipped, 840 subtests; viewer check/build passed; wheel and sdist passed twine check; pip check passed; and pip-audit found no known Python vulnerabilities.
Default to the impermissible policy-violation split only when every compared run has a usable denominator for that exact metric. Load each run's versioned taxonomy from its manifest, keep artifact paths inside the suite, and treat valid node names as authoritative over stale indices. Add one-sided and reordered-taxonomy regressions and document the behavior-by-arm matrix command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7cb46daf-b5ce-4ad5-a85d-977737e5c02b
|
Jake Present (@jakepresent) Addressed both exact-head blockers and synced the branch with current
Validation: Ready for re-review. |
Preserve the results-matrix changelog entry alongside the 0.3 dependency-boundary release notes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7cb46daf-b5ce-4ad5-a85d-977737e5c02b
|
Jake Present (@jakepresent) Synced the reviewed results-matrix fixes onto current The only merge conflict was the Unreleased changelog; both the results-matrix entry and the 0.3 dependency-boundary notes are preserved. Focused validation still passes: Ready for exact-head re-review. |
Summary
Adds
assert-ai results matrix, a behavior-by-arm comparison for evaluation runs across one or more suites.baseline,prompted, andacs.--suiteexpands all scored runs in a suite and may be repeated.SUITE/RUNarguments can select individual runs.--metricselects any Boolean judge dimension.--jsonemits the same matrix as machine-readable output.Default metric
The matrix defaults to
policy_violation_not_permissibleonly when every selected run has a non-empty denominator for that exact metric. Otherwise it falls back to the unionpolicy_violationrate.This prevents an all-empty matrix for one-sided taxonomies that contain only permissible behavior.
Permissibility-split rates use only rows where a behavior in that bucket was relevant. The permissible and not-permissible rates can therefore have different denominators and do not sum to the union rate.
Historical taxonomy integrity
Each run is interpreted using the versioned taxonomy recorded in
manifest.artifact_versions.systematize.path, rather than the suite's mutable currenttaxonomy.json.Manifest paths are constrained to the suite directory. Valid
node_namevalues are authoritative;node_indexis used only for legacy judgments without a usable name. Regenerating or reordering the suite taxonomy therefore cannot silently reverse historical split results.Safety and error handling
Documentation
The command and its denominator semantics are documented in:
docs/cli/commands.mddocs/guides/results.mdCHANGELOG.mdValidation