evijit HF Staff commited on
Commit
f073e7a
·
1 Parent(s): 8717cca

Group model/eval-detail benchmarks by hierarchy.json families

Browse files

The eval row's `family_id` is null for ~7% of evals (e.g. CySE2
composites) and points at the leaf for ~70% (singleton families),
so the model detail and eval detail pages flattened the curated
family→composite groupings shipped in hierarchy.json. The
`evalHierarchy` prop on `BenchmarkDetail` was destructured but
never read.

Add `lib/hierarchy-lookup.ts` to map each `eval_summary_id` to
its family/composite from hierarchy.json, and consume it from the
four buckets that drive the visible UI: `groupByComposite`,
`listFamiliesByCategory`, and `plotboxUnits` in benchmark-detail,
plus the eval-detail header / sub-benchmark grid in evals/[id].
Falls back to `evaluation.family_id` when the hierarchy has no
entry. The 31 multi-family eval IDs disambiguate via the eval
row's own `family_id` when it's set.

app/evals/[id]/page.tsx CHANGED
@@ -9,9 +9,14 @@ import { EvalDetail } from "@/components/eval-detail"
9
  import { ParamRangePicker } from "@/components/param-range-picker"
10
  import { useAudienceMode } from "@/components/audience-mode-provider"
11
  import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
12
- import { fetchEvalSummary } from "@/lib/dashboard-data-client"
13
  import { humanizeEvaluationId } from "@/lib/utils"
14
  import { PARAM_RANGE_MAX_INDEX, parseParamsBillionsFromModelName, paramStepToNumeric } from "@/lib/param-range"
 
 
 
 
 
15
 
