evijit HF Staff Claude Opus 4.7 (1M context) commited on
Commit
ca20f78
·
1 Parent(s): cb0ce7c

Restore curated benchmark families; polish frontier panel UX

Browse files

* clean-hierarchy: stop nuking curated benchmark families whose rows
happen to all be republished by a single source. The strict-subset
wrapper rule and the consolidate-poorer-duplicate pass were both
treating "tau2-bench has all its benches under exgentic-open-agent"
as a redundant wrapper — but tau2-bench is a real curated leaderboard,
not a duplicate. Two changes:
- the strict-subset rule now requires the two family keys to be
textually related (slug equality or substring) before declaring
one a wrapper of the other. live-bench still absorbs livebench
(their slugs match); tau2-bench is no longer absorbed by
exgentic-open-agent (`tau2bench` vs `exgenticopenagent` — no
relationship).
- isPoorerDuplicate now also requires the two bench instances to
share an eval_summary_id before considering them duplicates.
gaia%2Fgaia and hal%2Fgaia are different physical rows reporting
the same benchmark concept; both should surface. live-bench's
`live-bench%2Flivebench-coding` is literally the same row as
livebench's, so the dedup still fires there.
Net effect: family count goes from 50 to 64, restoring tau-bench,
tau2-bench, gaia, judgebench, mmlu, mmlu-pro, mathvista, mathverse,
videomme, appworld, arena, terminal-bench-2-0 etc. as their own
cards on /evals.

* score-distribution: reorganise the panel header into two stacked
rows so the View toggle (Distribution / Frontier) and the Metric
picker chips read as visually distinct controls — same chip styling
was making them blur into one undifferentiated strip. Each row now
carries its own subdued kicker label ("View", "Metric"). Add a
getMetricChipLabel helper that walks display_name → metric_name →
metric_id → column_key tail and humanises underscores so AgentHarm's
null-display-name metrics surface as "avg full score", "avg refusals"
etc. instead of four chips literally labelled "Metric".

* score-distribution: render the Frontier plot in HTML over an
unstretched 0-100 SVG so the dots stay round and the year labels
stay readable. preserveAspectRatio="none" was squashing the
circles into ovals and the glyphs into illegible bars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

components/eval-detail.tsx CHANGED
@@ -463,19 +463,46 @@ function describeLeaderboardMetric(metric: LeaderboardMetric) {
463
  return metric.canonical_display_name || metric.display_name
464
  }
465
 
466
- function getCompactMetricLabel(value: string | undefined) {
467
- if (!value) {
468
- return "Metric"
469
- }
470
-
471
  const parts = value
472
  .split("/")
473
  .map((part) => part.trim())
474
  .filter(Boolean)
475
-
476
  return parts[parts.length - 1] ?? value
477
  }
478
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  /**
480
  * Best-effort "setup" caption for a row (e.g. "8-shot CoT", "0-shot").
481
  *
@@ -1078,14 +1105,23 @@ export function EvalDetail({
1078
  />
1079
  )}
1080
 
1081
- {/* Score distribution — paper-themed mean/median/quartile summary */}
 
1082
  {leaderboardRows.length >= 3 && (
1083
  <div className="mb-4">
1084
  <ScoreDistribution
1085
- values={leaderboardRows.map((r) => r.modelResult.score)}
1086
- label={lb.metric_config.unit ?? "Score"}
1087
- unit={lb.metric_config.unit}
1088
- lowerIsBetter={lb.metric_config.lower_is_better}
 
 
 
 
 
 
 
 
1089
  />
1090
  </div>
1091
  )}
