j-chim commited on
Commit
2ed4959
·
1 Parent(s): 2f8b51d

Refactor to align on benchmark hierarchy

Browse files
app/evals/page.tsx CHANGED
@@ -1,9 +1,10 @@
1
  "use client"
2
 
3
  import { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react"
 
4
  import { ChevronDown, ChevronUp, Search, Tag } from "lucide-react"
5
 
6
- import { FamilyTable } from "@/components/family-table"
7
  import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
8
  import { Navigation } from "@/components/navigation"
9
  import type { EvalHierarchy, HierarchyFamily } from "@/lib/backend-artifacts"
@@ -18,42 +19,35 @@ type FamilySort = "results" | "benchmarks" | "name" | "category"
18
  function familyEvalsCount(fam: HierarchyFamily): number {
19
  if (fam.evals_count != null) return fam.evals_count
20
 
21
- const composites = fam.composites ?? []
22
- const standalone = fam.standalone_benchmarks ?? []
23
- const benchmarks = fam.benchmarks ?? []
24
- const leaves = fam.leaves ?? []
25
-
26
  const allBenchmarks = [
27
- ...standalone,
28
- ...benchmarks,
29
- ...composites.flatMap((c) => c.benchmarks ?? []),
30
  ]
31
-
32
- return (
33
- (fam.metrics?.length ?? 0) +
34
- allBenchmarks.reduce(
35
- (sum, b) => sum + ((b as { metrics?: unknown[] }).metrics?.length ?? 0),
36
- 0,
37
- ) +
38
- leaves.reduce((sum, l) => sum + (l.evals_count ?? 0), 0)
39
  )
40
  }
41
 
42
  function familyBenchmarkCount(fam: HierarchyFamily): number {
43
- const composites = fam.composites ?? []
44
- const standalone = fam.standalone_benchmarks ?? []
45
- const benchmarks = fam.benchmarks ?? []
46
- const leaves = fam.leaves ?? []
47
-
48
- const all = [
49
- ...standalone,
50
- ...benchmarks,
51
- ...composites.flatMap((c) => c.benchmarks ?? []),
52
- ]
53
- return all.length > 0 ? all.length : leaves.length
54
  }
55
 
56
  export default function EvalsPage() {
 
 
 
 
57
  const [hierarchy, setHierarchy] = useState<EvalHierarchy | null>(null)
58
  const [totalModels, setTotalModels] = useState<number>(0)
59
  const [evalItems, setEvalItems] = useState<Map<string, BenchmarkEvalListItem>>(new Map())
@@ -81,6 +75,23 @@ export default function EvalsPage() {
81
  .finally(() => setLoading(false))
82
  }, [])
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  const families = hierarchy?.families ?? []
85
 
86
  // Build a domain → family-count map. The lite eval list doesn't carry
@@ -116,13 +127,7 @@ export default function EvalsPage() {
116
  }
117
  }
118
 
119
- // Legacy fallback: per-leaf tags + cards keyed by leaf slug.
120
- for (const leaf of fam.leaves ?? []) {
121
- for (const d of leaf.tags?.domains ?? []) seen.add(d.trim().toLowerCase())
122
- for (const d of lookupDomains(leaf.key)) seen.add(d.trim().toLowerCase())
123
- }
124
-
125
- // Family-level eval_summary_ids cover both shapes.
126
  for (const id of fam.eval_summary_ids ?? []) {
127
  for (const d of lookupDomains(id)) seen.add(d.trim().toLowerCase())
128
  }
@@ -154,9 +159,6 @@ export default function EvalsPage() {
154
  for (const benchmark of nestedBenchmarks) {
155
  for (const d of benchmark.tags?.domains ?? []) recordLabel(d)
156
  }
157
- for (const leaf of fam.leaves ?? []) {
158
- for (const d of leaf.tags?.domains ?? []) recordLabel(d)
159
- }
160
  }
161
  for (const set of familyDomains.values()) {
162
  for (const key of set) counts.set(key, (counts.get(key) ?? 0) + 1)
@@ -280,11 +282,7 @@ export default function EvalsPage() {
280
  <div className="ec-page-meta-item">
281
  <span className="ec-page-meta-item-l">Single benchmarks</span>
282
  <span className="ec-page-meta-item-v">
283
- {(
284
- stats.benchmark_count ??
285
- (stats.single_benchmark_count ?? 0) +
286
- (stats.standalone_benchmark_count ?? 0)
287
- ).toLocaleString()}
288
  </span>
289
  </div>
290
  <div className="ec-page-meta-item">
 
1
  "use client"
2
 
3
  import { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react"
4
+ import { useRouter, useSearchParams } from "next/navigation"
5
  import { ChevronDown, ChevronUp, Search, Tag } from "lucide-react"
6
 
7
+ import { FamilyTable, getFamilyNavId } from "@/components/family-table"
8
  import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
9
  import { Navigation } from "@/components/navigation"
10
  import type { EvalHierarchy, HierarchyFamily } from "@/lib/backend-artifacts"
 
19
  function familyEvalsCount(fam: HierarchyFamily): number {
20
  if (fam.evals_count != null) return fam.evals_count
21
 
22
+ // v3 fallback: sum metric counts across the family's benchmarks (one
23
+ // of the three layout fields is present per family per spec §5.1).
 
 
 
24
  const allBenchmarks = [
25
+ ...(fam.standalone_benchmarks ?? []),
26
+ ...(fam.benchmarks ?? []),
27
+ ...((fam.composites ?? []).flatMap((c) => c.benchmarks ?? [])),
28
  ]
29
+ return allBenchmarks.reduce(
30
+ (sum, b) => sum + (b.metrics?.length ?? 0),
31
+ 0,
 
 
 
 
 
32
  )
33
  }
34
 
35
  function familyBenchmarkCount(fam: HierarchyFamily): number {
36
+ return (
37
+ (fam.standalone_benchmarks?.length ?? 0) +
38
+ (fam.benchmarks?.length ?? 0) +
39
+ ((fam.composites ?? []).reduce(
40
+ (sum, c) => sum + (c.benchmarks?.length ?? 0),
41
+ 0,
42
+ ))
43
+ )
 
 
 
44
  }
45
 
46
  export default function EvalsPage() {
47
+ const router = useRouter()
48
+ const searchParams = useSearchParams()
49
+ const familyParam = searchParams.get("family")
50
+
51
  const [hierarchy, setHierarchy] = useState<EvalHierarchy | null>(null)
52
  const [totalModels, setTotalModels] = useState<number>(0)
53
  const [evalItems, setEvalItems] = useState<Map<string, BenchmarkEvalListItem>>(new Map())
 
75
  .finally(() => setLoading(false))
76
  }, [])
77
 
78
+ // Resolve the `?family=<key>` deep link from the home page family cards.
79
+ // For families with a clean family-level summary we redirect to the
80
+ // detail page; for aggregator families (no nav target) we seed the
81
+ // search box so the listing narrows to that family and the user can
82
+ // expand it. Runs once per `family` param value, after data loads.
83
+ useEffect(() => {
84
+ if (!familyParam || !hierarchy) return
85
+ const fam = hierarchy.families.find((f) => f.key === familyParam)
86
+ if (!fam) return
87
+ const navId = getFamilyNavId(fam, benchmarkCards)
88
+ if (navId) {
89
+ router.replace(`/evals/${encodeURIComponent(navId)}`)
90
+ return
91
+ }
92
+ setSearchQuery(fam.display_name || fam.key)
93
+ }, [familyParam, hierarchy, benchmarkCards, router])
94
+
95
  const families = hierarchy?.families ?? []
96
 
97
  // Build a domain → family-count map. The lite eval list doesn't carry
 
127
  }
128
  }
129
 
130
+ // Family-level eval_summary_ids cover the v3 shape.
 
 
 
 
 
 
131
  for (const id of fam.eval_summary_ids ?? []) {
132
  for (const d of lookupDomains(id)) seen.add(d.trim().toLowerCase())
133
  }
 
159
  for (const benchmark of nestedBenchmarks) {
160
  for (const d of benchmark.tags?.domains ?? []) recordLabel(d)
161
  }
 
 
 
162
  }