16
  export default function EvalDetailPage() {
17
  const params = useParams()
@@ -20,6 +25,7 @@ export default function EvalDetailPage() {
20
  const searchParams = useSearchParams()
21
  const [summary, setSummary] = useState<BenchmarkEvalSummary | null>(null)
22
  const [subSummaries, setSubSummaries] = useState<BenchmarkEvalSummary[]>([])
 
23
  const [loading, setLoading] = useState(true)
24
  const [error, setError] = useState<string | null>(null)
25
  const [matrixSearch, setMatrixSearch] = useState("")
@@ -49,8 +55,15 @@ export default function EvalDetailPage() {
49
  const load = async () => {
50
  try {
51
  const evalId = decodeURIComponent(params.id as string)
52
- const found = await fetchEvalSummary(evalId)
 
 
 
 
 
 
53
  setSummary(found)
 
54
  document.title = `${found.evaluation_name} | Benchmark`
55
 
56
  if (found.is_aggregated && found.aggregate_sources?.length) {
@@ -75,6 +88,28 @@ export default function EvalDetailPage() {
75
  load()
76
  }, [params.id])
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  if (loading) {
79
  return (
80
  <div className="min-h-screen bg-background">
@@ -126,9 +161,11 @@ export default function EvalDetailPage() {
126
  matrixSearch={matrixSearch}
127
  onMatrixSearchChange={setMatrixSearch}
128
  currentDetailHref={currentDetailHref}
 
 
129
  />
130
  ) : (
131
- <EvalDetail summary={summary} />
132
  )}
133
  </main>
134
  </div>
@@ -150,12 +187,16 @@ function CompositeEvalView({
150
  matrixSearch,
151
  onMatrixSearchChange,
152
  currentDetailHref,
 
 
153
  }: {
154
  summary: BenchmarkEvalSummary
155
  subSummaries: BenchmarkEvalSummary[]
156
  matrixSearch: string
157
  onMatrixSearchChange: (v: string) => void
158
  currentDetailHref: string
 
 
159
  }) {
160
  const { mode } = useAudienceMode()
161
  const isPolicy = mode === "policy"
@@ -169,6 +210,12 @@ function CompositeEvalView({
169
  const limitations = card?.purpose_and_intended_users?.limitations?.trim()
170
  const audience = card?.purpose_and_intended_users?.audience
171
  const audienceText = Array.isArray(audience) ? audience.join("; ") : audience
 
 
 
 
 
 
172
  const lede = isPolicy
173
  ? overview || goal || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
174
  : goal || overview || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
@@ -182,9 +229,9 @@ function CompositeEvalView({
182
  className="mb-5 flex flex-wrap items-center gap-3 font-mono text-[11px] uppercase tracking-[0.12em]"
183
  style={{ color: "var(--fg-muted)" }}
184
  >
185
- {summary.composite_benchmark_name && summary.composite_benchmark_name !== summary.evaluation_name && (
186
  <>
187
- <span>{summary.composite_benchmark_name}</span>
188
  <span style={{ color: "var(--fg-subtle)" }}>·</span>
189
  </>
190
  )}
@@ -291,6 +338,7 @@ function CompositeEvalView({
291
  sources={sources}
292
  subSummaries={subSummaries}
293
  currentDetailHref={currentDetailHref}
 
294
  />
295
  ) : (
296
  <MatrixLeaderboard
@@ -313,16 +361,47 @@ function SubBenchmarkGrid({
313
  sources,
314
  subSummaries,
315
  currentDetailHref,
 
316
  }: {
317
  sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>
318
  subSummaries: BenchmarkEvalSummary[]
319
  currentDetailHref: string
 
320
  }) {
321
  const subMap = useMemo(
322
  () => new Map(subSummaries.map((s) => [s.evaluation_id, s])),
323
  [subSummaries]
324
  )
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  if (sources.length === 0) {
327
  return (
328
  <div className="ec-card" style={{ padding: 32, textAlign: "center" }}>
@@ -331,67 +410,89 @@ function SubBenchmarkGrid({
331
  )
332
  }
333
 
334
- return (
335
- <div className="fam-grid">
336
- {sources.map((source) => {
337
- const sub = subMap.get(source.evaluation_id)
338
- const card = sub?.benchmark_card
339
- const overview = card?.benchmark_details?.overview ?? sub?.metric_config?.evaluation_description
340
- const goal = card?.purpose_and_intended_users?.goal
341
- const summaryLine = goal || overview
342
-
343
- return (
344
- <Link
345
- key={source.evaluation_id}
346
- href={`/evals/${source.evaluation_id}?from=${encodeURIComponent(currentDetailHref)}`}
347
- className="fam-card group block"
348
- style={{ textDecoration: "none", color: "inherit" }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  >
350
- <div className="flex items-start justify-between gap-2 mb-1">
351
- <div className="fam-card-kind">Component benchmark</div>
352
- <div className="fam-card-counts">
353
- {source.models_count} model{source.models_count === 1 ? "" : "s"}
354
- </div>
355
- </div>
356
- <h3 className="fam-card-name group-hover:text-[color:var(--accent)] transition-colors">
357
- {card?.benchmark_details?.name ?? source.composite_benchmark_name}
358
- </h3>
359
- <div className="fam-card-org">{humanizeEvaluationId(source.evaluation_id)}</div>
360
- {summaryLine && (
361
- <p className="fam-card-summary line-clamp-3">{summaryLine}</p>
362
- )}
363
- {sub?.best_model && (
364
- <div
365
- className="mt-3 pt-3 text-[12px]"
366
- style={{
367
- borderTop: "1px dashed var(--border-soft)",
368
- color: "var(--fg-muted)",
369
- }}
370
- >
371
- <span
372
- className="font-mono uppercase tracking-[0.12em] mr-2"
373
- style={{ fontSize: 9.5, color: "var(--fg-subtle)" }}
374
- >
375
- Top
376
- </span>
377
- <span style={{ color: "var(--fg)", fontWeight: 600 }}>
378
- {sub.best_model.name}
379
- </span>
380
- <span className="ml-1 font-mono tabular-nums" style={{ color: "var(--fg-muted)" }}>
381
- {(sub.best_model.score * 100).toFixed(1)}%
382
- </span>
383
- </div>
384
- )}
385
- <div
386
- className="mt-3 inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-[0.12em]"
387
- style={{ color: "var(--accent)" }}
388
  >
389
- Open
390
- <ArrowUpRight className="h-3 w-3" />
391
- </div>
392
- </Link>
393
- )
394
- })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  </div>
396
  )
397
  }
 
9
  import { ParamRangePicker } from "@/components/param-range-picker"
10
  import { useAudienceMode } from "@/components/audience-mode-provider"
11
  import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
12
+ import { fetchEvalHierarchy, fetchEvalSummary } from "@/lib/dashboard-data-client"
13
  import { humanizeEvaluationId } from "@/lib/utils"
14
  import { PARAM_RANGE_MAX_INDEX, parseParamsBillionsFromModelName, paramStepToNumeric } from "@/lib/param-range"
15
+ import type { EvalHierarchy } from "@/lib/backend-artifacts"
16
+ import {
17
+ buildHierarchyEvalIndex,
18
+ type HierarchyEvalLocation,
19
+ } from "@/lib/hierarchy-lookup"
20
 
21
  export default function EvalDetailPage() {
22
  const params = useParams()
 
25
  const searchParams = useSearchParams()
26
  const [summary, setSummary] = useState<BenchmarkEvalSummary | null>(null)
27
  const [subSummaries, setSubSummaries] = useState<BenchmarkEvalSummary[]>([])
28
+ const [hierarchy, setHierarchy] = useState<EvalHierarchy | null>(null)
29
  const [loading, setLoading] = useState(true)
30
  const [error, setError] = useState<string | null>(null)
31
  const [matrixSearch, setMatrixSearch] = useState("")
 
55
  const load = async () => {
56
  try {
57
  const evalId = decodeURIComponent(params.id as string)
58
+ const [found, evalHierarchy] = await Promise.all([
59
+ fetchEvalSummary(evalId),
60
+ fetchEvalHierarchy().catch((err) => {
61
+ console.warn("Failed to load eval-hierarchy:", err)
62
+ return null as EvalHierarchy | null
63
+ }),
64
+ ])
65
  setSummary(found)
66
+ setHierarchy(evalHierarchy)
67
  document.title = `${found.evaluation_name} | Benchmark`
68
 
69
  if (found.is_aggregated && found.aggregate_sources?.length) {
 
88
  load()
89
  }, [params.id])
90
 
91
+ const hierarchyIndex = useMemo(() => {
92
+ if (!hierarchy) return null
93
+ const familyIdHints = new Map<string, string>()
94
+ if (summary?.evaluation_id && summary.family_id) {
95
+ familyIdHints.set(summary.evaluation_id, summary.family_id)
96
+ }
97
+ for (const sub of subSummaries) {
98
+ if (sub.evaluation_id && sub.family_id) {
99
+ familyIdHints.set(sub.evaluation_id, sub.family_id)
100
+ }
101
+ }
102
+ return buildHierarchyEvalIndex(
103
+ hierarchy,
104
+ (evalSummaryId) => familyIdHints.get(evalSummaryId) ?? null,
105
+ )
106
+ }, [hierarchy, summary, subSummaries])
107
+
108
+ const hierarchyLocation = useMemo<HierarchyEvalLocation | null>(() => {
109
+ if (!hierarchyIndex || !summary?.evaluation_id) return null
110
+ return hierarchyIndex.get(summary.evaluation_id) ?? null
111
+ }, [hierarchyIndex, summary])
112
+
113
  if (loading) {
114
  return (
115
  <div className="min-h-screen bg-background">
 
161
  matrixSearch={matrixSearch}
162
  onMatrixSearchChange={setMatrixSearch}
163
  currentDetailHref={currentDetailHref}
164
+ hierarchyIndex={hierarchyIndex}
165
+ hierarchyLocation={hierarchyLocation}
166
  />
167
  ) : (
168
+ <EvalDetail summary={summary} hierarchyLocation={hierarchyLocation} />
169
  )}
170
  </main>
171
  </div>
 
187
  matrixSearch,
188
  onMatrixSearchChange,
189
  currentDetailHref,
190
+ hierarchyIndex,
191
+ hierarchyLocation,
192
  }: {
193
  summary: BenchmarkEvalSummary
194
  subSummaries: BenchmarkEvalSummary[]
195
  matrixSearch: string
196
  onMatrixSearchChange: (v: string) => void
197
  currentDetailHref: string
198
+ hierarchyIndex: Map<string, HierarchyEvalLocation> | null
199
+ hierarchyLocation: HierarchyEvalLocation | null
200
  }) {
201
  const { mode } = useAudienceMode()
202
  const isPolicy = mode === "policy"
 
210
  const limitations = card?.purpose_and_intended_users?.limitations?.trim()
211
  const audience = card?.purpose_and_intended_users?.audience
212
  const audienceText = Array.isArray(audience) ? audience.join("; ") : audience
213
+ const familyHeader =
214
+ hierarchyLocation?.familyDisplayName && hierarchyLocation.familyDisplayName !== summary.evaluation_name
215
+ ? hierarchyLocation.familyDisplayName
216
+ : summary.composite_benchmark_name && summary.composite_benchmark_name !== summary.evaluation_name
217
+ ? summary.composite_benchmark_name
218
+ : null
219
  const lede = isPolicy
220
  ? overview || goal || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
221
  : goal || overview || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
 
229
  className="mb-5 flex flex-wrap items-center gap-3 font-mono text-[11px] uppercase tracking-[0.12em]"
230
  style={{ color: "var(--fg-muted)" }}
231
  >
232
+ {familyHeader && (
233
  <>
234
+ <span>{familyHeader}</span>
235
  <span style={{ color: "var(--fg-subtle)" }}>·</span>
236
  </>
237
  )}
 
338
  sources={sources}
339
  subSummaries={subSummaries}
340
  currentDetailHref={currentDetailHref}
341
+ hierarchyIndex={hierarchyIndex}
342
  />
343
  ) : (
344
  <MatrixLeaderboard
 
361
  sources,
362
  subSummaries,
363
  currentDetailHref,
364
+ hierarchyIndex,
365
  }: {
366
  sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>
367
  subSummaries: BenchmarkEvalSummary[]
368
  currentDetailHref: string
369
+ hierarchyIndex: Map<string, HierarchyEvalLocation> | null
370
  }) {
371
  const subMap = useMemo(
372
  () => new Map(subSummaries.map((s) => [s.evaluation_id, s])),
373
  [subSummaries]
374
  )
375
 
376
+ // Group sources by hierarchy family. When the hierarchy doesn't resolve a
377
+ // family for a source (or no hierarchy was loaded), bucket those entries
378
+ // under a single "Other" section instead of spamming per-eval headers.
379
+ type FamilyBucket = {
380
+ key: string
381
+ displayName: string | null
382
+ sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>
383
+ }
384
+ const buckets = useMemo<FamilyBucket[]>(() => {
385
+ if (!hierarchyIndex) {
386
+ return [{ key: "__all__", displayName: null, sources }]
387
+ }
388
+ const ordered: FamilyBucket[] = []
389
+ const byKey = new Map<string, FamilyBucket>()
390
+ for (const source of sources) {
391
+ const location = hierarchyIndex.get(source.evaluation_id) ?? null
392
+ const key = location?.familyKey ?? "__unmapped__"
393
+ const displayName = location?.familyDisplayName ?? null
394
+ let bucket = byKey.get(key)
395
+ if (!bucket) {
396
+ bucket = { key, displayName, sources: [] }
397
+ byKey.set(key, bucket)
398
+ ordered.push(bucket)
399
+ }
400
+ bucket.sources.push(source)
401
+ }
402
+ return ordered
403
+ }, [sources, hierarchyIndex])
404
+
405
  if (sources.length === 0) {
406
  return (
407
  <div className="ec-card" style={{ padding: 32, textAlign: "center" }}>
 
410
  )
411
  }
412
 
413
+ const renderCard = (source: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>[number]) => {
414
+ const sub = subMap.get(source.evaluation_id)
415
+ const card = sub?.benchmark_card
416
+ const overview = card?.benchmark_details?.overview ?? sub?.metric_config?.evaluation_description
417
+ const goal = card?.purpose_and_intended_users?.goal
418
+ const summaryLine = goal || overview
419
+
420
+ return (
421
+ <Link
422
+ key={source.evaluation_id}
423
+ href={`/evals/${source.evaluation_id}?from=${encodeURIComponent(currentDetailHref)}`}
424
+ className="fam-card group block"
425
+ style={{ textDecoration: "none", color: "inherit" }}
426
+ >
427
+ <div className="flex items-start justify-between gap-2 mb-1">
428
+ <div className="fam-card-kind">Component benchmark</div>
429
+ <div className="fam-card-counts">
430
+ {source.models_count} model{source.models_count === 1 ? "" : "s"}
431
+ </div>
432
+ </div>
433
+ <h3 className="fam-card-name group-hover:text-[color:var(--accent)] transition-colors">
434
+ {card?.benchmark_details?.name ?? source.composite_benchmark_name}
435
+ </h3>
436
+ <div className="fam-card-org">{humanizeEvaluationId(source.evaluation_id)}</div>
437
+ {summaryLine && (
438
+ <p className="fam-card-summary line-clamp-3">{summaryLine}</p>
439
+ )}
440
+ {sub?.best_model && (
441
+ <div
442
+ className="mt-3 pt-3 text-[12px]"
443
+ style={{
444
+ borderTop: "1px dashed var(--border-soft)",
445
+ color: "var(--fg-muted)",
446
+ }}
447
  >
448
+ <span
449
+ className="font-mono uppercase tracking-[0.12em] mr-2"
450
+ style={{ fontSize: 9.5, color: "var(--fg-subtle)" }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  >
452
+ Top
453
+ </span>
454
+ <span style={{ color: "var(--fg)", fontWeight: 600 }}>
455
+ {sub.best_model.name}
456
+ </span>
457
+ <span className="ml-1 font-mono tabular-nums" style={{ color: "var(--fg-muted)" }}>
458
+ {(sub.best_model.score * 100).toFixed(1)}%
459
+ </span>
460
+ </div>
461
+ )}
462
+ <div
463
+ className="mt-3 inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-[0.12em]"
464
+ style={{ color: "var(--accent)" }}
465
+ >
466
+ Open
467
+ <ArrowUpRight className="h-3 w-3" />
468
+ </div>
469
+ </Link>
470
+ )
471
+ }
472
+
473
+ // Render flat when only a single bucket — the per-family headers add no
474
+ // signal in that case (which is the typical "all components belong to one
475
+ // family" composite).
476
+ if (buckets.length <= 1) {
477
+ return <div className="fam-grid">{sources.map(renderCard)}</div>
478
+ }
479
+
480
+ return (
481
+ <div className="space-y-8">
482
+ {buckets.map((bucket) => (
483
+ <section key={bucket.key}>
484
+ <div
485
+ className="kicker mb-3"
486
+ style={{ display: "flex", alignItems: "baseline", gap: 8 }}
487
+ >
488
+ <span>{bucket.displayName ?? "Other"}</span>
489
+ <span style={{ color: "var(--fg-subtle)", fontWeight: 400 }}>
490
+ · {bucket.sources.length} component{bucket.sources.length === 1 ? "" : "s"}
491
+ </span>
492
+ </div>
493
+ <div className="fam-grid">{bucket.sources.map(renderCard)}</div>
494
+ </section>
495
+ ))}
496
  </div>
497
  )
498
  }
components/benchmark-detail.tsx CHANGED
@@ -49,6 +49,10 @@ import type {
49
  EvalHierarchy,
50
  SubmissionAxis,
51
  } from "@/lib/backend-artifacts"
 
 
 
 
52
  import { type CSSProperties, Fragment, useState, useEffect, useMemo } from "react"
53
 
54
  interface BenchmarkDetailProps {
@@ -268,7 +272,38 @@ function doesLabelMatchSuiteKey(label: string | null | undefined, compositeKey:
268
  return normalizeCompositeKey(normalizeDisplayKey(label)) === normalizeCompositeKey(compositeKey)
269
  }
270
 
271
- function getCompositeKey(group: BenchmarkGroup): string {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  const evaluation = group.variants[0]?.evaluation
273
  const backendSuiteKey = evaluation?.family_id
274
 
@@ -283,7 +318,16 @@ function getCompositeDisplayName(key: string): string {
283
  return normalizeDisplayLabel(key)
284
  }
285
 
286
- function getCompositeName(group: BenchmarkGroup, compositeKey: string): string {
 
 
 
 
 
 
 
 
 
287
  const evaluation = group.variants[0]?.evaluation
288
  const benchmarkCardName = group.benchmarkCard?.benchmark_details?.name
289
  const backendParentName = evaluation?.benchmark_parent_name
@@ -307,11 +351,12 @@ function getCompositeName(group: BenchmarkGroup, compositeKey: string): string {
307
  function groupByComposite(
308
  groups: BenchmarkGroup[],
309
  modelIds: string[],
310
- peerRanks: PeerRanksMap
 
311
  ): CompositeGroup[] {
312
  const composites = new Map<string, BenchmarkGroup[]>()
313
  for (const group of groups) {
314
- const key = getCompositeKey(group)
315
  const existing = composites.get(key) ?? []
316
  existing.push(group)
317
  composites.set(key, existing)
@@ -335,7 +380,7 @@ function groupByComposite(
335
 
336
  return {
337
  compositeKey,
338
- compositeName: benchmarks[0] ? getCompositeName(benchmarks[0], compositeKey) : getCompositeDisplayName(compositeKey),
339
  benchmarks,
340
  avgRawScore,
341
  avgNormalizedScore: avgScore,
@@ -1663,6 +1708,28 @@ export function BenchmarkDetail({
1663
  loadPeerRanks().then(setPeerRanks)
1664
  }, [])
1665
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1666
  // Composite relevance score for benchmark ordering
1667
  // relevance = population × 0.4 + rank_extremity × 0.3 + has_metadata × 0.2 + recency × 0.1
1668
  const getRelevanceScore = useMemo(() => {
@@ -1985,8 +2052,13 @@ export function BenchmarkDetail({
1985
  ?.evaluation.eval_summary_id
1986
  const evalEntry =
1987
  evalId && comparisonIndex ? comparisonIndex.evals[evalId] : null
1988
- const famKey = evalEntry?.family_id ?? group.key
 
 
 
 
1989
  const famName =
 
1990
  evalEntry?.family_display_name ||
1991
  evalEntry?.display_name ||
1992
  group.title
@@ -2014,31 +2086,31 @@ export function BenchmarkDetail({
2014
  kind: f.groups.length > 1 ? "multi-eval" as const : "single-eval" as const,
2015
  })),
2016
  }))
2017
- }, [filteredBenchmarkGroups, comparisonIndex, summary.categories_covered])
2018
 
2019
  const compositeGroups = useMemo(() => {
2020
- const groups = groupByComposite(filteredBenchmarkGroups, modelIds, peerRanks)
2021
  // Re-sort composites by max relevance of their benchmarks
2022
  return groups.sort((a, b) => {
2023
  const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
2024
  const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
2025
  return bMax - aMax
2026
  })
2027
- }, [filteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore])
2028
 
2029
  const categoryCompositeSections = useMemo(
2030
  () =>
2031
  groupedFilteredBenchmarkGroups
2032
  .map(({ category, groups }) => ({
2033
  category,
2034
- composites: groupByComposite(groups, modelIds, peerRanks).sort((a, b) => {
2035
  const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
2036
  const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
2037
  return bMax - aMax
2038
  }),
2039
  }))
2040
  .filter((section) => section.composites.length > 0),
2041
- [groupedFilteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore]
2042
  )
2043
 
2044
  const categoryScoreRanges = useMemo(() => {
@@ -2456,9 +2528,17 @@ export function BenchmarkDetail({
2456
  const evalEntry = comparisonIndex.evals[evalId]
2457
  if (!evalEntry) continue
2458
 
2459
- const famKey = evalEntry.family_id ?? evalId
 
 
 
 
 
2460
  const famName =
2461
- evalEntry.family_display_name || evalEntry.display_name || famKey
 
 
 
2462
  const bucket = familyBuckets.get(famKey) ?? {
2463
  familyName: famName,
2464
  category: group.category,
@@ -2673,7 +2753,7 @@ export function BenchmarkDetail({
2673
  }
2674
 
2675
  return units
2676
- }, [comparisonIndex, filteredBenchmarkGroups])
2677
 
2678
  const [activeViewByUnit, setActiveViewByUnit] = useState<Record<string, string>>({})
2679
  const [activeMetricByUnit, setActiveMetricByUnit] = useState<Record<string, string>>({})
 
49
  EvalHierarchy,
50
  SubmissionAxis,
51
  } from "@/lib/backend-artifacts"
52
+ import {
53
+ buildHierarchyEvalIndex,
54
+ type HierarchyEvalLocation,
55
+ } from "@/lib/hierarchy-lookup"
56
  import { type CSSProperties, Fragment, useState, useEffect, useMemo } from "react"
57
 
58
  interface BenchmarkDetailProps {
 
272
  return normalizeCompositeKey(normalizeDisplayKey(label)) === normalizeCompositeKey(compositeKey)
273
  }
274
 
275
+ function getHierarchyLocation(
276
+ group: BenchmarkGroup,
277
+ hierarchyIndex: Map<string, HierarchyEvalLocation> | null,
278
+ ): HierarchyEvalLocation | undefined {
279
+ if (!hierarchyIndex) {
280
+ return undefined
281
+ }
282
+ for (const variant of group.variants) {
283
+ const evalSummaryId = variant.evaluation.eval_summary_id
284
+ if (evalSummaryId) {
285
+ const location = hierarchyIndex.get(evalSummaryId)
286
+ if (location) {
287
+ return location
288
+ }
289
+ }
290
+ }
291
+ return undefined
292
+ }
293
+
294
+ function getCompositeKey(
295
+ group: BenchmarkGroup,
296
+ hierarchyIndex: Map<string, HierarchyEvalLocation> | null,
297
+ ): string {
298
+ // Prefer the curated grouping from hierarchy.json. The eval row's own
299
+ // family_id is null for ~7% of evals (e.g. CySE2 composites) and points
300
+ // at the leaf for singleton families, so the hierarchy is the only
301
+ // source that captures family→composite groupings authoritatively.
302
+ const location = getHierarchyLocation(group, hierarchyIndex)
303
+ if (location) {
304
+ return normalizeCompositeKey(location.familyKey)
305
+ }
306
+
307
  const evaluation = group.variants[0]?.evaluation
308
  const backendSuiteKey = evaluation?.family_id
309
 
 
318
  return normalizeDisplayLabel(key)
319
  }
320
 
321
+ function getCompositeName(
322
+ group: BenchmarkGroup,
323
+ compositeKey: string,
324
+ hierarchyIndex: Map<string, HierarchyEvalLocation> | null,
325
+ ): string {
326
+ const location = getHierarchyLocation(group, hierarchyIndex)
327
+ if (location?.familyDisplayName) {
328
+ return location.familyDisplayName
329
+ }
330
+
331
  const evaluation = group.variants[0]?.evaluation
332
  const benchmarkCardName = group.benchmarkCard?.benchmark_details?.name
333
  const backendParentName = evaluation?.benchmark_parent_name
 
351
  function groupByComposite(
352
  groups: BenchmarkGroup[],
353
  modelIds: string[],
354
+ peerRanks: PeerRanksMap,
355
+ hierarchyIndex: Map<string, HierarchyEvalLocation> | null
356
  ): CompositeGroup[] {
357
  const composites = new Map<string, BenchmarkGroup[]>()
358
  for (const group of groups) {
359
+ const key = getCompositeKey(group, hierarchyIndex)
360
  const existing = composites.get(key) ?? []
361
  existing.push(group)
362
  composites.set(key, existing)
 
380
 
381
  return {
382
  compositeKey,
383
+ compositeName: benchmarks[0] ? getCompositeName(benchmarks[0], compositeKey, hierarchyIndex) : getCompositeDisplayName(compositeKey),
384
  benchmarks,
385
  avgRawScore,
386
  avgNormalizedScore: avgScore,
 
1708
  loadPeerRanks().then(setPeerRanks)
1709
  }, [])
1710
 
1711
+ // Build an eval_summary_id → family/composite lookup from hierarchy.json.
1712
+ // ~31 eval_summary_ids appear in multiple families (e.g. mmlu-pro under
1713
+ // both `mmlu` and `artificial-analysis`); use the eval row's own family_id
1714
+ // as the disambiguating preference when present.
1715
+ const hierarchyIndex = useMemo(() => {
1716
+ if (!evalHierarchy) {
1717
+ return null
1718
+ }
1719
+ const familyIdByEvalSummaryId = new Map<string, string>()
1720
+ for (const evals of Object.values(summary.evaluations_by_category)) {
1721
+ for (const evaluation of evals) {
1722
+ if (evaluation.eval_summary_id && evaluation.family_id) {
1723
+ familyIdByEvalSummaryId.set(evaluation.eval_summary_id, evaluation.family_id)
1724
+ }
1725
+ }
1726
+ }
1727
+ return buildHierarchyEvalIndex(
1728
+ evalHierarchy,
1729
+ (evalSummaryId) => familyIdByEvalSummaryId.get(evalSummaryId) ?? null,
1730
+ )
1731
+ }, [evalHierarchy, summary.evaluations_by_category])
1732
+
1733
  // Composite relevance score for benchmark ordering
1734
  // relevance = population × 0.4 + rank_extremity × 0.3 + has_metadata × 0.2 + recency × 0.1
1735
  const getRelevanceScore = useMemo(() => {
 
2052
  ?.evaluation.eval_summary_id
2053
  const evalEntry =
2054
  evalId && comparisonIndex ? comparisonIndex.evals[evalId] : null
2055
+ const hierarchyLocation = evalId
2056
+ ? hierarchyIndex?.get(evalId) ?? null
2057
+ : null
2058
+ const famKey =
2059
+ hierarchyLocation?.familyKey ?? evalEntry?.family_id ?? group.key
2060
  const famName =
2061
+ hierarchyLocation?.familyDisplayName ||
2062
  evalEntry?.family_display_name ||
2063
  evalEntry?.display_name ||
2064
  group.title
 
2086
  kind: f.groups.length > 1 ? "multi-eval" as const : "single-eval" as const,
2087
  })),
2088
  }))
2089
+ }, [filteredBenchmarkGroups, comparisonIndex, hierarchyIndex, summary.categories_covered])
2090
 
2091
  const compositeGroups = useMemo(() => {
2092
+ const groups = groupByComposite(filteredBenchmarkGroups, modelIds, peerRanks, hierarchyIndex)
2093
  // Re-sort composites by max relevance of their benchmarks
2094
  return groups.sort((a, b) => {
2095
  const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
2096
  const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
2097
  return bMax - aMax
2098
  })
2099
+ }, [filteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore, hierarchyIndex])
2100
 
2101
  const categoryCompositeSections = useMemo(
2102
  () =>
2103
  groupedFilteredBenchmarkGroups
2104
  .map(({ category, groups }) => ({
2105
  category,
2106
+ composites: groupByComposite(groups, modelIds, peerRanks, hierarchyIndex).sort((a, b) => {
2107
  const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
2108
  const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
2109
  return bMax - aMax
2110
  }),
2111
  }))
2112
  .filter((section) => section.composites.length > 0),
2113
+ [groupedFilteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore, hierarchyIndex]
2114
  )
2115
 
2116
  const categoryScoreRanges = useMemo(() => {
 
2528
  const evalEntry = comparisonIndex.evals[evalId]
2529
  if (!evalEntry) continue
2530
 
2531
+ // Prefer hierarchy.json grouping. The comparison-index family_id is
2532
+ // null for ~7% of evals (e.g. CySE2 composites) and points at the
2533
+ // leaf for singleton families, so the hierarchy is the only source
2534
+ // that captures family→composite groupings authoritatively.
2535
+ const hierarchyLocation = hierarchyIndex?.get(evalId) ?? null
2536
+ const famKey = hierarchyLocation?.familyKey ?? evalEntry.family_id ?? evalId
2537
  const famName =
2538
+ hierarchyLocation?.familyDisplayName ||
2539
+ evalEntry.family_display_name ||
2540
+ evalEntry.display_name ||
2541
+ famKey
2542
  const bucket = familyBuckets.get(famKey) ?? {
2543
  familyName: famName,
2544
  category: group.category,
 
2753
  }
2754
 
2755
  return units
2756
+ }, [comparisonIndex, filteredBenchmarkGroups, hierarchyIndex])
2757
 
2758
  const [activeViewByUnit, setActiveViewByUnit] = useState<Record<string, string>>({})
2759
  const [activeMetricByUnit, setActiveMetricByUnit] = useState<Record<string, string>>({})
components/eval-detail.tsx CHANGED
@@ -52,6 +52,7 @@ import {
52
  } from "lucide-react"
53
  import type { BenchmarkCard } from "@/lib/benchmark-schema"
54
  import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
 
55
  import { PolicyOverview } from "@/components/policy-overview"
56
  import { ResearcherReproducibilityCard } from "@/components/researcher-reproducibility-card"
57
  import { KnownIssuesPanel } from "@/components/known-issues-panel"
@@ -61,6 +62,7 @@ import { FlagScoreButton } from "@/components/flag-score-button"
61
 
62
  interface EvalDetailProps {
63
  summary: BenchmarkEvalSummary
 
64
  }
65
 
66
  interface LeaderboardRow {
@@ -478,7 +480,7 @@ function getSetupLabel(modelResult: ModelResultForBenchmark): string {
478
  return parts.join(" ")
479
  }
480
 
481
- export function EvalDetail({ summary }: EvalDetailProps) {
482
  const { mode } = useAudienceMode()
483
  const isResearchView = mode === "research"
484
  const hasMultiMetricLeaderboard =
@@ -581,9 +583,21 @@ export function EvalDetail({ summary }: EvalDetailProps) {
581
  [key]: !current[key],
582
  }))
583
 
584
- const headerOrg = summary.composite_benchmark_name && summary.composite_benchmark_name !== summary.evaluation_name
585
- ? summary.composite_benchmark_name
586
- : null
 
 
 
 
 
 
 
 
 
 
 
 
587
 
588
  const heroLede = isResearchView
589
  ? summary.metric_config.evaluation_description
 
52
  } from "lucide-react"
53
  import type { BenchmarkCard } from "@/lib/benchmark-schema"
54
  import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
55
+ import type { HierarchyEvalLocation } from "@/lib/hierarchy-lookup"
56
  import { PolicyOverview } from "@/components/policy-overview"
57
  import { ResearcherReproducibilityCard } from "@/components/researcher-reproducibility-card"
58
  import { KnownIssuesPanel } from "@/components/known-issues-panel"
 
62
 
63
  interface EvalDetailProps {
64
  summary: BenchmarkEvalSummary
65
+ hierarchyLocation?: HierarchyEvalLocation | null
66
  }
67
 
68
  interface LeaderboardRow {
 
480
  return parts.join(" ")
481
  }
482
 
483
+ export function EvalDetail({ summary, hierarchyLocation }: EvalDetailProps) {
484
  const { mode } = useAudienceMode()
485
  const isResearchView = mode === "research"
486
  const hasMultiMetricLeaderboard =
 
583
  [key]: !current[key],
584
  }))
585
 
586
+ // Prefer the curated family name from hierarchy.json — the producer's
587
+ // composite_benchmark_name often equals the eval's own slug (e.g.
588
+ // "cyse2_interpreter_abuse"), so the header repeats itself. When the
589
+ // hierarchy resolves a different parent family ("CySE2"), use that instead.
590
+ const hierarchyHeaderOrg = (() => {
591
+ const familyName = hierarchyLocation?.familyDisplayName?.trim()
592
+ if (!familyName || familyName === summary.evaluation_name) {
593
+ return null
594
+ }
595
+ return familyName
596
+ })()
597
+ const headerOrg = hierarchyHeaderOrg
598
+ ?? (summary.composite_benchmark_name && summary.composite_benchmark_name !== summary.evaluation_name
599
+ ? summary.composite_benchmark_name
600
+ : null)
601
 
602
  const heroLede = isResearchView
603
  ? summary.metric_config.evaluation_description
lib/hierarchy-lookup.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ EvalHierarchy,
3
+ HierarchyComposite,
4
+ HierarchyFamily,
5
+ } from "@/lib/backend-artifacts"
6
+
7
+ export interface HierarchyEvalLocation {
8
+ familyKey: string
9
+ familyDisplayName: string
10
+ compositeKey?: string
11
+ compositeDisplayName?: string
12
+ }
13
+
14
+ interface FamilyAppearance {
15
+ family: HierarchyFamily
16
+ composite?: HierarchyComposite
17
+ }
18
+
19
+ function findComposite(
20
+ family: HierarchyFamily,
21
+ evalSummaryId: string,
22
+ ): HierarchyComposite | undefined {
23
+ const composites = family.composites
24
+ if (!composites?.length) {
25
+ return undefined
26
+ }
27
+ const sourcePrefix = evalSummaryId.split("%2F")[0]
28
+ return composites.find((composite) => composite.key === sourcePrefix)
29
+ }
30
+
31
+ function buildAppearancesIndex(
32
+ hierarchy: EvalHierarchy | null | undefined,
33
+ ): Map<string, FamilyAppearance[]> {
34
+ const index = new Map<string, FamilyAppearance[]>()
35
+ if (!hierarchy?.families) {
36
+ return index
37
+ }
38
+
39
+ for (const family of hierarchy.families) {
40
+ for (const evalSummaryId of family.eval_summary_ids ?? []) {
41
+ const composite = findComposite(family, evalSummaryId)
42
+ const list = index.get(evalSummaryId) ?? []
43
+ list.push({ family, composite })
44
+ index.set(evalSummaryId, list)
45
+ }
46
+ }
47
+
48
+ return index
49
+ }
50
+
51
+ /**
52
+ * Build a lookup that maps each `eval_summary_id` to the family / composite
53
+ * that contains it in `hierarchy.json`. The model-detail benchmark grouping
54
+ * needs this because the eval row's own `family_id` is null for ~7% of evals
55
+ * (e.g. CySE2 composites) and points at the leaf instead of the parent for
56
+ * ~70% (singleton families). The hierarchy is the only source that captures
57
+ * curated family→composite→benchmark grouping.
58
+ *
59
+ * 31 eval_summary_ids appear in multiple families. The optional
60
+ * `preferFamilyKey(evalSummaryId)` callback lets the caller pick the canonical
61
+ * family — typically by passing in the eval row's own `family_id`. When no
62
+ * preference is given, the first family encountered wins.
63
+ */
64
+ export function buildHierarchyEvalIndex(
65
+ hierarchy: EvalHierarchy | null | undefined,
66
+ preferFamilyKey?: (evalSummaryId: string) => string | null | undefined,
67
+ ): Map<string, HierarchyEvalLocation> {
68
+ const appearances = buildAppearancesIndex(hierarchy)
69
+ const index = new Map<string, HierarchyEvalLocation>()
70
+
71
+ for (const [evalSummaryId, candidates] of appearances) {
72
+ let chosen = candidates[0]
73
+ if (candidates.length > 1) {
74
+ const preferredKey = preferFamilyKey?.(evalSummaryId)?.toString().trim()
75
+ if (preferredKey) {
76
+ const match = candidates.find((c) => c.family.key === preferredKey)
77
+ if (match) {
78
+ chosen = match
79
+ }
80
+ }
81
+ }
82
+
83
+ index.set(evalSummaryId, {
84
+ familyKey: chosen.family.key,
85
+ familyDisplayName: chosen.family.display_name,
86
+ compositeKey: chosen.composite?.key,
87
+ compositeDisplayName: chosen.composite?.display_name,
88
+ })
89
+ }
90
+
91
+ return index
92
+ }