@@ -2017,18 +2053,26 @@ function MultiMetricLeaderboard({
2017
  {(() => {
2018
  const distSeries = visibleMetrics
2019
  .map((metric) => {
2020
- const values = filteredRows
2021
- .map((r) => r.values[metric.column_key])
2022
- .filter((v): v is number => isNumericScore(v))
2023
- if (values.length < 3) return null
2024
- const label = getCompactMetricLabel(metric.display_name)
 
 
 
 
 
 
 
2025
  return {
2026
  key: metric.column_key,
2027
  label,
2028
  caption: metric.unit ?? undefined,
2029
- values,
2030
  unit: metric.unit ?? undefined,
2031
  lowerIsBetter: metric.lower_is_better,
 
2032
  }
2033
  })
2034
  .filter((entry): entry is NonNullable<typeof entry> => entry !== null)
 
463
  return metric.canonical_display_name || metric.display_name
464
  }
465
 
466
+ function compactizePath(value: string): string {
 
 
 
 
467
  const parts = value
468
  .split("/")
469
  .map((part) => part.trim())
470
  .filter(Boolean)
 
471
  return parts[parts.length - 1] ?? value
472
  }
473
 
474
+ function getCompactMetricLabel(value: string | undefined): string {
475
+ if (value && value.trim()) return compactizePath(value)
476
+ return "Metric"
477
+ }
478
+
479
+ /**
480
+ * Build a chip-friendly label for a leaderboard metric. Prefers
481
+ * display_name, then metric_name, then a humanised tail of metric_id /
482
+ * column_key. The upstream pipeline frequently leaves display_name
483
+ * blank (e.g. inspect_evals/avg_full_score), in which case the
484
+ * column_key tail ('avg_full_score') is what we want to surface.
485
+ */
486
+ function getMetricChipLabel(metric: {
487
+ display_name?: string | null
488
+ metric_name?: string | null
489
+ metric_id?: string | null
490
+ column_key?: string | null
491
+ }): string {
492
+ const candidates = [
493
+ metric.display_name,
494
+ metric.metric_name,
495
+ metric.metric_id,
496
+ metric.column_key,
497
+ ]
498
+ for (const c of candidates) {
499
+ if (c && String(c).trim()) {
500
+ return compactizePath(String(c)).replace(/_/g, " ")
501
+ }
502
+ }
503
+ return "Metric"
504
+ }
505
+
506
  /**
507
  * Best-effort "setup" caption for a row (e.g. "8-shot CoT", "0-shot").
508
  *
 
1105
  />
1106
  )}
1107
 
1108
+ {/* Score distribution — paper-themed mean/median/quartile summary,
1109
+ with an optional Frontier toggle when models carry release dates. */}
1110
  {leaderboardRows.length >= 3 && (
1111
  <div className="mb-4">
1112
  <ScoreDistribution
1113
+ series={[{
1114
+ key: "primary",
1115
+ label: lb.metric_config.unit ?? "Score",
1116
+ values: leaderboardRows.map((r) => r.modelResult.score),
1117
+ unit: lb.metric_config.unit,
1118
+ lowerIsBetter: lb.metric_config.lower_is_better,
1119
+ points: leaderboardRows.map((r) => ({
1120
+ score: r.modelResult.score,
1121
+ releaseDate: r.modelResult.model_info.release_date,
1122
+ modelName: r.modelResult.model_info.name,
1123
+ })),
1124
+ }]}
1125
  />
1126
  </div>
1127
  )}
 
2053
  {(() => {
2054
  const distSeries = visibleMetrics
2055
  .map((metric) => {
2056
+ const points: Array<{ score: number; releaseDate: string | null; modelName: string }> = []
2057
+ for (const r of filteredRows) {
2058
+ const score = r.values[metric.column_key]
2059
+ if (!isNumericScore(score)) continue
2060
+ points.push({
2061
+ score,
2062
+ releaseDate: r.model_info?.release_date ?? null,
2063
+ modelName: r.model_info?.name ?? "",
2064
+ })
2065
+ }
2066
+ if (points.length < 3) return null
2067
+ const label = getMetricChipLabel(metric)
2068
  return {
2069
  key: metric.column_key,
2070
  label,
2071
  caption: metric.unit ?? undefined,
2072
+ values: points.map((p) => p.score),
2073
  unit: metric.unit ?? undefined,
2074
  lowerIsBetter: metric.lower_is_better,
2075
+ points,
2076
  }
2077
  })
2078
  .filter((entry): entry is NonNullable<typeof entry> => entry !== null)
components/score-distribution.tsx CHANGED
@@ -12,6 +12,17 @@ interface ScoreSeries {
12
  values: number[]
13
  unit?: string
14
  lowerIsBetter?: boolean
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
 
17
  interface ScoreDistributionProps {
@@ -38,6 +49,27 @@ interface SummaryStats {
38
  q3: number
39
  }
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  function computeStats(values: number[]): SummaryStats | null {
42
  const sorted = values.filter((v) => Number.isFinite(v)).slice().sort((a, b) => a - b)
43
  const n = sorted.length
@@ -116,6 +148,45 @@ export function ScoreDistribution({
116
 
117
  const stats = useMemo(() => (active ? computeStats(active.values) : null), [active])
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  const density = useMemo(() => {
120
  if (!active || !stats) return null
121
  if (stats.max === stats.min) {
@@ -199,15 +270,58 @@ export function ScoreDistribution({
199
  }}
200
  >
201
  {!compact && (
202
- <div className="flex items-center justify-between gap-3 mb-3 flex-wrap">
203
- <div className="flex items-center gap-3 min-w-0">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  <div
205
  className="font-mono uppercase shrink-0"
206
- style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
207
  >
208
- Score distribution
209
  </div>
210
- {showPicker ? (
 
 
 
 
 
 
 
 
 
 
 
 
211
  <div className="flex flex-wrap items-center gap-1.5">
212
  {seriesList.map((s) => {
213
  const on = s.key === active.key
@@ -232,22 +346,17 @@ export function ScoreDistribution({
232
  )
233
  })}
234
  </div>
235
- ) : (
236
- <span
237
- className="font-mono uppercase truncate"
238
- style={{ fontSize: 11, letterSpacing: "0.08em", color: "var(--fg)" }}
239
- title={active.label}
240
- >
241
- · {active.label}
242
- </span>
243
- )}
244
- </div>
245
- <div
246
- className="font-mono uppercase shrink-0"
247
- style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
248
- >
249
- {directionHint}
250
- </div>
251
  </div>
252
  )}
253
 
@@ -261,6 +370,15 @@ export function ScoreDistribution({
261
  </div>
262
  )}
263
 
 
 
 
 
 
 
 
 
 
264
  <svg
265
  viewBox={`0 0 ${width} ${plotHeight + 8}`}
266
  preserveAspectRatio="none"
@@ -349,8 +467,11 @@ export function ScoreDistribution({
349
  vectorEffect="non-scaling-stroke"
350
  />
351
  </svg>
 
352
 
353
- {/* Caption row */}
 
 
354
  <div
355
  className="mt-2 flex flex-wrap items-baseline font-mono"
356
  style={{
@@ -389,8 +510,9 @@ export function ScoreDistribution({
389
  </span>
390
  ))}
391
  </div>
 
392
 
393
- {!compact && (
394
  <div
395
  className="mt-1 flex items-center gap-3 font-mono"
396
  style={{ fontSize: 9, letterSpacing: "0.06em", color: "var(--fg-subtle)" }}
@@ -436,3 +558,369 @@ export function ScoreDistribution({
436
  </div>
437
  )
438
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  values: number[]
13
  unit?: string
14
  lowerIsBetter?: boolean
15
+ /**
16
+ * Per-model rows for the optional frontier-plot view. When provided
17
+ * (and at least one row carries a parseable releaseDate), the panel
18
+ * exposes a chip toggle that swaps the density curve for a
19
+ * release-date frontier (cumulative best score over time).
20
+ */
21
+ points?: Array<{
22
+ score: number
23
+ releaseDate?: string | null
24
+ modelName?: string | null
25
+ }>
26
  }
27
 
28
  interface ScoreDistributionProps {
 
49
  q3: number
50
  }
51
 
52
+ function parseReleaseDate(value: string | null | undefined): number | null {
53
+ if (!value) return null
54
+ const raw = String(value).trim()
55
+ if (!raw) return null
56
+ // Numeric epoch — treat seconds-since-epoch values as such, ms otherwise.
57
+ const numeric = Number(raw)
58
+ if (!Number.isNaN(numeric) && !raw.includes("-")) {
59
+ const ms = numeric > 1_000_000_000_000 ? numeric : numeric * 1000
60
+ return Number.isFinite(ms) ? ms : null
61
+ }
62
+ const parsed = new Date(raw).getTime()
63
+ return Number.isFinite(parsed) ? parsed : null
64
+ }
65
+
66
+ const MONTH_LABELS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
67
+ function formatMonthYear(ms: number): string {
68
+ const d = new Date(ms)
69
+ if (Number.isNaN(d.getTime())) return ""
70
+ return `${MONTH_LABELS[d.getUTCMonth()]} ${d.getUTCFullYear()}`
71
+ }
72
+
73
  function computeStats(values: number[]): SummaryStats | null {
74
  const sorted = values.filter((v) => Number.isFinite(v)).slice().sort((a, b) => a - b)
75
  const n = sorted.length
 
148
 
149
  const stats = useMemo(() => (active ? computeStats(active.values) : null), [active])
150
 
151
+ // Frontier-plot data: parse release dates, sort by time, then walk the
152
+ // sequence emitting an event whenever a model improves on the best
153
+ // score seen so far. Honours lowerIsBetter so e.g. "Mean Response
154
+ // Time · ms" shows the frontier descending instead of climbing.
155
+ const frontier = useMemo(() => {
156
+ if (!active?.points || active.points.length === 0) return null
157
+ const lowerIsBetter = active.lowerIsBetter ?? false
158
+ const parsed = active.points
159
+ .map((p) => {
160
+ const t = parseReleaseDate(p.releaseDate)
161
+ if (t == null) return null
162
+ if (!Number.isFinite(p.score)) return null
163
+ return { time: t, score: p.score, name: p.modelName ?? "" }
164
+ })
165
+ .filter((p): p is { time: number; score: number; name: string } => p !== null)
166
+ .sort((a, b) => a.time - b.time)
167
+
168
+ if (parsed.length < 2) return null
169
+
170
+ let best = lowerIsBetter ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
171
+ const events: typeof parsed = []
172
+ for (const p of parsed) {
173
+ const better = lowerIsBetter ? p.score < best : p.score > best
174
+ if (better) {
175
+ best = p.score
176
+ events.push(p)
177
+ }
178
+ }
179
+ if (events.length < 2) return null
180
+ return { events, samples: parsed }
181
+ }, [active])
182
+
183
+ const canShowFrontier = frontier != null
184
+ const [view, setView] = useState<"distribution" | "frontier">("distribution")
185
+ // If the active series doesn't support frontier (e.g. user switched to
186
+ // a metric whose models don't carry release_date), fall back to the
187
+ // distribution view rather than rendering an empty panel.
188
+ const effectiveView = canShowFrontier ? view : "distribution"
189
+
190
  const density = useMemo(() => {
191
  if (!active || !stats) return null
192
  if (stats.max === stats.min) {
 
270
  }}
271
  >
272
  {!compact && (
273
+ <div className="mb-3 space-y-2">
274
+ {/* Row 1 View toggle (left) + direction hint (right). The
275
+ kicker label makes it clear that these chips switch the
276
+ chart type, distinguishing them from the metric chips
277
+ below. */}
278
+ <div className="flex items-center justify-between gap-3 flex-wrap">
279
+ <div className="flex items-center gap-2 min-w-0">
280
+ <span
281
+ className="font-mono uppercase shrink-0"
282
+ style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
283
+ >
284
+ {canShowFrontier ? "View" : "Score distribution"}
285
+ </span>
286
+ {canShowFrontier && (
287
+ <div className="flex items-center gap-1">
288
+ <button
289
+ type="button"
290
+ className={`ec-pill${effectiveView === "distribution" ? " on" : ""}`}
291
+ onClick={() => setView("distribution")}
292
+ >
293
+ Distribution
294
+ </button>
295
+ <button
296
+ type="button"
297
+ className={`ec-pill${effectiveView === "frontier" ? " on" : ""}`}
298
+ onClick={() => setView("frontier")}
299
+ title="Frontier score over model release dates (cumulative best)."
300
+ >
301
+ Frontier
302
+ </button>
303
+ </div>
304
+ )}
305
+ </div>
306
  <div
307
  className="font-mono uppercase shrink-0"
308
+ style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
309
  >
310
+ {directionHint}
311
  </div>
312
+ </div>
313
+
314
+ {/* Row 2 — Metric chips. Only shown when there's more than
315
+ one series; otherwise the active label gets a quiet inline
316
+ caption next to the view kicker. */}
317
+ {showPicker ? (
318
+ <div className="flex items-baseline gap-2 flex-wrap">
319
+ <span
320
+ className="font-mono uppercase shrink-0"
321
+ style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
322
+ >
323
+ Metric
324
+ </span>
325
  <div className="flex flex-wrap items-center gap-1.5">
326
  {seriesList.map((s) => {
327
  const on = s.key === active.key
 
346
  )
347
  })}
348
  </div>
349
+ </div>
350
+ ) : (
351
+ <div
352
+ className="font-mono uppercase truncate"
353
+ style={{ fontSize: 11, letterSpacing: "0.08em", color: "var(--fg-muted)" }}
354
+ title={active.label}
355
+ >
356
+ {active.label}
357
+ {active.unit ? <span style={{ color: "var(--fg-subtle)" }}>{" · " + active.unit}</span> : null}
358
+ </div>
359
+ )}
 
 
 
 
 
360
  </div>
361
  )}
362
 
 
370
  </div>
371
  )}
372
 
373
+ {effectiveView === "frontier" && frontier ? (
374
+ <FrontierPlot
375
+ events={frontier.events}
376
+ samples={frontier.samples}
377
+ unit={active.unit}
378
+ lowerIsBetter={active.lowerIsBetter ?? false}
379
+ label={active.label}
380
+ />
381
+ ) : (
382
  <svg
383
  viewBox={`0 0 ${width} ${plotHeight + 8}`}
384
  preserveAspectRatio="none"
 
467
  vectorEffect="non-scaling-stroke"
468
  />
469
  </svg>
470
+ )}
471
 
472
+ {/* Caption row — hidden in frontier view since it tracks
473
+ distribution stats; the frontier panel renders its own caption. */}
474
+ {effectiveView !== "frontier" && (
475
  <div
476
  className="mt-2 flex flex-wrap items-baseline font-mono"
477
  style={{
 
510
  </span>
511
  ))}
512
  </div>
513
+ )}
514
 
515
+ {!compact && effectiveView !== "frontier" && (
516
  <div
517
  className="mt-1 flex items-center gap-3 font-mono"
518
  style={{ fontSize: 9, letterSpacing: "0.06em", color: "var(--fg-subtle)" }}
 
558
  </div>
559
  )
560
  }
561
+
562
+ interface FrontierPlotProps {
563
+ /** Strictly-improving subset of the input — each entry pushes the
564
+ * cumulative best score further. Already sorted ascending by time. */
565
+ events: Array<{ time: number; score: number; name: string }>
566
+ /** Every dated sample (improving or not), used as background dots. */
567
+ samples: Array<{ time: number; score: number; name: string }>
568
+ unit?: string
569
+ lowerIsBetter: boolean
570
+ label: string
571
+ }
572
+
573
+ function FrontierPlot({ events, samples, unit, lowerIsBetter, label }: FrontierPlotProps) {
574
+ const PLOT_HEIGHT = 180
575
+ const PAD_T = 8
576
+ const PAD_B = 22 // room for year labels under the axis
577
+ const PAD_L_PCT = 1
578
+ const PAD_R_PCT = 1
579
+
580
+ const tMin = Math.min(...samples.map((s) => s.time))
581
+ const tMaxData = Math.max(...samples.map((s) => s.time))
582
+ // Always extend the rightmost edge to "now" so the user sees how
583
+ // long the current frontier holder has been on top.
584
+ const tMax = Math.max(tMaxData, Date.now())
585
+ const tRange = tMax - tMin || 1
586
+ const sValues = samples.map((s) => s.score)
587
+ const sMin = Math.min(...sValues)
588
+ const sMax = Math.max(...sValues)
589
+ const sRange = sMax - sMin || Math.abs(sMax) || 1
590
+ // Pad y so dots don't kiss the borders.
591
+ const yLo = sMin - sRange * 0.05
592
+ const yHi = sMax + sRange * 0.05
593
+ const yRange = yHi - yLo || 1
594
+
595
+ // Percent helpers — used for both HTML overlay positioning and the
596
+ // SVG path (which uses a 0-100 viewBox so the line scales with the
597
+ // container without distorting other glyphs).
598
+ const xPct = (t: number) =>
599
+ PAD_L_PCT + ((t - tMin) / tRange) * (100 - PAD_L_PCT - PAD_R_PCT)
600
+ const yPct = (s: number) =>
601
+ 100 - ((s - yLo) / yRange) * 100 // 0 at top, 100 at bottom
602
+
603
+ // Pixel helpers for the step-line SVG. Keep its viewBox at 100x100
604
+ // so it overlays the container 1:1, while strokeWidth uses
605
+ // vectorEffect=non-scaling-stroke so the line stays crisp.
606
+ let d = ""
607
+ for (let i = 0; i < events.length; i++) {
608
+ const e = events[i]
609
+ const x = xPct(e.time)
610
+ const y = yPct(e.score)
611
+ if (i === 0) {
612
+ d += `M${x.toFixed(3)},${y.toFixed(3)} `
613
+ } else {
614
+ const prev = events[i - 1]
615
+ const yPrev = yPct(prev.score)
616
+ d += `L${x.toFixed(3)},${yPrev.toFixed(3)} L${x.toFixed(3)},${y.toFixed(3)} `
617
+ }
618
+ }
619
+ if (events.length > 0) {
620
+ const last = events[events.length - 1]
621
+ d += `L${xPct(tMax).toFixed(3)},${yPct(last.score).toFixed(3)}`
622
+ }
623
+
624
+ // Year tick marks along the x-axis. Keep at most 6 to avoid label
625
+ // collisions on narrow viewports.
626
+ const startYear = new Date(tMin).getUTCFullYear()
627
+ const endYear = new Date(tMax).getUTCFullYear()
628
+ const yearSpan = endYear - startYear
629
+ const tickStep = yearSpan <= 6 ? 1 : Math.ceil(yearSpan / 6)
630
+ const yearTicks: number[] = []
631
+ for (let y = startYear; y <= endYear; y += tickStep) yearTicks.push(y)
632
+
633
+ // Pre-bucket samples that *aren't* on the frontier so we don't
634
+ // double-render them (the frontier dots are emphasised separately).
635
+ const eventTimes = new Set(events.map((e) => `${e.time}|${e.score}`))
636
+ const bgSamples = samples.filter((s) => !eventTimes.has(`${s.time}|${s.score}`))
637
+
638
+ // Local hover state so we can render a richer label than the native
639
+ // `title=` tooltip — keeps the dot and the popup nameplate in sync
640
+ // even when the cursor sits right between two dots.
641
+ const [hover, setHover] = useState<{
642
+ x: number
643
+ y: number
644
+ name: string
645
+ when: string
646
+ score: string
647
+ onFrontier: boolean
648
+ } | null>(null)
649
+
650
+ return (
651
+ <div>
652
+ <div
653
+ style={{
654
+ position: "relative",
655
+ width: "100%",
656
+ height: PLOT_HEIGHT,
657
+ paddingTop: PAD_T,
658
+ paddingBottom: PAD_B,
659
+ boxSizing: "border-box",
660
+ }}
661
+ onMouseLeave={() => setHover(null)}
662
+ >
663
+ {/* Inner plot canvas (the area minus axis padding). */}
664
+ <div
665
+ style={{
666
+ position: "absolute",
667
+ top: PAD_T,
668
+ bottom: PAD_B,
669
+ left: 0,
670
+ right: 0,
671
+ }}
672
+ >
673
+ {/* Step line. The SVG uses a 0-100 viewBox so its path lines
674
+ up with HTML overlays positioned via the same xPct/yPct
675
+ helpers; non-scaling-stroke keeps the stroke crisp. */}
676
+ <svg
677
+ viewBox="0 0 100 100"
678
+ preserveAspectRatio="none"
679
+ style={{
680
+ position: "absolute",
681
+ inset: 0,
682
+ width: "100%",
683
+ height: "100%",
684
+ pointerEvents: "none",
685
+ }}
686
+ aria-hidden
687
+ >
688
+ <path
689
+ d={d}
690
+ fill="none"
691
+ stroke="var(--accent)"
692
+ strokeWidth={1.5}
693
+ vectorEffect="non-scaling-stroke"
694
+ strokeLinejoin="round"
695
+ strokeLinecap="round"
696
+ />
697
+ </svg>
698
+
699
+ {/* Background sample dots — every dated model that's NOT on
700
+ the frontier. Rendered as HTML so they're crisp circles
701
+ and individually clickable / focusable. */}
702
+ {bgSamples.map((s, i) => (
703
+ <button
704
+ key={`s-${i}`}
705
+ type="button"
706
+ aria-label={`${s.name || "Model"} · ${formatMonthYear(s.time)} · ${formatValue(s.score, unit)}`}
707
+ onMouseEnter={(event) => {
708
+ const rect = event.currentTarget.parentElement!.getBoundingClientRect()
709
+ const dot = event.currentTarget.getBoundingClientRect()
710
+ setHover({
711
+ x: dot.left + dot.width / 2 - rect.left,
712
+ y: dot.top + dot.height / 2 - rect.top,
713
+ name: s.name || "Model",
714
+ when: formatMonthYear(s.time),
715
+ score: formatValue(s.score, unit),
716
+ onFrontier: false,
717
+ })
718
+ }}
719
+ onFocus={(event) => {
720
+ const rect = event.currentTarget.parentElement!.getBoundingClientRect()
721
+ const dot = event.currentTarget.getBoundingClientRect()
722
+ setHover({
723
+ x: dot.left + dot.width / 2 - rect.left,
724
+ y: dot.top + dot.height / 2 - rect.top,
725
+ name: s.name || "Model",
726
+ when: formatMonthYear(s.time),
727
+ score: formatValue(s.score, unit),
728
+ onFrontier: false,
729
+ })
730
+ }}
731
+ style={{
732
+ position: "absolute",
733
+ left: `${xPct(s.time)}%`,
734
+ top: `${yPct(s.score)}%`,
735
+ transform: "translate(-50%, -50%)",
736
+ width: 7,
737
+ height: 7,
738
+ borderRadius: "50%",
739
+ background: "var(--fg-subtle)",
740
+ opacity: 0.4,
741
+ border: "none",
742
+ padding: 0,
743
+ cursor: "pointer",
744
+ }}
745
+ />
746
+ ))}
747
+
748
+ {/* Frontier-crossing dots, foregrounded. */}
749
+ {events.map((e, i) => (
750
+ <button
751
+ key={`e-${i}`}
752
+ type="button"
753
+ aria-label={`Frontier: ${e.name || "Model"} · ${formatMonthYear(e.time)} · ${formatValue(e.score, unit)}`}
754
+ onMouseEnter={(event) => {
755
+ const rect = event.currentTarget.parentElement!.getBoundingClientRect()
756
+ const dot = event.currentTarget.getBoundingClientRect()
757
+ setHover({
758
+ x: dot.left + dot.width / 2 - rect.left,
759
+ y: dot.top + dot.height / 2 - rect.top,
760
+ name: e.name || "Model",
761
+ when: formatMonthYear(e.time),
762
+ score: formatValue(e.score, unit),
763
+ onFrontier: true,
764
+ })
765
+ }}
766
+ onFocus={(event) => {
767
+ const rect = event.currentTarget.parentElement!.getBoundingClientRect()
768
+ const dot = event.currentTarget.getBoundingClientRect()
769
+ setHover({
770
+ x: dot.left + dot.width / 2 - rect.left,
771
+ y: dot.top + dot.height / 2 - rect.top,
772
+ name: e.name || "Model",
773
+ when: formatMonthYear(e.time),
774
+ score: formatValue(e.score, unit),
775
+ onFrontier: true,
776
+ })
777
+ }}
778
+ style={{
779
+ position: "absolute",
780
+ left: `${xPct(e.time)}%`,
781
+ top: `${yPct(e.score)}%`,
782
+ transform: "translate(-50%, -50%)",
783
+ width: 11,
784
+ height: 11,
785
+ borderRadius: "50%",
786
+ background: "var(--accent)",
787
+ border: "1.5px solid var(--bg)",
788
+ padding: 0,
789
+ cursor: "pointer",
790
+ boxShadow: "0 0 0 0.5px var(--accent)",
791
+ }}
792
+ />
793
+ ))}
794
+
795
+ {/* Hover nameplate */}
796
+ {hover && (
797
+ <div
798
+ role="status"
799
+ style={{
800
+ position: "absolute",
801
+ left: hover.x,
802
+ top: hover.y - 14,
803
+ transform: "translate(-50%, -100%)",
804
+ pointerEvents: "none",
805
+ background: "var(--fg)",
806
+ color: "var(--bg)",
807
+ padding: "5px 9px",
808
+ fontSize: 11,
809
+ lineHeight: 1.3,
810
+ whiteSpace: "nowrap",
811
+ fontFamily: "var(--font-sans, inherit)",
812
+ boxShadow: "var(--shadow-card, 0 2px 6px rgba(0,0,0,0.18))",
813
+ zIndex: 2,
814
+ }}
815
+ >
816
+ <div style={{ fontWeight: 600 }}>{hover.name}</div>
817
+ <div
818
+ className="font-mono"
819
+ style={{
820
+ fontSize: 10,
821
+ letterSpacing: "0.04em",
822
+ opacity: 0.8,
823
+ marginTop: 1,
824
+ }}
825
+ >
826
+ {hover.when} · {hover.score}
827
+ {hover.onFrontier ? " · frontier" : ""}
828
+ </div>
829
+ </div>
830
+ )}
831
+
832
+ {/* Baseline */}
833
+ <div
834
+ aria-hidden
835
+ style={{
836
+ position: "absolute",
837
+ left: 0,
838
+ right: 0,
839
+ bottom: 0,
840
+ height: 1,
841
+ background: "var(--border-strong)",
842
+ }}
843
+ />
844
+ </div>
845
+
846
+ {/* Year ticks under the baseline */}
847
+ {yearTicks.map((y) => {
848
+ const t = Date.UTC(y, 0, 1)
849
+ if (t < tMin || t > tMax) return null
850
+ return (
851
+ <div
852
+ key={y}
853
+ aria-hidden
854
+ style={{
855
+ position: "absolute",
856
+ left: `${xPct(t)}%`,
857
+ bottom: 4,
858
+ transform: "translateX(-50%)",
859
+ fontFamily: "var(--font-mono)",
860
+ fontSize: 10,
861
+ color: "var(--fg-subtle)",
862
+ letterSpacing: "0.06em",
863
+ }}
864
+ >
865
+ {y}
866
+ </div>
867
+ )
868
+ })}
869
+ </div>
870
+
871
+ <div
872
+ className="mt-1 flex flex-wrap items-baseline font-mono"
873
+ style={{
874
+ fontSize: 10.5,
875
+ letterSpacing: "0.04em",
876
+ color: "var(--fg-muted)",
877
+ gap: "4px 14px",
878
+ }}
879
+ >
880
+ <span className="inline-flex items-baseline gap-1">
881
+ <span
882
+ className="uppercase"
883
+ style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
884
+ >
885
+ frontier
886
+ </span>
887
+ <span className="tabular-nums" style={{ color: "var(--fg)", fontWeight: 600 }}>
888
+ {events.length} step{events.length === 1 ? "" : "s"}
889
+ </span>
890
+ </span>
891
+ <span className="inline-flex items-baseline gap-1">
892
+ <span style={{ color: "var(--fg-subtle)" }}>·</span>
893
+ <span
894
+ className="uppercase"
895
+ style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
896
+ >
897
+ best
898
+ </span>
899
+ <span className="tabular-nums" style={{ color: "var(--fg)" }}>
900
+ {formatValue(events[events.length - 1]?.score, unit)}
901
+ </span>
902
+ <span style={{ color: "var(--fg-subtle)" }}>by</span>
903
+ <span style={{ color: "var(--fg)" }}>{events[events.length - 1]?.name || "—"}</span>
904
+ </span>
905
+ <span className="inline-flex items-baseline gap-1">
906
+ <span style={{ color: "var(--fg-subtle)" }}>·</span>
907
+ <span
908
+ className="uppercase"
909
+ style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
910
+ >
911
+ since
912
+ </span>
913
+ <span className="tabular-nums" style={{ color: "var(--fg)" }}>
914
+ {formatMonthYear(events[0].time)}
915
+ </span>
916
+ </span>
917
+ <span className="inline-flex items-baseline gap-1" style={{ color: "var(--fg-subtle)" }}>
918
+ <span>·</span>
919
+ <span style={{ fontSize: 9 }}>
920
+ {lowerIsBetter ? "frontier descends — lower is better" : "frontier ascends — higher is better"}
921
+ </span>
922
+ </span>
923
+ </div>
924
+ </div>
925
+ )
926
+ }
lib/clean-hierarchy.ts CHANGED
@@ -533,12 +533,13 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
533
  return sliceCount + metricCount
534
  }
535
 
536
- // Score every appearance of every benchmark by key.
537
- const maxRichnessByKey = new Map<string, number>()
 
538
  const visit = (b: HierarchyBenchmark) => {
539
- const r = richness(b)
540
- const cur = maxRichnessByKey.get(b.key) ?? -1
541
- if (r > cur) maxRichnessByKey.set(b.key, r)
542
  }
543
  for (const fam of h.families ?? []) {
544
  for (const b of fam.benchmarks ?? []) visit(b)
@@ -548,11 +549,28 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
548
  }
549
  }
550
 
551
- // Drop strictly-poorer copies; tie-breaker is to leave them alone so
552
- // tied wrappers (big-bench / big-bench-hard) both keep their copy.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  const isPoorerDuplicate = (b: HierarchyBenchmark): boolean => {
554
- const max = maxRichnessByKey.get(b.key) ?? 0
555
- return richness(b) < max
 
 
 
556
  }
557
 
558
  for (const fam of h.families ?? []) {
@@ -640,7 +658,26 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
640
  return false
641
  }
642
 
643
- // (a) strict-subset
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  for (const a of allFamilies) {
645
  if (dropped.has(a)) continue
646
  if (PROTECTED_LEADERBOARD_FAMILIES.has(a.key)) continue
@@ -648,6 +685,7 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
648
  if (aBenches.length === 0) continue
649
  for (const b of allFamilies) {
650
  if (a === b || dropped.has(b)) continue
 
651
  const bBenches = benchesByFam.get(b) ?? []
652
  if (bBenches.length <= aBenches.length) continue
653
  const bByKey = new Map(bBenches.map((h) => [h.bench.key, h.bench]))
 
533
  return sliceCount + metricCount
534
  }
535
 
536
+ // Collect every appearance of every benchmark, indexed by key, so the
537
+ // duplicate check can compare each instance against its peers.
538
+ const instancesByKey = new Map<string, HierarchyBenchmark[]>()
539
  const visit = (b: HierarchyBenchmark) => {
540
+ const list = instancesByKey.get(b.key)
541
+ if (list) list.push(b)
542
+ else instancesByKey.set(b.key, [b])
543
  }
544
  for (const fam of h.families ?? []) {
545
  for (const b of fam.benchmarks ?? []) visit(b)
 
549
  }
550
  }
551
 
552
+ const sharesAnyEvalId = (a: HierarchyBenchmark, b: HierarchyBenchmark) => {
553
+ const aIds = new Set(a.summary_eval_ids ?? [])
554
+ if (aIds.size === 0) return false
555
+ for (const id of b.summary_eval_ids ?? []) if (aIds.has(id)) return true
556
+ return false
557
+ }
558
+
559
+ // Drop strictly-poorer copies, BUT only when the poor copy shares an
560
+ // eval_summary_id with a richer instance. Without that gate, a curated
561
+ // family (e.g. `gaia` reporting GAIA at richness 2 with eval_id
562
+ // `gaia%2Fgaia`) gets nuked just because some unrelated source family
563
+ // (`hal` reporting GAIA at richness 8 with eval_id `hal%2Fgaia`)
564
+ // happens to use the same bench key — they're different physical
565
+ // rows and both deserve to surface. With the gate, livebench's
566
+ // structurally-poorer rows still lose to live-bench since they share
567
+ // eval_ids (`live-bench%2Flivebench-coding` lives in both).
568
  const isPoorerDuplicate = (b: HierarchyBenchmark): boolean => {
569
+ const peers = instancesByKey.get(b.key) ?? []
570
+ const r = richness(b)
571
+ return peers.some(
572
+ (peer) => peer !== b && richness(peer) > r && sharesAnyEvalId(peer, b),
573
+ )
574
  }
575
 
576
  for (const fam of h.families ?? []) {
 
658
  return false
659
  }
660
 
661
+ // (a) strict-subset.
662
+ //
663
+ // Only fires when A's family key is textually related to B's — they
664
+ // slugify the same, or one's slug contains the other. Without that
665
+ // gate the rule was eating curated benchmark families like
666
+ // `tau2-bench` whenever its rows happened to all be republished by a
667
+ // single source family (`exgentic-open-agent`). Those aren't redundant
668
+ // wrappers — they're separate curated leaderboards that the upstream
669
+ // pipeline intentionally surfaces. The intended target is wrappers
670
+ // like `livebench` (slug `livebench`) being a strict subset of
671
+ // `live-bench` (slug `livebench`), where the slugs match exactly.
672
+ const slugForKey = (key: string) =>
673
+ key.toLowerCase().replace(/[^a-z0-9]+/g, "")
674
+ const familyKeysRelated = (aKey: string, bKey: string) => {
675
+ const aSlug = slugForKey(aKey)
676
+ const bSlug = slugForKey(bKey)
677
+ if (!aSlug || !bSlug) return false
678
+ return aSlug === bSlug || aSlug.includes(bSlug) || bSlug.includes(aSlug)
679
+ }
680
+
681
  for (const a of allFamilies) {
682
  if (dropped.has(a)) continue
683
  if (PROTECTED_LEADERBOARD_FAMILIES.has(a.key)) continue
 
685
  if (aBenches.length === 0) continue
686
  for (const b of allFamilies) {
687
  if (a === b || dropped.has(b)) continue
688
+ if (!familyKeysRelated(a.key, b.key)) continue
689
  const bBenches = benchesByFam.get(b) ?? []
690
  if (bBenches.length <= aBenches.length) continue
691
  const bByKey = new Map(bBenches.map((h) => [h.bench.key, h.bench]))