Skip to content

feat(site): surface v1 composite + correctness + safety + catastrophic (frontend Phase 1) - #206

Open
jessie1111101 wants to merge 7 commits into
mainfrom
feat/scoring-v1-frontend
Open

feat(site): surface v1 composite + correctness + safety + catastrophic (frontend Phase 1)#206
jessie1111101 wants to merge 7 commits into
mainfrom
feat/scoring-v1-frontend

Conversation

@jessie1111101

Copy link
Copy Markdown
Collaborator

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

  • New metrics (schema.d.ts, vocab.js, shared scoring in seed/mock-data.mjs + ingest/derive.mjs):
    • Outcome — the composite headline, cat_v · √(c · rec_v) (default metric).
    • Correctness (c) and Recoverable Safety (rec_v) — continuous 0–100 means, selectable.
    • Pass@1 repointed to threshold on correctness (a clean "did it work" rate). Pass@5/Pass^5 stay disabled until multi-iteration runs land.
  • Catastrophic surfaced as a red ⚠ N badge per row (a binary veto, not a score column) + a Catastrophic stat card on the detail page; it zeroes the Outcome of affected tasks.
  • Per-metric explanations — the SCORE ⓘ tooltip is now contextual (explains the selected metric; shows the formula on Outcome), and each toggle tab has its own hover tooltip. One shared METRIC_DESCRIPTIONS map.
  • Contract (load.mjs, PROTOCOL.md): validates/documents the new ResultRow fields as optional (pre-v1 rows still ingest).

Graceful degradation (why it's safe to merge independently)

On rows without the v1 fields: composite falls back to outcomeScore, correctness/recoverableSafety come back null (their tabs auto-hide via availableMetrics), and catastrophic → 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

  • Previewed locally against the Firestore emulator + mock seed (all columns populate, ⚠ badges on ~4/8 setups, contextual tooltips).
  • npm run test95/95 passed; npm run build:staging — clean.

Follow-ups (not in this PR)

  • Phase 2 — efficiency axes: raw latency / tokens / turns / cost + Pareto flags. Blocked on producer work (emit turns + cost with cached-token accounting) — likely upstream in kubernetes-sigs.
  • README/seed bug: the documented emulator seed uses GCLOUD_PROJECT=devops-bench-demo, but the dev app connects as devops-bench-shared; newer firebase-tools keeps those namespaces separate, so the app reads empty. Seed with devops-bench-shared (or fix the README/seed default).

@jessie1111101
jessie1111101 force-pushed the feat/scoring-v1-frontend branch from 3a21b8f to 36d9d60 Compare July 22, 2026 18:28
jessie1111101 added a commit that referenced this pull request Jul 22, 2026
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.
Comment thread site/src/pages/Detail.jsx Outdated
<StatCard
label="Catastrophic"
value={String(setup.catastrophicCount ?? 0)}
sub={setup.catastrophicCount ? "tasks zeroed" : "none"}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Polish for singular task failure using sub={setup.catastrophicCount === 1 ? "task zeroed" : setup.catastrophicCount ? "tasks zeroed" : "none"}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, applied verbatim.

Comment thread site/seed/mock-data.mjs Outdated
@@ -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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an empty state check here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread site/seed/mock-data.mjs Outdated
// 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we decide which one to use ["!= null" or ".isFinite()"] consistently throughout the package?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread site/ingest/load.mjs
// 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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows [0,1], which diverges from the Protocol.md range

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread site/ingest/PROTOCOL.md Outdated
| `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. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/-/boolean

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

itssimrank
itssimrank previously approved these changes Jul 27, 2026
jessie1111101 added a commit that referenced this pull request Jul 28, 2026
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.
@jessie1111101
jessie1111101 force-pushed the feat/scoring-v1-frontend branch from e181244 to be980f6 Compare July 28, 2026 17:08
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants