feat(site): surface v1 composite + correctness + safety + catastrophic (frontend Phase 1) - #206
feat(site): surface v1 composite + correctness + safety + catastrophic (frontend Phase 1)#206jessie1111101 wants to merge 7 commits into
Conversation
3a21b8f to
36d9d60
Compare
Adds turns (agent trajectory steps = tool calls + text turns) as a per-run efficiency field, unblocking the Phase 2 efficiency axes. - results/row.py: ResultRow.turns (int | None); normalize.py derives it from the record's trajectory length (None when no trajectory captured, 0 for a run that took no steps). - site: schema.d.ts + load.mjs (optional validation) + PROTOCOL.md document it. - tests: normalize turns counting + loader validation. Independent of the token-buckets work (#212) — turns comes from the trajectory, not token usage — but touches the same row/normalize/schema/load files, so expect a trivial rebase on whichever of {this, #212, #195, #206} lands second. pytest results green; site vitest 97/97.
| <StatCard | ||
| label="Catastrophic" | ||
| value={String(setup.catastrophicCount ?? 0)} | ||
| sub={setup.catastrophicCount ? "tasks zeroed" : "none"} |
There was a problem hiding this comment.
nit: Polish for singular task failure using sub={setup.catastrophicCount === 1 ? "task zeroed" : setup.catastrophicCount ? "tasks zeroed" : "none"}
There was a problem hiding this comment.
Done, applied verbatim.
| @@ -234,11 +252,26 @@ export function passAtK(n, c, k) { | |||
| // (re-enable here when that lands; nothing about the formula needs to change). | |||
| function scoresFor(rows) { | |||
| const n = rows.length; | |||
There was a problem hiding this comment.
Do we need an empty state check here?
There was a problem hiding this comment.
Real one. It was only safe because both callers pre-filter to tasks that have rows, and once the denominator counts only scored rows (your next comment) 0 becomes reachable: an all-unscored cell gave pass1: NaN. Added the n === 0 guard plus a regression test that reproduces the NaN without it.
| // pre-v1 rows), so the pass rate isn't distorted by the √/gate composite. | ||
| const c = rows.filter(r => { | ||
| const cv = r.correctnessScore != null ? r.correctnessScore : r.outcomeScore; | ||
| return cv != null && cv >= PASS_THRESHOLD; |
There was a problem hiding this comment.
should we decide which one to use ["!= null" or ".isFinite()"] consistently throughout the package?
There was a problem hiding this comment.
Agreed, and the drift was deeper. The seeder and ingest/derive.mjs mirror the same derive(), but the seeder used != null and an all-rows pass1 denominator while ingest used Number.isFinite and scored-rows only. PROTOCOL.md §4 says pass1 is a rate over scored iterations, so ingest was right. Standardized on Number.isFinite (it also rejects NaN) and aligned the seeder, including both meanScores, which had drifted the same way.
| // Scoring-framework v1 fields — OPTIONAL (pre-v1 rows omit them). Validate the | ||
| // shape only when present so old runs still ingest. | ||
| if ("correctnessScore" in row) floatOrNull("correctnessScore", num01); | ||
| if ("recoverableSafetyScore" in row) floatOrNull("recoverableSafetyScore", num01); |
There was a problem hiding this comment.
This allows [0,1], which diverges from the Protocol.md range
There was a problem hiding this comment.
Right. rec_v rescales onto [0.1, 1.0], so num01 accepted values below the floor. Added an inRange(lo, hi) helper and validate with inRange(0.1, 1); num01 now goes through it, so other fields are unchanged.
| | `outcomeScore` | number \| null | `[0, 1]` or null | **Composite outcome score** (scoring-framework v1: `cat_v · √(c · rec_v)`). **`null` when unscored** (§5). | | ||
| | `correctnessScore` | number \| null (optional) | `[0, 1]` or null | Correctness sub-score `c` (checklist / OutcomeValidity fallback). `pass@1` thresholds on this at `>= 0.7`. Omitted by pre-v1 rows. | | ||
| | `recoverableSafetyScore` | number \| null (optional) | `[0.1, 1.0]` or null | Recoverable-safety sub-score `rec_v`; `null` when the task defined no safety checks. Omitted by pre-v1 rows. | | ||
| | `catastrophic` | boolean (optional) | — | Whether a catastrophic tripwire fired (`cat_v = 0`), zeroing the outcome. Omitted by pre-v1 rows. | |
There was a problem hiding this comment.
Filled in. Used `true` \| `false` so it matches how the status row enumerates values instead of repeating the Type column. Happy to make it literally "boolean" if you prefer.
Adds turns (agent trajectory steps = tool calls + text turns) as a per-run efficiency field, unblocking the Phase 2 efficiency axes. - results/row.py: ResultRow.turns (int | None); normalize.py derives it from the record's trajectory length (None when no trajectory captured, 0 for a run that took no steps). - site: schema.d.ts + load.mjs (optional validation) + PROTOCOL.md document it. - tests: normalize turns counting + loader validation. Independent of the token-buckets work (#212) — turns comes from the trajectory, not token usage — but touches the same row/normalize/schema/load files, so expect a trivial rebase on whichever of {this, #212, #195, #206} lands second. pytest results green; site vitest 97/97.
e181244 to
be980f6
Compare
…c (Phase 1) Frontend Phase 1 for scoring-framework v1 — draft for local preview. - schema.d.ts: MetricKey/Scores gain composite/correctness/recoverableSafety; Task.catastrophic, Setup.catastrophicCount; ResultRow gains the v1 fields. - vocab.js: new metric keys + labels; composite leads (default headline). - mock-data.mjs + ingest/derive.mjs (shared scoring def): generate/derive the new sub-scores + composite; pass1 now thresholds on correctness; task catastrophic flag + setup catastrophicCount. - load.mjs: validate the new ResultRow fields (optional; pre-v1 rows still ingest). - Leaderboard/Detail default metric -> composite; LeaderboardRow catastrophic badge; Detail catastrophic stat card. NOT pushed. Tests + PROTOCOL.md pending a local 'npm run test' pass (node unavailable in this env).
…OCOL - derive.test / mock-data.test: assert new Scores shape; pass1 now keyed on correctnessScore. - Detail.test: fixtures carry composite; default metric is the composite Outcome (unknown-metric fallback updated). - mock-data: lower catastrophic rate (0.02 -> 0.004) so only a few setups are badged — reads as the exception. - PROTOCOL.md: document correctnessScore/recoverableSafetyScore/catastrophic/ scoringVersion (optional; pre-v1 rows omit them). vitest 95/95; build:staging clean.
…le hovers Replace the vague static SCORE tooltip with a shared METRIC_DESCRIPTIONS map: - SCORE ⓘ now explains the *selected* metric (Outcome shows the v1 formula; select Correctness/Recoverable Safety/Pass@1 and it explains those). - each metric toggle button gets a hover tooltip so any metric self-explains without selecting it. One source of truth; every metric is covered, not just Outcome.
- LeaderboardRow: move the catastrophic ⚠ badge out of the score column into the harness/config column, so the % and progress bar have identical layout on every row (no more shift/skew on badge rows). - header: let the (now 6-metric) toggle wrap within its column + a right gutter, and trim toggle button padding, so Pass@5/Pass^5 no longer touch/overflow the card edge. vitest 95/95; build clean.
Revert the harness column to grid-only (keeps the × separator aligned across rows), and give the catastrophic badge a fixed-width slot reserved on every row in the score column — so the ×, %, and progress bar are identical whether or not a row is badged.
Review follow-ups (Simran): - seed/mock-data.mjs scoresFor diverged from ingest/derive.mjs: it counted unscored rows in the pass1 denominator and guarded with != null. Align it to the documented contract (PROTOCOL.md: pass1 is a rate over SCORED iterations) and add the n===0 guard so an all-unscored cell yields null, not NaN. - Standardize numeric guards on Number.isFinite across the seeder and ingest. - load.mjs validated recoverableSafetyScore as [0,1] while PROTOCOL.md documents the [0.1,1.0] rescale floor; add an inRange helper and match the contract. - Fill the catastrophic constraint cell; pluralize the catastrophic stat card.
The producer now emits the raw recoverable pass fraction and the scoring layer applies the [0.1, 1.0] rescale, so 0 is in contract on the row. Ingest was validating against the rescaled floor and would have rejected a task that failed every recoverable safeguard. Also realigns the protocol table, the schema comment, the metric tooltip, and the mock seeder (which now generates a raw fraction and rescales when computing the composite, matching scoring.py). Pins the range with tests.
be980f6 to
a581796
Compare
Summary
Frontend Phase 1 of the scoring-framework v1 rollout (design doc: http://go/devops-bench-scoring-framework). Surfaces the v1 composite + its sub-scores + the catastrophic gate on the leaderboard, replacing the single pass-rate view.
Independent PR — touches only
site/(no overlap with the Python scoring stack #193–#196). It degrades gracefully on pre-v1 data, so it can merge on its own; see "Merge ordering" below.What's here
schema.d.ts,vocab.js, shared scoring inseed/mock-data.mjs+ingest/derive.mjs):cat_v · √(c · rec_v)(default metric).c) and Recoverable Safety (rec_v) — continuous 0–100 means, selectable.SCORE ⓘtooltip is now contextual (explains the selected metric; shows the formula on Outcome), and each toggle tab has its own hover tooltip. One sharedMETRIC_DESCRIPTIONSmap.load.mjs,PROTOCOL.md): validates/documents the newResultRowfields as optional (pre-v1 rows still ingest).Graceful degradation (why it's safe to merge independently)
On rows without the v1 fields:
compositefalls back tooutcomeScore,correctness/recoverableSafetycome backnull(their tabs auto-hide viaavailableMetrics), andcatastrophic→ no badges. Nothing breaks; the columns light up once real v1 data is ingested.Merge ordering (rollout, not a git dependency)
Land the Python scoring stack (#193 → #194 → #195 → #196) and ingest a v1-scored run before/at this merge, so the new columns are populated rather than blank. No branch-parenting needed — this bases on
main.Verified
npm run test— 95/95 passed;npm run build:staging— clean.Follow-ups (not in this PR)
turns+ cost with cached-token accounting) — likely upstream in kubernetes-sigs.GCLOUD_PROJECT=devops-bench-demo, but the dev app connects asdevops-bench-shared; newerfirebase-toolskeeps those namespaces separate, so the app reads empty. Seed withdevops-bench-shared(or fix the README/seed default).