163
  for (const set of familyDomains.values()) {
164
  for (const key of set) counts.set(key, (counts.get(key) ?? 0) + 1)
 
282
  <div className="ec-page-meta-item">
283
  <span className="ec-page-meta-item-l">Single benchmarks</span>
284
  <span className="ec-page-meta-item-v">
285
+ {(stats.benchmark_count ?? 0).toLocaleString()}
 
 
 
 
286
  </span>
287
  </div>
288
  <div className="ec-page-meta-item">
app/page.tsx CHANGED
@@ -53,14 +53,11 @@ export default async function HomePage() {
53
  const stats = hierarchy.stats
54
  const familyCount = stats?.family_count ?? hierarchy.families.length
55
  const compositeCount = stats?.composite_count ?? 0
56
- // Single benchmarks: prefer the new `benchmark_count` (total distinct
57
- // benchmarks across all composites in the v2 dim), fall back to the
58
- // legacy single + standalone split when the adapter synthesised them
59
- // from an older snapshot shape.
60
- const singleBenchmarkCount = stats?.single_benchmark_count ?? 0
61
- const standaloneBenchmarkCount = stats?.standalone_benchmark_count ?? 0
62
- const benchmarkLeafCount =
63
- stats?.benchmark_count ?? singleBenchmarkCount + standaloneBenchmarkCount
64
  const sliceCount = stats?.slice_count ?? 0
65
  const metricCount = stats?.metric_count ?? 0
66
  const tripleCount = stats?.metric_rows_scanned ?? 0
@@ -77,9 +74,19 @@ export default async function HomePage() {
77
  const generatedAt = formatGeneratedAt(manifest?.generated_at)
78
 
79
  // Featured family cards — pick the first six families with summaries.
 
 
 
 
 
80
  const featuredFamilies = hierarchy.families.slice(0, 6).map((family) => {
81
- const benches: unknown[] =
82
- family.benchmarks ?? family.standalone_benchmarks ?? family.leaves ?? []
 
 
 
 
 
83
  let slices = 0
84
  for (const b of benches) {
85
  const benchSlices = (b as { slices?: unknown[] }).slices
 
53
  const stats = hierarchy.stats
54
  const familyCount = stats?.family_count ?? hierarchy.families.length
55
  const compositeCount = stats?.composite_count ?? 0
56
+ // v3 hierarchy ships `benchmark_count` directly. The legacy
57
+ // `single_benchmark_count` / `standalone_benchmark_count` synthesis
58
+ // is gone with the adapter (Step 4b); v3's `benchmark_count` is the
59
+ // distinct (composite, benchmark) row count from the dim.
60
+ const benchmarkLeafCount = stats?.benchmark_count ?? 0
 
 
 
61
  const sliceCount = stats?.slice_count ?? 0
62
  const metricCount = stats?.metric_count ?? 0
63
  const tripleCount = stats?.metric_rows_scanned ?? 0
 
74
  const generatedAt = formatGeneratedAt(manifest?.generated_at)
75
 
76
  // Featured family cards — pick the first six families with summaries.
77
+ // Curated multi-benchmark families (BFCL, MMLU, JudgeBench, …) put
78
+ // their benchmarks under composites[].benchmarks[] after the adapter;
79
+ // singletons land in standalone_benchmarks[] or benchmarks[]. Pull
80
+ // from all four shapes so the count reflects the union (matches
81
+ // family-table.tsx:313's logic).
82
  const featuredFamilies = hierarchy.families.slice(0, 6).map((family) => {
83
+ // v3 family layouts: exactly one of standalone_benchmarks / benchmarks /
84
+ // composites is present per family. Walk all three to count benchmarks.
85
+ const benches: unknown[] = [
86
+ ...(family.standalone_benchmarks ?? []),
87
+ ...(family.benchmarks ?? []),
88
+ ...((family.composites ?? []).flatMap((c) => c.benchmarks ?? [])),
89
+ ]
90
  let slices = 0
91
  for (const b of benches) {
92
  const benchSlices = (b as { slices?: unknown[] }).slices
components/benchmark-detail.tsx CHANGED
@@ -105,38 +105,15 @@ interface CompositeGroup {
105
 
106
  const INSTANCE_PREVIEW_LIMIT = 5
107
 
108
- const SUITE_DISPLAY_NAMES: Record<string, string> = {
109
- hfopenllm_v2: "HF Open LLM v2",
110
- helm_lite: "HELM Lite",
111
- helm_capabilities: "HELM Capabilities",
112
- helm_classic: "HELM Classic",
113
- helm_instruct: "HELM Instruct",
114
- helm_mmlu: "HELM MMLU",
115
- reward_bench: "RewardBench",
116
- reward_bench_2: "RewardBench 2",
117
- bfcl: "BFCL",
118
- global_mmlu_lite: "Global MMLU Lite",
119
- swe_bench: "SWE-bench",
120
- arc_agi: "ARC-AGI",
121
- tau_bench_2: "TAU-Bench 2",
122
- ace: "ACE",
123
- apex_agents: "APEX Agents",
124
- apex_v1: "APEX v1",
125
- appworld: "AppWorld",
126
- browsecompplus: "BrowseComp+",
127
- livecodebenchpro: "LiveCodeBench Pro",
128
- sciarena: "SciArena",
129
- terminal_bench_2_0: "Terminal Bench 2.0",
130
- la_leaderboard: "LA Leaderboard",
131
- theory_of_mind: "Theory of Mind",
132
- fibble_arena: "Fibble Arena",
133
- fibble1_arena: "Fibble Arena v1",
134
- fibble2_arena: "Fibble Arena v2",
135
- fibble3_arena: "Fibble Arena v3",
136
- fibble4_arena: "Fibble Arena v4",
137
- fibble5_arena: "Fibble Arena v5",
138
- wordle_arena: "Wordle Arena",
139
- }
140
 
141
  const DISPLAY_TOKEN_OVERRIDES: Record<string, string> = {
142
  ace: "ACE",
@@ -169,7 +146,6 @@ const DISPLAY_TOKEN_OVERRIDES: Record<string, string> = {
169
  }
170
 
171
  const DISPLAY_NAME_OVERRIDES: Record<string, string> = {
172
- ...SUITE_DISPLAY_NAMES,
173
  apex: "APEX",
174
  apex_agents: "APEX Agents",
175
  apex_v1: "APEX v1",
@@ -294,17 +270,17 @@ function doesLabelMatchSuiteKey(label: string | null | undefined, compositeKey:
294
 
295
  function getCompositeKey(group: BenchmarkGroup): string {
296
  const evaluation = group.variants[0]?.evaluation
297
- const backendSuiteKey =
298
- evaluation?.benchmark_parent_key ||
299
- evaluation?.benchmark_family_key ||
300
- evaluation?.benchmark
301
 
302
  return normalizeCompositeKey(backendSuiteKey ?? group.key)
303
  }
304
 
305
  function getCompositeDisplayName(key: string): string {
306
- const normalizedKey = normalizeCompositeKey(key)
307
- return SUITE_DISPLAY_NAMES[normalizedKey] ?? normalizeDisplayLabel(key)
 
 
 
308
  }
309
 
310
  function getCompositeName(group: BenchmarkGroup, compositeKey: string): string {
@@ -1426,11 +1402,12 @@ function buildBenchmarkGroups(
1426
  (entry.evaluation.slice_name && (entry.evaluation.benchmark_parent_name || entry.evaluation.benchmark)
1427
  ? `${entry.evaluation.benchmark_parent_name || entry.evaluation.benchmark} / ${entry.evaluation.slice_name}`
1428
  : title)
 
 
1429
  const groupKey =
1430
  entry.evaluation.eval_summary_id ??
1431
- entry.evaluation.benchmark_parent_key ??
1432
- entry.evaluation.benchmark_leaf_key ??
1433
- entry.evaluation.benchmark ??
1434
  "benchmark"
1435
  const card = benchmarkCards
1436
  ? lookupBenchmarkCard(benchmarkCards, rawBenchmarkName)
@@ -1988,7 +1965,7 @@ export function BenchmarkDetail({
1988
 
1989
  // Family-bucketed groups for the list view, mirroring plotboxUnits logic.
1990
  // When comparisonIndex is available we use the backend-authoritative
1991
- // benchmark_family_key; otherwise we fall back to the group's own key so each
1992
  // BenchmarkGroup forms its own family.
1993
  type ListFamily = {
1994
  familyKey: string
@@ -2008,9 +1985,9 @@ export function BenchmarkDetail({
2008
  ?.evaluation.eval_summary_id
2009
  const evalEntry =
2010
  evalId && comparisonIndex ? comparisonIndex.evals[evalId] : null
2011
- const famKey = evalEntry?.benchmark_family_key ?? group.key
2012
  const famName =
2013
- evalEntry?.benchmark_family_name ||
2014
  evalEntry?.display_name ||
2015
  group.title
2016
 
@@ -2338,7 +2315,12 @@ export function BenchmarkDetail({
2338
  .filter((p) => selectedIds.has(p.model_route_id))
2339
  .map((p) => ({
2340
  modelId: p.model_route_id,
2341
- modelName: getModelDisplayName(p.model_family_name),
 
 
 
 
 
2342
  score: p.score,
2343
  isCurrent: false,
2344
  isDefault: defaults.has(p.model_route_id),
@@ -2406,7 +2388,7 @@ export function BenchmarkDetail({
2406
  // A plotbox can expose a top-level "view" selector (slices, child
2407
  // benchmarks, components) and an optional metric tab rail beneath the chart.
2408
  // Plotbox grouping is driven entirely by comparison-index's own
2409
- // benchmark_family_key so it stays in sync with the backend.
2410
  type PlotboxMetricTab = {
2411
  tabKey: string
2412
  label: string
@@ -2474,9 +2456,9 @@ export function BenchmarkDetail({
2474
  const evalEntry = comparisonIndex.evals[evalId]
2475
  if (!evalEntry) continue
2476
 
2477
- const famKey = evalEntry.benchmark_family_key ?? evalId
2478
  const famName =
2479
- evalEntry.benchmark_family_name || evalEntry.display_name || famKey
2480
  const bucket = familyBuckets.get(famKey) ?? {
2481
  familyName: famName,
2482
  category: group.category,
@@ -2509,8 +2491,7 @@ export function BenchmarkDetail({
2509
  histKey: histKeyFor(evalEntry.eval_summary_id, metric.metric_summary_id),
2510
  evalSummaryId: evalEntry.eval_summary_id,
2511
  metricSummaryId: metric.metric_summary_id,
2512
- evalDisplayName:
2513
- evalEntry.display_name || evalEntry.benchmark_leaf_name || group.title,
2514
  evalEntry,
2515
  metricEntry: metric,
2516
  isRollup,
@@ -2526,8 +2507,7 @@ export function BenchmarkDetail({
2526
  // One eval in scope — slices/splits become the view selector while
2527
  // metrics move to a compact tab rail beneath the chart.
2528
  const { group, evalEntry } = resolved[0]
2529
- const evalDisplay =
2530
- evalEntry.display_name || evalEntry.benchmark_leaf_name || group.title
2531
  const singleEvalViewBuckets = new Map<
2532
  string,
2533
  { viewKey: string; label: string; variants: BenchmarkVariant[] }
@@ -2598,21 +2578,22 @@ export function BenchmarkDetail({
2598
 
2599
  // Multi-eval family — the view selector chooses among child evals and
2600
  // each view exposes that eval's metrics in the bottom tab rail.
 
 
 
 
2601
  const rollup =
2602
  resolved.find(
2603
  (r) =>
2604
- r.evalEntry.benchmark_leaf_key != null &&
2605
- r.evalEntry.benchmark_leaf_key === r.evalEntry.benchmark_family_key
2606
  ) ?? null
2607
  const children = rollup ? resolved.filter((r) => r !== rollup) : resolved
2608
  const ordered: ResolvedGroup[] = rollup ? [rollup, ...children] : children
2609
 
2610
  const views: PlotboxView[] = ordered
2611
  .map((r) => {
2612
- const rawLabel =
2613
- r.evalEntry.benchmark_leaf_name ||
2614
- r.evalEntry.display_name ||
2615
- r.group.title
2616
  const label =
2617
  r === rollup ? "Overall" : stripFamilyPrefix(rawLabel, familyName)
2618
  const tabs = r.evalEntry.metrics
@@ -2648,8 +2629,20 @@ export function BenchmarkDetail({
2648
  let hasSlice = false
2649
  let hasDistinctLeaves = false
2650
  for (const r of children) {
2651
- const leafKey = r.evalEntry.benchmark_leaf_key
2652
- if (leafKey && leafKey !== r.evalEntry.benchmark_family_key) {
 
 
 
 
 
 
 
 
 
 
 
 
2653
  hasDistinctLeaves = true
2654
  }
2655
  if (r.group.variants[0]?.evaluation.benchmark_component_key ?? null) {
 
105
 
106
  const INSTANCE_PREVIEW_LIMIT = 5
107
 
108
+ // SUITE_DISPLAY_NAMES (38-entry hardcoded slug→display map) was deleted
109
+ // in Step 4c of the hierarchy-alignment work
110
+ // (notes/hierarchy-alignment.md §6 / §7 Step 4). The producer now ships
111
+ // curated display names for every family / composite / benchmark via
112
+ // prettify_display + the registry's display_overrides.yaml. This
113
+ // component reads the shipped name; the DISPLAY_TOKEN_OVERRIDES /
114
+ // DISPLAY_NAME_OVERRIDES below remain as a per-token polish layer
115
+ // (mostly for raw model identifier rendering, where the producer's
116
+ // metadata doesn't carry a curated display).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  const DISPLAY_TOKEN_OVERRIDES: Record<string, string> = {
119
  ace: "ACE",
 
146
  }
147
 
148
  const DISPLAY_NAME_OVERRIDES: Record<string, string> = {
 
149
  apex: "APEX",
150
  apex_agents: "APEX Agents",
151
  apex_v1: "APEX v1",
 
270
 
271
  function getCompositeKey(group: BenchmarkGroup): string {
272
  const evaluation = group.variants[0]?.evaluation
273
+ const backendSuiteKey = evaluation?.family_id
 
 
 
274
 
275
  return normalizeCompositeKey(backendSuiteKey ?? group.key)
276
  }
277
 
278
  function getCompositeDisplayName(key: string): string {
279
+ // Display names come from the shipped hierarchy.json. This helper
280
+ // is used in fallback paths where only a raw key is in scope; it
281
+ // applies the same per-token polish (DISPLAY_TOKEN_OVERRIDES) the
282
+ // rest of the renderer uses.
283
+ return normalizeDisplayLabel(key)
284
  }
285
 
286
  function getCompositeName(group: BenchmarkGroup, compositeKey: string): string {
 
1402
  (entry.evaluation.slice_name && (entry.evaluation.benchmark_parent_name || entry.evaluation.benchmark)
1403
  ? `${entry.evaluation.benchmark_parent_name || entry.evaluation.benchmark} / ${entry.evaluation.slice_name}`
1404
  : title)
1405
+ // eval_summary_id is producer-shipped on every v3 entry; the
1406
+ // remaining ?? tiers are for legacy snapshots without that field.
1407
  const groupKey =
1408
  entry.evaluation.eval_summary_id ??
1409
+ entry.evaluation.parent_benchmark_id ??
1410
+ entry.evaluation.family_id ??
 
1411
  "benchmark"
1412
  const card = benchmarkCards
1413
  ? lookupBenchmarkCard(benchmarkCards, rawBenchmarkName)
 
1965
 
1966
  // Family-bucketed groups for the list view, mirroring plotboxUnits logic.
1967
  // When comparisonIndex is available we use the backend-authoritative
1968
+ // family_id; otherwise we fall back to the group's own key so each
1969
  // BenchmarkGroup forms its own family.
1970
  type ListFamily = {
1971
  familyKey: string
 
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
1993
 
 
2315
  .filter((p) => selectedIds.has(p.model_route_id))
2316
  .map((p) => ({
2317
  modelId: p.model_route_id,
2318
+ // Fall back to model_family_id when the registry has no display
2319
+ // name for this model. ~86% of score entries today land here;
2320
+ // root cause (registry coverage) is Step 2 work. The id is
2321
+ // already human-readable in this codebase ("anthropic/Sonnet 4.5"),
2322
+ // so the fallback is more useful than "Unknown Model".
2323
+ modelName: getModelDisplayName(p.model_family_name || p.model_family_id),
2324
  score: p.score,
2325
  isCurrent: false,
2326
  isDefault: defaults.has(p.model_route_id),
 
2388
  // A plotbox can expose a top-level "view" selector (slices, child
2389
  // benchmarks, components) and an optional metric tab rail beneath the chart.
2390
  // Plotbox grouping is driven entirely by comparison-index's own
2391
+ // family_id so it stays in sync with the backend.
2392
  type PlotboxMetricTab = {
2393
  tabKey: string
2394
  label: string
 
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,
 
2491
  histKey: histKeyFor(evalEntry.eval_summary_id, metric.metric_summary_id),
2492
  evalSummaryId: evalEntry.eval_summary_id,
2493
  metricSummaryId: metric.metric_summary_id,
2494
+ evalDisplayName: evalEntry.display_name || group.title,
 
2495
  evalEntry,
2496
  metricEntry: metric,
2497
  isRollup,
 
2507
  // One eval in scope — slices/splits become the view selector while
2508
  // metrics move to a compact tab rail beneath the chart.
2509
  const { group, evalEntry } = resolved[0]
2510
+ const evalDisplay = evalEntry.display_name || group.title
 
2511
  const singleEvalViewBuckets = new Map<
2512
  string,
2513
  { viewKey: string; label: string; variants: BenchmarkVariant[] }
 
2578
 
2579
  // Multi-eval family — the view selector chooses among child evals and
2580
  // each view exposes that eval's metrics in the bottom tab rail.
2581
+ // Rollup row = this eval IS the family root, i.e. its benchmark id
2582
+ // matches the family id. For multi-benchmark families like HELM,
2583
+ // there is no such eval (HELM has no "helm" benchmark), so rollup
2584
+ // stays null and the children render as siblings.
2585
  const rollup =
2586
  resolved.find(
2587
  (r) =>
2588
+ r.evalEntry.benchmark_id != null &&
2589
+ r.evalEntry.benchmark_id === r.evalEntry.family_id,
2590
  ) ?? null
2591
  const children = rollup ? resolved.filter((r) => r !== rollup) : resolved
2592
  const ordered: ResolvedGroup[] = rollup ? [rollup, ...children] : children
2593
 
2594
  const views: PlotboxView[] = ordered
2595
  .map((r) => {
2596
+ const rawLabel = r.evalEntry.display_name || r.group.title
 
 
 
2597
  const label =
2598
  r === rollup ? "Overall" : stripFamilyPrefix(rawLabel, familyName)
2599
  const tabs = r.evalEntry.metrics
 
2629
  let hasSlice = false
2630
  let hasDistinctLeaves = false
2631
  for (const r of children) {
2632
+ // "Distinct leaf" = this child has an identity distinct from the
2633
+ // family root. Two cases:
2634
+ // 1. Non-slice child (e.g. HELM/MMLU under HELM family).
2635
+ // 2. Slice whose parent benchmark is not the family itself
2636
+ // (e.g. HELM/MMLU/anatomy — slice of MMLU under HELM, where
2637
+ // parent="mmlu" ≠ family="helm"). In a singleton family the
2638
+ // slice's parent equals the family and this case collapses
2639
+ // back to "slice", as expected.
2640
+ const evalEntry = r.evalEntry
2641
+ const isDistinctLeaf =
2642
+ evalEntry.is_slice === false ||
2643
+ (evalEntry.parent_benchmark_id != null &&
2644
+ evalEntry.parent_benchmark_id !== evalEntry.family_id)
2645
+ if (isDistinctLeaf) {
2646
  hasDistinctLeaves = true
2647
  }
2648
  if (r.group.variants[0]?.evaluation.benchmark_component_key ?? null) {
components/eval-detail.tsx CHANGED
@@ -373,7 +373,8 @@ function splitFlagNote(raw: string): { tag: string | null; body: string } {
373
  return { tag: match[1].trim(), body: match[2].trim() }
374
  }
375
 
376
- function formatRawScore(score: number, unit?: string) {
 
377
  const suffix = unit ? ` ${unit}` : ""
378
  return `${score.toFixed(2)}${suffix}`
379
  }
@@ -644,8 +645,8 @@ export function EvalDetail({ summary }: EvalDetailProps) {
644
  summary.evaluation_name,
645
  summary.composite_benchmark_name,
646
  summary.composite_benchmark_key,
647
- summary.benchmark_family_key,
648
- summary.benchmark_leaf_key,
649
  summary.benchmark_card.benchmark_details?.name,
650
  )}
651
  />
@@ -1317,7 +1318,7 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1317
  <div className="space-y-3">
1318
  <ResearcherReproducibilityCard
1319
  modelResult={modelResult}
1320
- benchmarkKey={summary.benchmark_id ?? summary.benchmark_leaf_key ?? summary.composite_benchmark_key}
1321
  evalName={summary.evaluation_name}
1322
  />
1323
  <div className="flex justify-end">
@@ -2039,7 +2040,7 @@ function MultiMetricLeaderboard({
2039
  <div className="space-y-3">
2040
  <ResearcherReproducibilityCard
2041
  modelResult={matchingResult}
2042
- benchmarkKey={summary.benchmark_id ?? summary.benchmark_leaf_key ?? summary.composite_benchmark_key}
2043
  evalName={summary.evaluation_name}
2044
  />
2045
  <div className="flex justify-end">
 
373
  return { tag: match[1].trim(), body: match[2].trim() }
374
  }
375
 
376
+ function formatRawScore(score: number | null | undefined, unit?: string) {
377
+ if (score == null || !Number.isFinite(score)) return "—"
378
  const suffix = unit ? ` ${unit}` : ""
379
  return `${score.toFixed(2)}${suffix}`
380
  }
 
645
  summary.evaluation_name,
646
  summary.composite_benchmark_name,
647
  summary.composite_benchmark_key,
648
+ summary.family_id,
649
+ summary.benchmark_id,
650
  summary.benchmark_card.benchmark_details?.name,
651
  )}
652
  />
 
1318
  <div className="space-y-3">
1319
  <ResearcherReproducibilityCard
1320
  modelResult={modelResult}
1321
+ benchmarkKey={summary.benchmark_id ?? summary.composite_benchmark_key}
1322
  evalName={summary.evaluation_name}
1323
  />
1324
  <div className="flex justify-end">
 
2040
  <div className="space-y-3">
2041
  <ResearcherReproducibilityCard
2042
  modelResult={matchingResult}
2043
+ benchmarkKey={summary.benchmark_id ?? summary.composite_benchmark_key}
2044
  evalName={summary.evaluation_name}
2045
  />
2046
  <div className="flex justify-end">
components/family-table.tsx CHANGED
@@ -4,7 +4,7 @@ import { Fragment, useMemo, useState } from "react"
4
  import { useRouter } from "next/navigation"
5
  import { ArrowUpRight, ChevronDown, ChevronRight } from "lucide-react"
6
 
7
- import type { HierarchyBenchmark, HierarchyFamily, HierarchyLeaf } from "@/lib/backend-artifacts"
8
  import type { BenchmarkCard, CategoryType } from "@/lib/benchmark-schema"
9
  import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
10
  import { humanizeEvaluationId } from "@/lib/utils"
@@ -118,38 +118,22 @@ function collectLeafEntries(
118
  })
119
  }
120
 
121
- if (out.length > 0) return out
122
-
123
- // ── Fallback: legacy `leaves` shape ────────────────────────────────
124
- for (const leaf of fam.leaves ?? []) {
125
- const explicit = leaf.eval_summary_ids ?? []
126
- const ids =
127
- explicit.length > 0
128
- ? explicit
129
- : leaf.key
130
- ? [`${fam.key}_${leaf.key}`, leaf.key]
131
- : []
132
- if (ids.length === 0) continue
133
- const collected = new Set<string>()
134
- for (const d of leaf.tags?.domains ?? []) collected.add(d.toLowerCase())
135
- const cardByLeaf = benchmarkCards?.[leaf.key]
136
- for (const d of cardByLeaf?.benchmark_details?.domains ?? []) collected.add(d.toLowerCase())
137
- for (const id of ids) {
138
- const cardById = benchmarkCards?.[id]
139
- for (const d of cardById?.benchmark_details?.domains ?? []) collected.add(d.toLowerCase())
140
- }
141
- out.push({
142
- id: ids[0],
143
- leafKey: leaf.key,
144
- leafName: leaf.display_name || leaf.key,
145
- evalsCount: leaf.evals_count ?? ids.length,
146
- domains: Array.from(collected),
147
- })
148
- }
149
-
150
  return out
151
  }
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  /**
154
  * Pick the eval_summary_id to navigate to when the user clicks the family
155
  * row. Returns null when the family has no genuine family-level summary —
@@ -308,21 +292,17 @@ export function FamilyTable({
308
  const composites = fam.composites ?? []
309
  const standalone = fam.standalone_benchmarks ?? []
310
  const benchmarks = fam.benchmarks ?? []
311
- const leaves: HierarchyLeaf[] = fam.leaves ?? []
312
 
313
  const allBenchmarks = [
314
  ...standalone,
315
  ...benchmarks,
316
  ...composites.flatMap((c) => c.benchmarks ?? []),
317
  ]
318
- const metricCount =
319
- (fam.metrics?.length ?? 0) +
320
- allBenchmarks.reduce(
321
- (sum, b) => sum + ((b as { metrics?: unknown[] }).metrics?.length ?? 0),
322
- 0,
323
- )
324
- const benchmarkCount =
325
- allBenchmarks.length > 0 ? allBenchmarks.length : leaves.length
326
 
327
  const leafEntries = collectLeafEntries(fam, benchmarkCards)
328
  const navId = pickFamilyNavId(fam, leafEntries)
 
4
  import { useRouter } from "next/navigation"
5
  import { ArrowUpRight, ChevronDown, ChevronRight } from "lucide-react"
6
 
7
+ import type { HierarchyBenchmark, HierarchyFamily } from "@/lib/backend-artifacts"
8
  import type { BenchmarkCard, CategoryType } from "@/lib/benchmark-schema"
9
  import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
10
  import { humanizeEvaluationId } from "@/lib/utils"
 
118
  })
119
  }
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  return out
122
  }
123
 
124
+ /**
125
+ * Resolve the family-level navigation target without exposing the
126
+ * internal `LeafEntry` shape. Returns the eval_summary_id to open when
127
+ * the user clicks the family card / row, or `null` for aggregator
128
+ * families that should expand inline instead of navigating.
129
+ */
130
+ export function getFamilyNavId(
131
+ fam: HierarchyFamily,
132
+ benchmarkCards?: Record<string, BenchmarkCard>,
133
+ ): string | null {
134
+ return pickFamilyNavId(fam, collectLeafEntries(fam, benchmarkCards))
135
+ }
136
+
137
  /**
138
  * Pick the eval_summary_id to navigate to when the user clicks the family
139
  * row. Returns null when the family has no genuine family-level summary —
 
292
  const composites = fam.composites ?? []
293
  const standalone = fam.standalone_benchmarks ?? []
294
  const benchmarks = fam.benchmarks ?? []
 
295
 
296
  const allBenchmarks = [
297
  ...standalone,
298
  ...benchmarks,
299
  ...composites.flatMap((c) => c.benchmarks ?? []),
300
  ]
301
+ const metricCount = allBenchmarks.reduce(
302
+ (sum, b) => sum + (b.metrics?.length ?? 0),
303
+ 0,
304
+ )
305
+ const benchmarkCount = allBenchmarks.length
 
 
 
306
 
307
  const leafEntries = collectLeafEntries(fam, benchmarkCards)
308
  const navId = pickFamilyNavId(fam, leafEntries)
components/policy-overview.tsx CHANGED
@@ -59,7 +59,7 @@ export function PolicyOverview({ summary }: PolicyOverviewProps) {
59
  const cardNameNorm = normalizeId(cardName)
60
  const evalIdentifiers = [
61
  summary.evaluation_name,
62
- summary.benchmark_leaf_key,
63
  summary.composite_benchmark_key,
64
  summary.composite_benchmark_name,
65
  summary.canonical_display_name,
@@ -180,16 +180,16 @@ export function PolicyOverview({ summary }: PolicyOverviewProps) {
180
  summary.evaluation_name,
181
  summary.composite_benchmark_name,
182
  summary.composite_benchmark_key,
183
- summary.benchmark_family_key,
184
- summary.benchmark_leaf_key,
185
  card?.benchmark_details?.name,
186
  ),
187
  [
188
  summary.evaluation_name,
189
  summary.composite_benchmark_name,
190
  summary.composite_benchmark_key,
191
- summary.benchmark_family_key,
192
- summary.benchmark_leaf_key,
193
  card?.benchmark_details?.name,
194
  ],
195
  )
 
59
  const cardNameNorm = normalizeId(cardName)
60
  const evalIdentifiers = [
61
  summary.evaluation_name,
62
+ summary.benchmark_id,
63
  summary.composite_benchmark_key,
64
  summary.composite_benchmark_name,
65
  summary.canonical_display_name,
 
180
  summary.evaluation_name,
181
  summary.composite_benchmark_name,
182
  summary.composite_benchmark_key,
183
+ summary.family_id,
184
+ summary.benchmark_id,
185
  card?.benchmark_details?.name,
186
  ),
187
  [
188
  summary.evaluation_name,
189
  summary.composite_benchmark_name,
190
  summary.composite_benchmark_key,
191
+ summary.family_id,
192
+ summary.benchmark_id,
193
  card?.benchmark_details?.name,
194
  ],
195
  )
components/signals/corpus-signals-strip.tsx CHANGED
@@ -87,7 +87,7 @@ export function CorpusSignalsStrip({
87
  statValue={pctNum(multiSourceRate)}
88
  statUnit="%"
89
  headline="of reported score triples have reports from more than one party."
90
- detail={`${formatPct(tpShare)} third-party, ${formatPct(fpShare)} first-party of ${totalReports.toLocaleString()} triples.`}
91
  asks="Who reported this score, and have others reproduced it?"
92
  />
93
  <SignalTile
 
87
  statValue={pctNum(multiSourceRate)}
88
  statUnit="%"
89
  headline="of reported score triples have reports from more than one party."
90
+ detail={`${formatPct(tpShare)} third-party, ${formatPct(fpShare)} first-party of ${totalReports.toLocaleString()} unique triples.`}
91
  asks="Who reported this score, and have others reproduced it?"
92
  />
93
  <SignalTile
lib/backend-artifacts.ts CHANGED
@@ -241,6 +241,24 @@ export interface ComparabilityCorpusBlock {
241
  groups_with_cross_party_check: number
242
  }
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  export interface HierarchyTags {
245
  domains: string[]
246
  languages: string[]
@@ -250,109 +268,108 @@ export interface HierarchyTags {
250
  export interface HierarchyMetric {
251
  key: string
252
  display_name: string
 
 
253
  sources?: string[]
 
 
 
 
 
 
254
  }
255
 
256
  export interface HierarchySlice {
257
  key: string
258
  display_name: string
259
  metrics: HierarchyMetric[]
260
- /** New: marks the bare-stem "Overall" slice (e.g. `gaia` inside
261
- * the `gaia` benchmark). Frontend can label such a row "Overall". */
262
  is_bare_stem?: boolean
263
  }
264
 
265
  export interface HierarchyBenchmark extends SignalSummaries {
266
  key: string
267
  display_name: string
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  has_card: boolean
269
  tags: HierarchyTags
270
  slices: HierarchySlice[]
271
  metrics: HierarchyMetric[]
272
  summary_eval_ids?: string[]
273
- /** New post-cutover: family slug (defaults to benchmark key for singletons). */
274
- family_id?: string
275
- /** New post-cutover: TRUE when this row is a slice of a root benchmark. */
276
- is_slice?: boolean
277
  }
278
 
279
  export interface HierarchyComposite extends SignalSummaries {
280
  key: string
281
  display_name: string
282
- has_card?: boolean
283
  category: string
284
  tags: HierarchyTags
285
  benchmarks: HierarchyBenchmark[]
286
- summary_eval_ids?: string[]
287
- /** New top-level shape: total triples in the composite. */
288
  evals_count?: number
 
 
 
289
  }
290
 
291
- export interface HierarchyLeaf extends SignalSummaries {
292
  key: string
293
  display_name: string
294
  category: string
295
- evals_count?: number
296
- eval_summary_ids?: string[]
297
- tags?: Partial<HierarchyTags>
298
- has_card?: boolean
 
 
 
299
  }
300
 
301
- export interface HierarchyFamily extends SignalSummaries {
 
 
 
 
 
 
 
 
 
302
  key: string
303
  display_name: string
304
- has_card?: boolean
305
- category: string
306
- tags?: Partial<HierarchyTags>
307
- evals_count?: number
308
- eval_summary_ids?: string[]
309
- // Legacy nested shape (composites + standalone benchmarks)
310
- standalone_benchmarks?: HierarchyBenchmark[]
311
- composites?: HierarchyComposite[]
312
- benchmarks?: HierarchyBenchmark[]
313
- slices?: HierarchySlice[]
314
- metrics?: HierarchyMetric[]
315
- // Newer 2-level shape (family → leaf)
316
- leaves?: HierarchyLeaf[]
317
  }
318
 
319
  export interface EvalHierarchyStats {
320
  family_count: number
321
  composite_count: number
322
- /** Legacy benchmark-stem grouping count. Removed in the composite/
323
- * family/slice taxonomy refactor — kept optional so the adapter
324
- * can synthesise it for the existing homepage stats strip. */
325
- standalone_benchmark_count?: number
326
- /** Same as above. */
327
- single_benchmark_count?: number
328
- /** New post-cutover field: total distinct (composite, benchmark) rows
329
- * in the benchmarks dim. */
330
- benchmark_count?: number
331
  slice_count: number
332
  metric_count: number
333
  metric_rows_scanned: number
334
  }
335
 
336
- /** Lightweight family-lookup index entry from the new top-level
337
- * `families[]` array (composite/family/slice taxonomy). One per
338
- * family_id with the list of member benchmark keys — no nested
339
- * composites, no slice payload. The legacy `HierarchyFamily` shape
340
- * (with nested `composites[]` / `standalone_benchmarks[]`) is
341
- * synthesised by the adapter for backward compat. */
342
- export interface HierarchyFamilyIndex {
343
- key: string
344
- display_name: string
345
- member_benchmark_keys: string[]
346
- }
347
-
348
  export interface EvalHierarchy {
 
 
 
349
  stats?: EvalHierarchyStats
350
  families: HierarchyFamily[]
351
- /** New post-cutover top-level array — one per leaderboard slug. */
352
- composites?: HierarchyComposite[]
353
- /** New post-cutover flat lookup. The adapter promotes this onto
354
- * per-family records as it builds the legacy shape. */
355
- family_index?: HierarchyFamilyIndex[]
356
  }
357
 
358
  // ---------------------------------------------------------------------------
@@ -407,14 +424,15 @@ export interface ComparisonMetricEntry {
407
 
408
  export interface ComparisonEvalEntry {
409
  eval_summary_id: string
410
- benchmark_family_key: string | null
411
- benchmark_family_name: string | null
412
- benchmark_parent_key: string | null
413
- benchmark_parent_name: string | null
414
- benchmark_leaf_key: string | null
415
- benchmark_leaf_name: string | null
416
  display_name: string | null
417
  category: string
 
418
  is_summary_score: boolean
419
  summary_score_for: string | null
420
  summary_eval_ids: string[]
 
241
  groups_with_cross_party_check: number
242
  }
243
 
244
+ // ---------------------------------------------------------------------------
245
+ // Hierarchy types (v3 — family-rooted tree).
246
+ //
247
+ // The producer emits this shape via eval_card_backend's
248
+ // `write_hierarchy()` after the Step 3 reshape. See
249
+ // /Users/jchim/projects/evaleval/notes/hierarchy-alignment.md §5.1
250
+ // for the canonical spec.
251
+ //
252
+ // Top level: `families[]` is the rich entity. Composites nest under
253
+ // families[].composites[]. `benchmark_index[]` cross-links a canonical
254
+ // benchmark that appears in multiple families.
255
+ //
256
+ // Each family chooses ONE of three layouts:
257
+ // - standalone_benchmarks: single-benchmark family.
258
+ // - benchmarks (flat): multiple benchmarks, no composite layer.
259
+ // - composites: multi-composite family (HELM has 7).
260
+ // ---------------------------------------------------------------------------
261
+
262
  export interface HierarchyTags {
263
  domains: string[]
264
  languages: string[]
 
268
  export interface HierarchyMetric {
269
  key: string
270
  display_name: string
271
+ /** Producer-supplied list of organisations whose results back this
272
+ * metric. Empty when source attribution wasn't recoverable. */
273
  sources?: string[]
274
+ /** Per spec §5.1 — true when this is the benchmark's primary metric
275
+ * (matches `primary_metric_key`). */
276
+ is_primary?: boolean
277
+ /** Distinct model count contributing to this metric — drives
278
+ * primary-metric tie-break. */
279
+ models_count?: number
280
  }
281
 
282
  export interface HierarchySlice {
283
  key: string
284
  display_name: string
285
  metrics: HierarchyMetric[]
286
+ /** Marks the bare-stem "Overall" slice (e.g. `gaia` inside the
287
+ * `gaia` benchmark). Frontend labels such a row "Overall". */
288
  is_bare_stem?: boolean
289
  }
290
 
291
  export interface HierarchyBenchmark extends SignalSummaries {
292
  key: string
293
  display_name: string
294
+ family_id: string
295
+ is_slice: boolean
296
+ /** True when this row IS the family/composite root (canonical_id
297
+ * matches the family or composite key). For a singleton family,
298
+ * the sole benchmark is overall. For multi-bench families with
299
+ * no head benchmark of the same name (HAL, BFCL with no `bfcl`
300
+ * benchmark), all are False. */
301
+ is_overall: boolean
302
+ /** True for the benchmark within its family that's the headline
303
+ * reading. Selected via FAMILY_PRIMARY_OVERRIDE → is_overall →
304
+ * alphabetical (see _mark_family_primary_benchmark in producer). */
305
+ is_primary?: boolean
306
+ /** Metric key whose primary metric should be displayed as the
307
+ * benchmark's headline number. Null when the benchmark has no
308
+ * metrics. */
309
+ primary_metric_key?: string | null
310
  has_card: boolean
311
  tags: HierarchyTags
312
  slices: HierarchySlice[]
313
  metrics: HierarchyMetric[]
314
  summary_eval_ids?: string[]
 
 
 
 
315
  }
316
 
317
  export interface HierarchyComposite extends SignalSummaries {
318
  key: string
319
  display_name: string
 
320
  category: string
321
  tags: HierarchyTags
322
  benchmarks: HierarchyBenchmark[]
 
 
323
  evals_count?: number
324
+ summary_eval_ids?: string[]
325
+ /** True for the headline composite within a multi-composite family. */
326
+ is_primary?: boolean
327
  }
328
 
329
+ export interface HierarchyFamily extends SignalSummaries {
330
  key: string
331
  display_name: string
332
  category: string
333
+ tags: HierarchyTags
334
+ evals_count: number
335
+ eval_summary_ids: string[]
336
+ /** Exactly ONE of the three layout fields below is present. */
337
+ standalone_benchmarks?: HierarchyBenchmark[]
338
+ benchmarks?: HierarchyBenchmark[]
339
+ composites?: HierarchyComposite[]
340
  }
341
 
342
+ export interface BenchmarkIndexAppearance {
343
+ family_key: string
344
+ benchmark_key: string
345
+ eval_summary_ids: string[]
346
+ /** True when the family this appearance is under is the benchmark's
347
+ * natural "home" family (family_key === benchmark_key). */
348
+ is_canonical_home: boolean
349
+ }
350
+
351
+ export interface BenchmarkIndexEntry {
352
  key: string
353
  display_name: string
354
+ appearances: BenchmarkIndexAppearance[]
 
 
 
 
 
 
 
 
 
 
 
 
355
  }
356
 
357
  export interface EvalHierarchyStats {
358
  family_count: number
359
  composite_count: number
360
+ benchmark_count: number
 
 
 
 
 
 
 
 
361
  slice_count: number
362
  metric_count: number
363
  metric_rows_scanned: number
364
  }
365
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  export interface EvalHierarchy {
367
+ /** Schema marker: "v3.hierarchy.1". Older snapshots lack this. */
368
+ schema_version?: string
369
+ generated_at?: string
370
  stats?: EvalHierarchyStats
371
  families: HierarchyFamily[]
372
+ benchmark_index?: BenchmarkIndexEntry[]
 
 
 
 
373
  }
374
 
375
  // ---------------------------------------------------------------------------
 
424
 
425
  export interface ComparisonEvalEntry {
426
  eval_summary_id: string
427
+ benchmark_id: string | null
428
+ family_id: string | null
429
+ family_display_name: string | null
430
+ composite_slug: string | null
431
+ composite_display_name: string | null
432
+ parent_benchmark_id: string | null
433
  display_name: string | null
434
  category: string
435
+ is_slice: boolean
436
  is_summary_score: boolean
437
  summary_score_for: string | null
438
  summary_eval_ids: string[]
lib/benchmark-schema.ts CHANGED
@@ -10,16 +10,22 @@ export interface BenchmarkEvaluation {
10
  eval_summary_id?: string
11
  evaluation_id: string
12
  retrieved_timestamp: string
 
 
 
 
 
 
13
  benchmark?: string
14
  display_name?: string
15
  canonical_display_name?: string
16
  category?: CategoryType
17
- benchmark_family_key?: string
18
  benchmark_family_name?: string
19
- benchmark_parent_key?: string
20
  benchmark_parent_name?: string
21
- benchmark_leaf_key?: string
22
  benchmark_leaf_name?: string
 
23
  benchmark_component_key?: string | null
24
  benchmark_component_name?: string | null
25
  is_summary_score?: boolean
 
10
  eval_summary_id?: string
11
  evaluation_id: string
12
  retrieved_timestamp: string
13
+ /** Legacy benchmark-name-or-slug field. The view-data layer
14
+ * (lib/view-data.ts) populates this from eval_evaluation_name with a
15
+ * benchmark_id fallback, so it works as a stable name source for
16
+ * badges and grouping when nothing better is on-hand. New callers
17
+ * should prefer `display_name` / `canonical_display_name`; this
18
+ * field is kept for the surfaces that aren't yet migrated. */
19
  benchmark?: string
20
  display_name?: string
21
  canonical_display_name?: string
22
  category?: CategoryType
23
+ family_id?: string
24
  benchmark_family_name?: string
25
+ parent_benchmark_id?: string
26
  benchmark_parent_name?: string
 
27
  benchmark_leaf_name?: string
28
+ is_slice?: boolean
29
  benchmark_component_key?: string | null
30
  benchmark_component_name?: string | null
31
  is_summary_score?: boolean
lib/eval-processing.ts CHANGED
@@ -176,22 +176,20 @@ export interface BenchmarkEvalSummary extends SignalSummaries {
176
  /** Canonical benchmark id (the registry-resolved benchmark). Drives
177
  * benchmark-card lookups regardless of slice/composite axis. */
178
  benchmark_id?: string
179
- /** Benchmark family grouping key — curated multi-benchmark family
180
- * slug (e.g. "mmlu"), defaults to benchmark id for singletons. */
181
- benchmark_family_key?: string
182
  /** Family display name. */
183
  benchmark_family_name?: string
184
- /** Leaf benchmark key — populated when this row is a slice of a
185
- * parent benchmark; null for non-slice rows. */
186
- benchmark_leaf_key?: string
187
  /** Composite (leaderboard) slug — e.g. "wasp", "helm-classic". */
188
  composite_slug?: string
189
  /** Composite display name — e.g. "WASP", "HELM Classic". */
190
  composite_display_name?: string
191
- /** Family slug, post-cutover canonical name (alias of benchmark_family_key). */
 
192
  family_id?: string
193
  /** Family display, post-cutover canonical name. */
194
  family_display_name?: string
 
 
 
195
  /** True when this row is a within-benchmark slice cut. */
196
  is_slice?: boolean
197
  /** Source dataset metadata from the pipeline */
 
176
  /** Canonical benchmark id (the registry-resolved benchmark). Drives
177
  * benchmark-card lookups regardless of slice/composite axis. */
178
  benchmark_id?: string
 
 
 
179
  /** Family display name. */
180
  benchmark_family_name?: string
 
 
 
181
  /** Composite (leaderboard) slug — e.g. "wasp", "helm-classic". */
182
  composite_slug?: string
183
  /** Composite display name — e.g. "WASP", "HELM Classic". */
184
  composite_display_name?: string
185
+ /** Curated multi-benchmark family slug (e.g. "mmlu"), defaults to
186
+ * benchmark id for singletons. */
187
  family_id?: string
188
  /** Family display, post-cutover canonical name. */
189
  family_display_name?: string
190
+ /** Parent benchmark id — populated when this row is a slice of a
191
+ * root benchmark; null for non-slice rows. */
192
+ parent_benchmark_id?: string
193
  /** True when this row is a within-benchmark slice cut. */
194
  is_slice?: boolean
195
  /** Source dataset metadata from the pipeline */
lib/hf-data.ts CHANGED
@@ -932,386 +932,35 @@ export async function fetchEvalHierarchy(): Promise<EvalHierarchy> {
932
  * back under per-family records, with the composite slug as the
933
  * family key when no curated multi-benchmark family applies.
934
  */
935
- export function adaptEvalHierarchy(raw: EvalHierarchy): EvalHierarchy {
936
- const newShape = Array.isArray(raw.composites) && raw.composites.length > 0
937
- if (newShape) {
938
- return adaptCompositeShape(raw)
939
- }
940
-
941
- const families = (raw.families ?? []).map((family) => {
942
- const hasLegacyTree =
943
- (family.composites && family.composites.length > 0) ||
944
- (family.standalone_benchmarks && family.standalone_benchmarks.length > 0) ||
945
- (family.benchmarks && family.benchmarks.length > 0)
946
-
947
- if (hasLegacyTree) {
948
- return family
949
- }
950
-
951
- const leaves = family.leaves ?? []
952
- if (leaves.length === 0) {
953
- return family
954
- }
955
-
956
- const standalone = leaves.map((leaf) => ({
957
- key: leaf.key,
958
- display_name: leaf.display_name,
959
- has_card: leaf.has_card ?? false,
960
- tags: {
961
- domains: leaf.tags?.domains ?? [],
962
- languages: leaf.tags?.languages ?? [],
963
- tasks: leaf.tags?.tasks ?? [],
964
- },
965
- slices: [],
966
- metrics: [],
967
- reproducibility_summary: leaf.reproducibility_summary,
968
- provenance_summary: leaf.provenance_summary,
969
- comparability_summary: leaf.comparability_summary,
970
- summary_eval_ids: leaf.eval_summary_ids,
971
- }))
972
-
973
- return {
974
- ...family,
975
- tags: {
976
- domains: family.tags?.domains ?? [],
977
- languages: family.tags?.languages ?? [],
978
- tasks: family.tags?.tasks ?? [],
979
- },
980
- standalone_benchmarks: standalone,
981
- }
982
- })
983
-
984
- if (raw.stats) {
985
- return { ...raw, families }
986
- }
987
-
988
- let composite_count = 0
989
- let standalone_benchmark_count = 0
990
- let single_benchmark_count = 0
991
- let slice_count = 0
992
- let metric_count = 0
993
-
994
- for (const family of families) {
995
- composite_count += family.composites?.length ?? 0
996
- const standalone = family.standalone_benchmarks ?? []
997
- standalone_benchmark_count += standalone.length
998
- if ((family.composites?.length ?? 0) === 0 && standalone.length === 1) {
999
- single_benchmark_count += 1
1000
- }
1001
- for (const composite of family.composites ?? []) {
1002
- for (const benchmark of composite.benchmarks ?? []) {
1003
- slice_count += benchmark.slices?.length ?? 0
1004
- metric_count += benchmark.metrics?.length ?? 0
1005
- }
1006
- }
1007
- for (const benchmark of standalone) {
1008
- slice_count += benchmark.slices?.length ?? 0
1009
- metric_count += benchmark.metrics?.length ?? 0
1010
- }
1011
- }
1012
-
1013
- return {
1014
- ...raw,
1015
- families,
1016
- stats: {
1017
- family_count: families.length,
1018
- composite_count,
1019
- standalone_benchmark_count,
1020
- single_benchmark_count,
1021
- slice_count,
1022
- metric_count,
1023
- metric_rows_scanned: 0,
1024
- },
1025
- }
1026
- }
1027
-
1028
  /**
1029
- * Translate the new composite/family/slice taxonomy shape (top-level
1030
- * `composites[]` + flat `families[]` lookup index) into the legacy
1031
- * `families[].composites[]` / `families[].standalone_benchmarks[]`
1032
- * tree.
1033
  *
1034
- * Bucketing rule: every benchmark in `composites[].benchmarks[]` is
1035
- * grouped by `family_id` (curated multi-benchmark family slug,
1036
- * defaulting to benchmark.key for singletons). A family with ≥2
1037
- * member benchmarks lands under a synthetic legacy `composites[]`
1038
- * entry keyed on the family slug; a singleton family lands under
1039
- * `standalone_benchmarks[]`. Slice rows on the new
1040
- * benchmark.slices[] carry through unchanged.
1041
  *
1042
- * Stats are taken straight from raw.stats (which has the new
1043
- * shape's `benchmark_count`) plus synthesised
1044
- * `standalone_benchmark_count` / `single_benchmark_count` for
1045
- * back-compat consumers.
1046
  */
1047
- function adaptCompositeShape(raw: EvalHierarchy): EvalHierarchy {
1048
- const familyIndex = new Map<string, { display_name: string; member_keys: string[] }>()
1049
- for (const fam of raw.families ?? []) {
1050
- if (!fam || typeof fam !== "object") continue
1051
- const f = fam as unknown as { key?: string; display_name?: string; member_benchmark_keys?: string[] }
1052
- if (!f.key) continue
1053
- familyIndex.set(f.key, {
1054
- display_name: f.display_name ?? f.key,
1055
- member_keys: f.member_benchmark_keys ?? [],
1056
- })
1057
- }
1058
-
1059
- type LegacyBenchmark = HierarchyBenchmark & { _composite_slug?: string }
1060
-
1061
- // Set of benchmark keys claimed by curated multi-benchmark families
1062
- // (families.yaml), so we don't redundantly bucket them under their
1063
- // composite below. Crucially this only counts MULTI-member families
1064
- // (≥2 benchmarks) — every benchmark also gets a synthetic singleton
1065
- // family from the backend's `_synthesise_singleton_families` pass,
1066
- // and treating those as curated would short-circuit the
1067
- // composite-implicit grouping (Pass B) and scatter every leaderboard
1068
- // benchmark into its own row.
1069
- const curatedMembers = new Set<string>()
1070
- for (const fam of familyIndex.values()) {
1071
- if (fam.member_keys.length < 2) continue
1072
- for (const k of fam.member_keys) curatedMembers.add(k)
1073
- }
1074
-
1075
- // Two grouping passes, in priority order:
1076
- //
1077
- // (1) Curated families from families.yaml — bucketed by `family_id`.
1078
- // Drives the MMLU family, BFCL family, JudgeBench family rows.
1079
- //
1080
- // (2) Composite-implicit groupings — for benchmarks NOT in any
1081
- // curated family, group by their composite_slug if the
1082
- // composite has ≥2 such benchmarks. This restores the legacy
1083
- // "HELM Classic / HELM Lite / HELM Safety" family rows that
1084
- // would otherwise scatter across one singleton family per
1085
- // leaf benchmark, hiding the leaderboard structure.
1086
- //
1087
- // (3) Anything still ungrouped lands as a singleton standalone.
1088
- type Bucket = {
1089
- key: string
1090
- display: string
1091
- benches: LegacyBenchmark[]
1092
- /** Curated family vs synthesised-from-composite vs singleton. Drives
1093
- * whether the legacy `composites[]` slot or `standalone_benchmarks[]`
1094
- * is populated. */
1095
- kind: "curated" | "composite" | "singleton"
1096
- }
1097
- const buckets = new Map<string, Bucket>()
1098
-
1099
- const toLegacyBenchmark = (
1100
- bench: HierarchyComposite["benchmarks"][number],
1101
- composite: HierarchyComposite,
1102
- ): LegacyBenchmark => ({
1103
- key: bench.key,
1104
- display_name: bench.display_name,
1105
- has_card: bench.has_card ?? false,
1106
- tags: {
1107
- domains: bench.tags?.domains ?? [],
1108
- languages: bench.tags?.languages ?? [],
1109
- tasks: bench.tags?.tasks ?? [],
1110
- },
1111
- slices: bench.slices ?? [],
1112
- metrics: bench.metrics ?? [],
1113
- summary_eval_ids: bench.summary_eval_ids,
1114
- reproducibility_summary: bench.reproducibility_summary,
1115
- provenance_summary: bench.provenance_summary,
1116
- comparability_summary: bench.comparability_summary,
1117
- family_id: bench.family_id ?? bench.key,
1118
- is_slice: bench.is_slice ?? false,
1119
- _composite_slug: composite.key,
1120
- })
1121
-
1122
- // Pass A: curated families.
1123
- for (const composite of raw.composites ?? []) {
1124
- for (const bench of composite.benchmarks ?? []) {
1125
- const familyId = bench.family_id ?? bench.key
1126
- if (!curatedMembers.has(bench.key)) continue
1127
- const entry = familyIndex.get(familyId)
1128
- if (!entry) continue
1129
- const bucketKey = `family:${familyId}`
1130
- if (!buckets.has(bucketKey)) {
1131
- buckets.set(bucketKey, {
1132
- key: familyId,
1133
- display: entry.display_name,
1134
- benches: [],
1135
- kind: "curated",
1136
- })
1137
- }
1138
- buckets.get(bucketKey)!.benches.push(toLegacyBenchmark(bench, composite))
1139
- }
1140
- }
1141
-
1142
- // Pass B: composite-implicit groupings. Per composite, count
1143
- // distinct non-curated benchmark keys; if ≥2, group them under the
1144
- // composite slug; otherwise fall through to the singleton pass.
1145
- for (const composite of raw.composites ?? []) {
1146
- const eligibleBenches = (composite.benchmarks ?? [])
1147
- .filter((b) => !curatedMembers.has(b.key))
1148
- const distinctKeys = new Set(eligibleBenches.map((b) => b.key))
1149
- if (distinctKeys.size < 2) continue
1150
- const bucketKey = `composite:${composite.key}`
1151
- if (!buckets.has(bucketKey)) {
1152
- buckets.set(bucketKey, {
1153
- key: composite.key,
1154
- display: composite.display_name,
1155
- benches: [],
1156
- kind: "composite",
1157
- })
1158
- }
1159
- for (const bench of eligibleBenches) {
1160
- buckets.get(bucketKey)!.benches.push(toLegacyBenchmark(bench, composite))
1161
- }
1162
- }
1163
-
1164
- // Pass C: singletons (benchmarks not in a curated family and whose
1165
- // composite carries only this benchmark). Bucketed by benchmark key.
1166
- for (const composite of raw.composites ?? []) {
1167
- const eligibleBenches = (composite.benchmarks ?? [])
1168
- .filter((b) => !curatedMembers.has(b.key))
1169
- const distinctKeys = new Set(eligibleBenches.map((b) => b.key))
1170
- if (distinctKeys.size >= 2) continue
1171
- for (const bench of eligibleBenches) {
1172
- const bucketKey = `bench:${bench.key}`
1173
- if (!buckets.has(bucketKey)) {
1174
- buckets.set(bucketKey, {
1175
- key: bench.key,
1176
- display: bench.display_name,
1177
- benches: [],
1178
- kind: "singleton",
1179
- })
1180
- }
1181
- buckets.get(bucketKey)!.benches.push(toLegacyBenchmark(bench, composite))
1182
- }
1183
- }
1184
-
1185
- // Synthesise legacy family records.
1186
- const families: HierarchyFamily[] = []
1187
- let standalone_benchmark_count = 0
1188
- let single_benchmark_count = 0
1189
- let synthesised_composite_count = 0
1190
-
1191
- // Sort by display name for stable rendering. Curated families first,
1192
- // then composite-implicit groupings, then singletons.
1193
- const sortedBuckets = Array.from(buckets.values()).sort((a, b) => {
1194
- const kindOrder = { curated: 0, composite: 1, singleton: 2 } as const
1195
- if (kindOrder[a.kind] !== kindOrder[b.kind]) {
1196
- return kindOrder[a.kind] - kindOrder[b.kind]
1197
- }
1198
- return a.key.localeCompare(b.key)
1199
- })
1200
-
1201
- for (const bucket of sortedBuckets) {
1202
- const benches = bucket.benches
1203
- if (benches.length === 0) continue
1204
- const display = bucket.display ?? bucket.key
1205
- const distinctBenchmarkKeys = new Set(benches.map((b) => b.key))
1206
-
1207
- const tagDomains = new Set<string>()
1208
- const tagLanguages = new Set<string>()
1209
- const tagTasks = new Set<string>()
1210
- for (const b of benches) {
1211
- b.tags.domains.forEach((d) => tagDomains.add(d))
1212
- b.tags.languages.forEach((l) => tagLanguages.add(l))
1213
- b.tags.tasks.forEach((t) => tagTasks.add(t))
1214
- }
1215
- const tags: HierarchyTags = {
1216
- domains: Array.from(tagDomains).sort(),
1217
- languages: Array.from(tagLanguages).sort(),
1218
- tasks: Array.from(tagTasks).sort(),
1219
- }
1220
-
1221
- const family: HierarchyFamily = {
1222
- key: bucket.key,
1223
- display_name: display,
1224
- category: "General",
1225
- tags,
1226
- has_card: benches.some((b) => b.has_card),
1227
- eval_summary_ids: Array.from(
1228
- new Set(benches.flatMap((b) => b.summary_eval_ids ?? [])),
1229
- ),
1230
- composites: [],
1231
- standalone_benchmarks: [],
1232
- }
1233
-
1234
- if (bucket.kind === "singleton") {
1235
- // Singleton family — flatten the (potentially N) per-composite
1236
- // copies of this benchmark into one standalone row by merging
1237
- // their slice/metric arrays.
1238
- const merged: HierarchyBenchmark = mergeSingletonBenchmarks(benches)
1239
- family.standalone_benchmarks!.push(merged)
1240
- standalone_benchmark_count += 1
1241
- single_benchmark_count += 1
1242
- } else {
1243
- // Multi-benchmark grouping (curated family or composite-implicit).
1244
- // Emit one legacy composite under the family. De-dup benchmarks
1245
- // that appear in multiple upstream composites.
1246
- const seen = new Set<string>()
1247
- const dedupedBenches: LegacyBenchmark[] = []
1248
- for (const b of benches) {
1249
- if (seen.has(b.key)) continue
1250
- seen.add(b.key)
1251
- dedupedBenches.push(b)
1252
- }
1253
- family.composites!.push({
1254
- key: bucket.key,
1255
- display_name: display,
1256
- has_card: family.has_card,
1257
- category: family.category,
1258
- tags,
1259
- benchmarks: dedupedBenches,
1260
- })
1261
- synthesised_composite_count += 1
1262
- }
1263
-
1264
- void distinctBenchmarkKeys
1265
- families.push(family)
1266
- }
1267
-
1268
- const stats = raw.stats
1269
- ? {
1270
- ...raw.stats,
1271
- standalone_benchmark_count:
1272
- raw.stats.standalone_benchmark_count ?? standalone_benchmark_count,
1273
- single_benchmark_count:
1274
- raw.stats.single_benchmark_count ?? single_benchmark_count,
1275
- }
1276
- : {
1277
- family_count: families.length,
1278
- composite_count: raw.composites?.length ?? synthesised_composite_count,
1279
- benchmark_count: 0,
1280
- standalone_benchmark_count,
1281
- single_benchmark_count,
1282
- slice_count: 0,
1283
- metric_count: 0,
1284
- metric_rows_scanned: 0,
1285
- }
1286
-
1287
- return {
1288
- ...raw,
1289
- families,
1290
- stats,
1291
- }
1292
- }
1293
-
1294
- function mergeSingletonBenchmarks(benches: HierarchyBenchmark[]): HierarchyBenchmark {
1295
- if (benches.length === 1) return benches[0]
1296
- const first = benches[0]
1297
- const sliceMap = new Map<string, HierarchySlice>()
1298
- const metricMap = new Map<string, HierarchyMetric>()
1299
- const summaryIds = new Set<string>()
1300
- for (const b of benches) {
1301
- for (const s of b.slices ?? []) {
1302
- if (!sliceMap.has(s.key)) sliceMap.set(s.key, s)
1303
- }
1304
- for (const m of b.metrics ?? []) {
1305
- if (!metricMap.has(m.key)) metricMap.set(m.key, m)
1306
- }
1307
- for (const id of b.summary_eval_ids ?? []) summaryIds.add(id)
1308
  }
1309
- return {
1310
- ...first,
1311
- slices: Array.from(sliceMap.values()),
1312
- metrics: Array.from(metricMap.values()),
1313
- summary_eval_ids: Array.from(summaryIds),
1314
  }
 
1315
  }
1316
 
1317
  export async function fetchComparisonIndex(): Promise<ComparisonIndex> {
@@ -1805,11 +1454,10 @@ function flattenHierarchyNode(
1805
  ? `${context.benchmark_parent_name ?? context.benchmark} / ${metric.slice_name}`
1806
  : context.canonical_display_name ?? context.benchmark),
1807
  category,
1808
- benchmark_family_key: context.benchmark_family_key,
1809
  benchmark_family_name: context.benchmark_family_name,
1810
- benchmark_parent_key: context.benchmark_parent_key,
1811
  benchmark_parent_name: context.benchmark_parent_name,
1812
- benchmark_leaf_key: metric.benchmark_leaf_key ?? context.benchmark_leaf_key,
1813
  benchmark_leaf_name: metric.benchmark_leaf_name ?? context.benchmark_leaf_name,
1814
  slice_key: sliceKey,
1815
  slice_name: sliceName,
 
932
  * back under per-family records, with the composite slug as the
933
  * family key when no curated multi-benchmark family applies.
934
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
935
  /**
936
+ * Validate-and-passthrough for the v3 hierarchy shape (per
937
+ * /Users/jchim/projects/evaleval/notes/hierarchy-alignment.md §5.1).
938
+ * The producer's `write_hierarchy()` emits family-rooted trees
939
+ * directly; the adapter no longer synthesises legacy shapes.
940
  *
941
+ * Behaviour:
942
+ * - v3 detection via `schema_version === "v3.hierarchy.1"`
943
+ * pass through unchanged.
944
+ * - Older snapshot lacking schema_version: log a warning and
945
+ * pass through. Consumers may render empty for missing fields
946
+ * but won't crash.
 
947
  *
948
+ * Step 4 deleted the legacy `adaptCompositeShape()` synthesis
949
+ * (~400 lines of bucketing logic that built families[].composites[]
950
+ * from the old top-level composites[]). The producer now does that
951
+ * grouping at write time using `canonical_composites.family_id`.
952
  */
953
+ export function adaptEvalHierarchy(raw: EvalHierarchy): EvalHierarchy {
954
+ if (!raw) {
955
+ return { families: [] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
956
  }
957
+ if (raw.schema_version && !raw.schema_version.startsWith("v3.hierarchy.")) {
958
+ console.warn(
959
+ `adaptEvalHierarchy: unexpected schema_version=${JSON.stringify(raw.schema_version)}; ` +
960
+ `expected v3.hierarchy.*. Frontend may render incompletely.`,
961
+ )
962
  }
963
+ return raw
964
  }
965
 
966
  export async function fetchComparisonIndex(): Promise<ComparisonIndex> {
 
1454
  ? `${context.benchmark_parent_name ?? context.benchmark} / ${metric.slice_name}`
1455
  : context.canonical_display_name ?? context.benchmark),
1456
  category,
1457
+ family_id: context.benchmark_family_key,
1458
  benchmark_family_name: context.benchmark_family_name,
1459
+ parent_benchmark_id: context.benchmark_parent_key,
1460
  benchmark_parent_name: context.benchmark_parent_name,
 
1461
  benchmark_leaf_name: metric.benchmark_leaf_name ?? context.benchmark_leaf_name,
1462
  slice_key: sliceKey,
1463
  slice_name: sliceName,
lib/model-data.ts CHANGED
@@ -493,8 +493,8 @@ export function hfEvalEntryToListItem(entry: HFEvalListEntry): BenchmarkEvalList
493
  metrics_count: entry.metrics_count,
494
  metric_names: entry.metric_names,
495
  instance_data: entry.instance_data,
496
- benchmark_family_key: entry.benchmark_family_key,
497
- benchmark_leaf_key: entry.benchmark_leaf_key,
498
  source_data: entry.source_data,
499
  top_score: entry.top_score,
500
  subtasks_count: entry.subtasks_count ?? 0,
 
493
  metrics_count: entry.metrics_count,
494
  metric_names: entry.metric_names,
495
  instance_data: entry.instance_data,
496
+ family_id: entry.benchmark_family_key,
497
+ parent_benchmark_id: entry.benchmark_parent_key,
498
  source_data: entry.source_data,
499
  top_score: entry.top_score,
500
  subtasks_count: entry.subtasks_count ?? 0,
lib/sidecars.ts CHANGED
@@ -57,7 +57,28 @@ export function fetchHierarchy(): Promise<EvalHierarchy> {
57
  }
58
 
59
  export function fetchComparisonIndex(): Promise<ComparisonIndex> {
60
- return (cache.comparisonIndex ??= fetchJson<ComparisonIndex>("comparison-index.json"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
  export function resetSidecarCacheForTests() {
 
57
  }
58
 
59
  export function fetchComparisonIndex(): Promise<ComparisonIndex> {
60
+ return (cache.comparisonIndex ??= fetchJson<ComparisonIndex>("comparison-index.json").then(
61
+ (index) => {
62
+ assertComparisonIndexShape(index)
63
+ return index
64
+ },
65
+ ))
66
+ }
67
+
68
+ /**
69
+ * Fail fast on contract regressions. Comparison-index rows must carry
70
+ * `family_id`; the model-page graph view collapses without it. See
71
+ * `notes/hierarchy-alignment.md` §5.2.
72
+ */
73
+ export function assertComparisonIndexShape(index: ComparisonIndex): void {
74
+ for (const [evalId, entry] of Object.entries(index.evals ?? {})) {
75
+ if (!Object.prototype.hasOwnProperty.call(entry, "family_id")) {
76
+ throw new Error(
77
+ `comparison-index contract regression: evals[${evalId}] is missing family_id. ` +
78
+ `See notes/hierarchy-alignment.md §5.2.`,
79
+ )
80
+ }
81
+ }
82
  }
83
 
84
  export function resetSidecarCacheForTests() {
lib/view-data.ts CHANGED
@@ -44,28 +44,23 @@ const MODEL_CARD_COLUMNS = `
44
 
45
  // The composite/family/slice taxonomy refactor (eval_card_backend
46
  // notes/09-) replaced the legacy `composite_benchmark_key` /
47
- // `composite_benchmark_name` / `benchmark_family_key` /
48
- // `benchmark_leaf_key` columns with `composite_slug` /
49
- // `composite_display_name` / `family_id` / `family_display_name` /
50
- // `is_slice`. We expose both names so existing consumers keep
51
- // reading without rewrites. Mapping:
52
  // composite_benchmark_key/name → composite_slug/display_name
53
  // (the leaderboard, e.g. "wasp"/"WASP" — what the eval-detail
54
  // "Composite" label shows)
55
- // benchmark_family_key/name → family_id/family_display_name
56
- // (curated multi-benchmark family, e.g. "judgebench"/"JudgeBench
57
- // family" — drives the family-table grouping in the legacy
58
- // hierarchy adapter)
59
  const EVAL_LIST_COLUMNS = `
60
  evaluation_id, evaluation_name, canonical_display_name,
61
  benchmark_id,
62
  composite_slug, composite_display_name,
63
  family_id, family_display_name, is_slice,
 
64
  composite_slug AS composite_benchmark_key,
65
  composite_display_name AS composite_benchmark_name,
66
- family_id AS benchmark_family_key,
67
  family_display_name AS benchmark_family_name,
68
- CASE WHEN is_slice THEN benchmark_id ELSE NULL END AS benchmark_leaf_key,
69
  category,
70
  metric_config, models_count, evaluator_names, source_types,
71
  latest_source_name, third_party_ratio,
@@ -89,11 +84,10 @@ const CELL_JOIN_COLUMNS = `
89
  e.family_id AS eval_family_id,
90
  e.family_display_name AS eval_family_display_name,
91
  e.is_slice AS eval_is_slice,
 
92
  e.composite_slug AS eval_composite_benchmark_key,
93
  e.composite_display_name AS eval_composite_benchmark_name,
94
- e.family_id AS eval_benchmark_family_key,
95
  e.family_display_name AS eval_benchmark_family_name,
96
- CASE WHEN e.is_slice THEN e.benchmark_id ELSE NULL END AS eval_benchmark_leaf_key,
97
  e.category AS eval_category,
98
  e.metric_config AS eval_metric_config,
99
  e.source_data AS eval_source_data,
@@ -350,12 +344,12 @@ function reshapeCellToBenchmarkEvaluation(row: Row): BenchmarkEvaluation {
350
  display_name: optionalString(row.eval_evaluation_name),
351
  canonical_display_name: optionalString(row.eval_canonical_display_name),
352
  category: normalizeCategory(row.eval_category ?? row.category),
353
- benchmark_family_key: optionalString(row.eval_benchmark_family_key),
354
- benchmark_family_name: optionalString(row.eval_composite_benchmark_name),
355
- benchmark_parent_key: optionalString(row.eval_composite_benchmark_key),
356
  benchmark_parent_name: optionalString(row.eval_composite_benchmark_name),
357
- benchmark_leaf_key: optionalString(row.eval_benchmark_leaf_key),
358
  benchmark_leaf_name: optionalString(row.eval_evaluation_name),
 
359
  is_summary_score: Boolean(row.eval_is_summary_score ?? row.is_summary_score),
360
  source_data: sourceDataFromRow(row),
361
  source_metadata: sourceMetadataFromRow(row),
 
44
 
45
  // The composite/family/slice taxonomy refactor (eval_card_backend
46
  // notes/09-) replaced the legacy `composite_benchmark_key` /
47
+ // `composite_benchmark_name` columns with `composite_slug` /
48
+ // `composite_display_name`. The `family_id` / `family_display_name` /
49
+ // `is_slice` columns are the canonical identity surface; we still
50
+ // alias the composite_* legacy names for backward compat with
51
+ // consumers that haven't migrated yet. Mapping:
52
  // composite_benchmark_key/name → composite_slug/display_name
53
  // (the leaderboard, e.g. "wasp"/"WASP" — what the eval-detail
54
  // "Composite" label shows)
 
 
 
 
55
  const EVAL_LIST_COLUMNS = `
56
  evaluation_id, evaluation_name, canonical_display_name,
57
  benchmark_id,
58
  composite_slug, composite_display_name,
59
  family_id, family_display_name, is_slice,
60
+ parent_benchmark_id,
61
  composite_slug AS composite_benchmark_key,
62
  composite_display_name AS composite_benchmark_name,
 
63
  family_display_name AS benchmark_family_name,
 
64
  category,
65
  metric_config, models_count, evaluator_names, source_types,
66
  latest_source_name, third_party_ratio,
 
84
  e.family_id AS eval_family_id,
85
  e.family_display_name AS eval_family_display_name,
86
  e.is_slice AS eval_is_slice,
87
+ e.parent_benchmark_id AS eval_parent_benchmark_id,
88
  e.composite_slug AS eval_composite_benchmark_key,
89
  e.composite_display_name AS eval_composite_benchmark_name,
 
90
  e.family_display_name AS eval_benchmark_family_name,
 
91
  e.category AS eval_category,
92
  e.metric_config AS eval_metric_config,
93
  e.source_data AS eval_source_data,
 
344
  display_name: optionalString(row.eval_evaluation_name),
345
  canonical_display_name: optionalString(row.eval_canonical_display_name),
346
  category: normalizeCategory(row.eval_category ?? row.category),
347
+ family_id: optionalString(row.eval_family_id),
348
+ benchmark_family_name: optionalString(row.eval_family_display_name),
349
+ parent_benchmark_id: optionalString(row.eval_parent_benchmark_id),
350
  benchmark_parent_name: optionalString(row.eval_composite_benchmark_name),
 
351
  benchmark_leaf_name: optionalString(row.eval_evaluation_name),
352
+ is_slice: Boolean(row.eval_is_slice),
353
  is_summary_score: Boolean(row.eval_is_summary_score ?? row.is_summary_score),
354
  source_data: sourceDataFromRow(row),
355
  source_metadata: sourceMetadataFromRow(row),
tests/__snapshots__/adapters.test.ts.snap CHANGED
The diff for this file is too large to render. See raw diff
 
tests/adapters.test.ts CHANGED
@@ -55,7 +55,7 @@ describe("flattenModelEvaluations", () => {
55
  const evaluations = flattenModelEvaluations(input)
56
  // Snapshot a digest rather than the full output (which can be 10k+ lines
57
  // for large models). The digest captures: count, distinct categories,
58
- // distinct evaluator_relationships, count of distinct benchmark_family_keys,
59
  // count of unique evaluation_ids, and a hash of the full output. Any change
60
  // to the full output changes the hash; the structured fields make the diff
61
  // readable when something changes.
@@ -92,7 +92,7 @@ function digestEvaluations(evaluations: BenchmarkEvaluation[]) {
92
  let missingSourceMetadata = 0
93
  for (const e of evaluations) {
94
  if (e.category) categories.add(e.category)
95
- if (e.benchmark_family_key) families.add(e.benchmark_family_key)
96
  if (e.source_metadata?.evaluator_relationship) evaluators.add(e.source_metadata.evaluator_relationship)
97
  if (e.evaluation_id) evaluationIds.add(e.evaluation_id)
98
  if (!e.source_metadata) missingSourceMetadata += 1
@@ -101,7 +101,7 @@ function digestEvaluations(evaluations: BenchmarkEvaluation[]) {
101
  count: evaluations.length,
102
  distinct_evaluation_ids: evaluationIds.size,
103
  distinct_categories: [...categories].sort(),
104
- distinct_benchmark_family_keys: families.size,
105
  distinct_evaluator_relationships: [...evaluators].sort(),
106
  missing_source_metadata: missingSourceMetadata,
107
  full_output_hash: stableHash(evaluations),
 
55
  const evaluations = flattenModelEvaluations(input)
56
  // Snapshot a digest rather than the full output (which can be 10k+ lines
57
  // for large models). The digest captures: count, distinct categories,
58
+ // distinct evaluator_relationships, count of distinct family_ids,
59
  // count of unique evaluation_ids, and a hash of the full output. Any change
60
  // to the full output changes the hash; the structured fields make the diff
61
  // readable when something changes.
 
92
  let missingSourceMetadata = 0
93
  for (const e of evaluations) {
94
  if (e.category) categories.add(e.category)
95
+ if (e.family_id) families.add(e.family_id)
96
  if (e.source_metadata?.evaluator_relationship) evaluators.add(e.source_metadata.evaluator_relationship)
97
  if (e.evaluation_id) evaluationIds.add(e.evaluation_id)
98
  if (!e.source_metadata) missingSourceMetadata += 1
 
101
  count: evaluations.length,
102
  distinct_evaluation_ids: evaluationIds.size,
103
  distinct_categories: [...categories].sort(),
104
+ distinct_family_ids: families.size,
105
  distinct_evaluator_relationships: [...evaluators].sort(),
106
  missing_source_metadata: missingSourceMetadata,
107
  full_output_hash: stableHash(evaluations),
tests/comparison-index-loader.test.ts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { mkdir, mkdtemp, rm, writeFile } from "fs/promises"
2
+ import os from "os"
3
+ import path from "path"
4
+
5
+ import { describe, expect, it } from "vitest"
6
+
7
+ import type { ComparisonIndex } from "../lib/backend-artifacts"
8
+ import { assertComparisonIndexShape } from "../lib/sidecars"
9
+
10
+ // Shared fixture: a comparison-index that mirrors the post-migration
11
+ // shape from notes/hierarchy-alignment.md §5.2. Tests mutate copies of
12
+ // this to exercise contract assertions.
13
+ const validFixture: ComparisonIndex = {
14
+ generated_at: "2026-05-04T00:00:00Z",
15
+ config_version: 2,
16
+ metric_group_order: ["capability", "robustness", "efficiency", "cost", "latency", "rank", "other"],
17
+ evals: {
18
+ "helm-classic%2Fmmlu": {
19
+ eval_summary_id: "helm-classic%2Fmmlu",
20
+ benchmark_id: "mmlu",
21
+ family_id: "helm",
22
+ family_display_name: "HELM",
23
+ composite_slug: "helm-classic",
24
+ composite_display_name: "HELM Classic",
25
+ parent_benchmark_id: null,
26
+ display_name: "MMLU",
27
+ category: "Knowledge",
28
+ is_slice: false,
29
+ is_summary_score: false,
30
+ summary_score_for: null,
31
+ summary_eval_ids: [],
32
+ metrics: [],
33
+ },
34
+ "helm-classic%2Fmmlu%2Fanatomy": {
35
+ eval_summary_id: "helm-classic%2Fmmlu%2Fanatomy",
36
+ benchmark_id: "mmlu",
37
+ family_id: "helm",
38
+ family_display_name: "HELM",
39
+ composite_slug: "helm-classic",
40
+ composite_display_name: "HELM Classic",
41
+ parent_benchmark_id: "mmlu",
42
+ display_name: "MMLU Anatomy",
43
+ category: "Knowledge",
44
+ is_slice: true,
45
+ is_summary_score: false,
46
+ summary_score_for: null,
47
+ summary_eval_ids: [],
48
+ metrics: [],
49
+ },
50
+ },
51
+ by_model: {},
52
+ }
53
+
54
+ describe("assertComparisonIndexShape", () => {
55
+ it("accepts entries with family_id present", () => {
56
+ expect(() => assertComparisonIndexShape(validFixture)).not.toThrow()
57
+ })
58
+
59
+ it("accepts the new parent_benchmark_id field on slice entries", () => {
60
+ const sliceEntry = validFixture.evals["helm-classic%2Fmmlu%2Fanatomy"]
61
+ expect(sliceEntry.parent_benchmark_id).toBe("mmlu")
62
+ expect(sliceEntry.is_slice).toBe(true)
63
+ })
64
+
65
+ it("rejects entries that are missing family_id", () => {
66
+ const broken = JSON.parse(JSON.stringify(validFixture)) as ComparisonIndex
67
+ delete (broken.evals["helm-classic%2Fmmlu"] as unknown as Record<string, unknown>).family_id
68
+
69
+ expect(() => assertComparisonIndexShape(broken)).toThrow(
70
+ /missing family_id/,
71
+ )
72
+ })
73
+ })
74
+
75
+ describe("fetchComparisonIndex (v2 sidecar)", () => {
76
+ it("validates the loaded sidecar against the contract", async () => {
77
+ const snapshotDir = await mkdtemp(path.join(os.tmpdir(), "eval-card-cmp-"))
78
+ const previousBackend = process.env.DATA_BACKEND
79
+ const previousSnapshotUrl = process.env.SNAPSHOT_URL
80
+
81
+ try {
82
+ await mkdir(snapshotDir, { recursive: true })
83
+ const broken = JSON.parse(JSON.stringify(validFixture)) as ComparisonIndex
84
+ delete (broken.evals["helm-classic%2Fmmlu"] as unknown as Record<string, unknown>).family_id
85
+ await writeFile(
86
+ path.join(snapshotDir, "comparison-index.json"),
87
+ JSON.stringify(broken),
88
+ )
89
+
90
+ process.env.DATA_BACKEND = "v2"
91
+ process.env.SNAPSHOT_URL = `file://${snapshotDir}`
92
+
93
+ const sidecars = await import("../lib/sidecars")
94
+ sidecars.resetSidecarCacheForTests()
95
+
96
+ await expect(sidecars.fetchComparisonIndex()).rejects.toThrow(
97
+ /missing family_id/,
98
+ )
99
+ } finally {
100
+ const sidecars = await import("../lib/sidecars")
101
+ sidecars.resetSidecarCacheForTests()
102
+ if (previousBackend == null) {
103
+ delete process.env.DATA_BACKEND
104
+ } else {
105
+ process.env.DATA_BACKEND = previousBackend
106
+ }
107
+ if (previousSnapshotUrl == null) {
108
+ delete process.env.SNAPSHOT_URL
109
+ } else {
110
+ process.env.SNAPSHOT_URL = previousSnapshotUrl
111
+ }
112
+ await rm(snapshotDir, { recursive: true, force: true })
113
+ }
114
+ })
115
+ })
tests/eval-hierarchy-adapter.test.ts CHANGED
@@ -1,11 +1,19 @@
1
- import { describe, expect, it } from "vitest"
2
 
3
  import type { EvalHierarchy } from "../lib/backend-artifacts"
4
  import { adaptEvalHierarchy } from "../lib/hf-data"
5
 
6
- describe("adaptEvalHierarchy", () => {
7
- it("keeps GPQA-Diamond as a benchmark sibling in the curated GPQA family", () => {
8
- const raw = {
 
 
 
 
 
 
 
 
9
  stats: {
10
  family_count: 1,
11
  composite_count: 1,
@@ -18,22 +26,19 @@ describe("adaptEvalHierarchy", () => {
18
  {
19
  key: "gpqa",
20
  display_name: "GPQA family",
21
- member_benchmark_keys: ["gpqa", "gpqa-diamond"],
22
- },
23
- ],
24
- composites: [
25
- {
26
- key: "wasp",
27
- display_name: "WASP",
28
- category: "Reasoning",
29
  tags: { domains: ["reasoning"], languages: [], tasks: ["qa"] },
 
 
30
  benchmarks: [
31
  {
32
  key: "gpqa",
33
  display_name: "GPQA",
34
- has_card: false,
35
  family_id: "gpqa",
36
  is_slice: false,
 
 
 
37
  tags: { domains: ["reasoning"], languages: [], tasks: ["qa"] },
38
  metrics: [{ key: "accuracy", display_name: "Accuracy" }],
39
  slices: [],
@@ -42,9 +47,11 @@ describe("adaptEvalHierarchy", () => {
42
  {
43
  key: "gpqa-diamond",
44
  display_name: "GPQA Diamond",
45
- has_card: false,
46
  family_id: "gpqa",
47
  is_slice: false,
 
 
 
48
  tags: { domains: ["reasoning"], languages: [], tasks: ["qa"] },
49
  metrics: [{ key: "accuracy", display_name: "Accuracy" }],
50
  slices: [],
@@ -53,22 +60,44 @@ describe("adaptEvalHierarchy", () => {
53
  ],
54
  },
55
  ],
56
- } as unknown as EvalHierarchy
57
 
58
  const adapted = adaptEvalHierarchy(raw)
59
- const gpqa = adapted.families.find((family) => family.key === "gpqa")
60
 
61
- expect(gpqa).toBeDefined()
62
- expect(gpqa?.standalone_benchmarks).toEqual([])
63
- expect(gpqa?.composites).toHaveLength(1)
64
- expect(gpqa?.composites?.[0].benchmarks.map((benchmark) => benchmark.key)).toEqual([
65
  "gpqa",
66
  "gpqa-diamond",
67
  ])
68
- expect(
69
- gpqa?.composites?.[0].benchmarks.flatMap((benchmark) =>
70
- benchmark.slices.map((slice) => slice.key),
71
- ),
72
- ).not.toContain("gpqa-diamond")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  })
74
  })
 
1
+ import { describe, expect, it, vi } from "vitest"
2
 
3
  import type { EvalHierarchy } from "../lib/backend-artifacts"
4
  import { adaptEvalHierarchy } from "../lib/hf-data"
5
 
6
+ // adaptEvalHierarchy is a passthrough validator post-Step-4 — the
7
+ // producer's write_hierarchy() emits the v3 family-rooted tree
8
+ // directly (notes/hierarchy-alignment.md §5.1), so the adapter no
9
+ // longer synthesises legacy shapes. These tests confirm the
10
+ // passthrough preserves data and that the schema_version warning
11
+ // fires for unknown versions.
12
+
13
+ describe("adaptEvalHierarchy (passthrough)", () => {
14
+ it("returns the v3 hierarchy unchanged", () => {
15
+ const raw: EvalHierarchy = {
16
+ schema_version: "v3.hierarchy.1",
17
  stats: {
18
  family_count: 1,
19
  composite_count: 1,
 
26
  {
27
  key: "gpqa",
28
  display_name: "GPQA family",
29
+ category: "knowledge",
 
 
 
 
 
 
 
30
  tags: { domains: ["reasoning"], languages: [], tasks: ["qa"] },
31
+ evals_count: 4,
32
+ eval_summary_ids: ["wasp%2Fgpqa", "wasp%2Fgpqa-diamond"],
33
  benchmarks: [
34
  {
35
  key: "gpqa",
36
  display_name: "GPQA",
 
37
  family_id: "gpqa",
38
  is_slice: false,
39
+ is_overall: true,
40
+ is_primary: true,
41
+ has_card: false,
42
  tags: { domains: ["reasoning"], languages: [], tasks: ["qa"] },
43
  metrics: [{ key: "accuracy", display_name: "Accuracy" }],
44
  slices: [],
 
47
  {
48
  key: "gpqa-diamond",
49
  display_name: "GPQA Diamond",
 
50
  family_id: "gpqa",
51
  is_slice: false,
52
+ is_overall: false,
53
+ is_primary: false,
54
+ has_card: false,
55
  tags: { domains: ["reasoning"], languages: [], tasks: ["qa"] },
56
  metrics: [{ key: "accuracy", display_name: "Accuracy" }],
57
  slices: [],
 
60
  ],
61
  },
62
  ],
63
+ }
64
 
65
  const adapted = adaptEvalHierarchy(raw)
 
66
 
67
+ expect(adapted).toBe(raw) // passthrough: same reference
68
+ expect(adapted.families).toHaveLength(1)
69
+ expect(adapted.families[0].benchmarks).toHaveLength(2)
70
+ expect(adapted.families[0].benchmarks?.map((b) => b.key)).toEqual([
71
  "gpqa",
72
  "gpqa-diamond",
73
  ])
74
+ })
75
+
76
+ it("returns a safe empty shape on null/undefined input", () => {
77
+ expect(adaptEvalHierarchy(null as unknown as EvalHierarchy)).toEqual({
78
+ families: [],
79
+ })
80
+ })
81
+
82
+ it("warns on unknown schema_version but still passes through", () => {
83
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
84
+ const raw: EvalHierarchy = {
85
+ schema_version: "v2.hierarchy.999",
86
+ families: [],
87
+ }
88
+ const adapted = adaptEvalHierarchy(raw)
89
+ expect(adapted).toBe(raw)
90
+ expect(warn).toHaveBeenCalledOnce()
91
+ warn.mockRestore()
92
+ })
93
+
94
+ it("does not warn when schema_version matches v3.hierarchy.*", () => {
95
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
96
+ adaptEvalHierarchy({
97
+ schema_version: "v3.hierarchy.1",
98
+ families: [],
99
+ })
100
+ expect(warn).not.toHaveBeenCalled()
101
+ warn.mockRestore()
102
  })
103
  })
tests/view-data.test.ts CHANGED
@@ -109,8 +109,12 @@ async function writeSyntheticStageJSnapshot(snapshotDir: string) {
109
  'MMLU' AS canonical_display_name,
110
  'mmlu' AS composite_benchmark_key,
111
  'MMLU' AS composite_benchmark_name,
112
- 'mmlu' AS benchmark_family_key,
113
- 'mmlu' AS benchmark_leaf_key,
 
 
 
 
114
  'Reasoning' AS category,
115
  struct_pack(
116
  evaluation_description := 'Accuracy on MMLU',
 
109
  'MMLU' AS canonical_display_name,
110
  'mmlu' AS composite_benchmark_key,
111
  'MMLU' AS composite_benchmark_name,
112
+ 'mmlu' AS composite_slug,
113
+ 'MMLU' AS composite_display_name,
114
+ 'mmlu' AS family_id,
115
+ 'MMLU' AS family_display_name,
116
+ false AS is_slice,
117
+ NULL AS parent_benchmark_id,
118
  'Reasoning' AS category,
119
  struct_pack(
120
  evaluation_description := 'Accuracy on MMLU',