evijit HF Staff commited on
Commit
9b4cdbb
·
1 Parent(s): bca888a

Separate policy and researcher views

Browse files

- Foundations: glossary + Term + ModeSwitch helpers
- Policy: plain-language summary, scope chips, provenance row,
collapsible subtasks, technical details pushed into expander
- Researcher: per-row Reproducibility card with adaptive compact mode
for limited-disclosure rows; agent setup group for agentic evals;
lazy-fetched generation_config + score_details from model files
- Replace boilerplate IBM risk field with curated known-issues registry
- Apples-to-apples banner above leaderboards, anchored to
Comparability panel
- Methodology split: surface task setup, scoring, validation
- Compact row-signals indicator replacing repetitive coloured badges
- Cleaner divergence chips in Comparability panel; full-width when
only one divergence type fires
- Flag-this-score button that links to the dataset record on HF and
pre-fills a discussion title plus copyable context
- Defensive guard so an embedded ancestor card no longer leaks its
text onto a leaf-benchmark page

app/api/eval-row-config/route.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { fetchModelDetail } from "@/lib/hf-data"
4
+
5
+ /**
6
+ * Lightweight lookup that returns the per-(model, benchmark) reproducibility
7
+ * payload — generation_config + sample_size/standard_error/confidence_interval
8
+ * — extracted from the model's full record. Used by the leaderboard's
9
+ * Reproducibility card on row expand so we avoid joining N model files into
10
+ * the eval-detail page on every load.
11
+ */
12
+ export async function GET(request: Request) {
13
+ const { searchParams } = new URL(request.url)
14
+ const modelId = searchParams.get("model_id")
15
+ const benchmarkKey = (searchParams.get("benchmark_key") || "").toLowerCase()
16
+ const evalName = (searchParams.get("eval_name") || "").toLowerCase()
17
+
18
+ if (!modelId) {
19
+ return NextResponse.json({ error: "Missing model_id" }, { status: 400 })
20
+ }
21
+
22
+ const slug = modelId.replace(/[\/]/g, "__")
23
+ const detail = await fetchModelDetail(slug)
24
+ if (!detail) {
25
+ return NextResponse.json({ generation_config: null, score_details: null }, { status: 200 })
26
+ }
27
+
28
+ let bestGenerationConfig: unknown = null
29
+ let bestScoreDetails: unknown = null
30
+
31
+ const candidates = Object.values(detail.evaluations_by_category ?? {}).flat()
32
+ for (const ev of candidates) {
33
+ const evBench = ((ev.benchmark as string | undefined) ?? "").toLowerCase()
34
+ for (const r of ev.evaluation_results ?? []) {
35
+ const rName = (r.evaluation_name ?? "").toLowerCase()
36
+ const rDisplay = (r.display_name ?? "").toLowerCase()
37
+ const matches =
38
+ (benchmarkKey && (evBench === benchmarkKey || evBench.includes(benchmarkKey) || rName.includes(benchmarkKey))) ||
39
+ (evalName && (rName === evalName || rDisplay === evalName))
40
+ if (!matches) continue
41
+
42
+ const candidateGen = ev.generation_config ?? r.generation_config
43
+ const argsCount =
44
+ candidateGen && typeof candidateGen === "object" && "generation_args" in candidateGen
45
+ ? Object.keys((candidateGen as { generation_args?: Record<string, unknown> }).generation_args ?? {}).length
46
+ : 0
47
+ const currentCount =
48
+ bestGenerationConfig && typeof bestGenerationConfig === "object" && "generation_args" in bestGenerationConfig
49
+ ? Object.keys((bestGenerationConfig as { generation_args?: Record<string, unknown> }).generation_args ?? {}).length
50
+ : 0
51
+ if (candidateGen && argsCount > currentCount) {
52
+ bestGenerationConfig = candidateGen
53
+ }
54
+ if (!bestScoreDetails && r.score_details) {
55
+ bestScoreDetails = r.score_details
56
+ }
57
+ }
58
+ if (bestGenerationConfig && bestScoreDetails) break
59
+ }
60
+
61
+ return NextResponse.json({
62
+ generation_config: bestGenerationConfig,
63
+ score_details: bestScoreDetails,
64
+ })
65
+ }
components/apples-to-apples-banner.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { AlertTriangle, GitCompareArrows, UsersRound } from "lucide-react"
4
+ import { useAudienceMode } from "@/components/audience-mode-provider"
5
+ import { SignalTooltip } from "@/components/signals/signal-tooltip"
6
+ import type { ComparabilitySummary } from "@/lib/backend-artifacts"
7
+
8
+ interface ApplesToApplesBannerProps {
9
+ summary?: ComparabilitySummary | null
10
+ /**
11
+ * Optional anchor id so the "see details" link can scroll to the full
12
+ * Comparability panel further down the page.
13
+ */
14
+ detailsAnchorId?: string
15
+ }
16
+
17
+ /**
18
+ * High-visibility warning shown above the leaderboard when the row-level
19
+ * comparability summary indicates that some scores were collected under
20
+ * different setups (variant divergence) or only by the model developer
21
+ * (cross-party divergence). Designed to interrupt naive ranking comparisons
22
+ * before the reader scrolls to the per-row signals.
23
+ */
24
+ export function ApplesToApplesBanner({ summary, detailsAnchorId }: ApplesToApplesBannerProps) {
25
+ const { mode } = useAudienceMode()
26
+
27
+ if (!summary) return null
28
+
29
+ const variantCount = summary.variant_divergent_count ?? 0
30
+ const crossPartyCount = summary.cross_party_divergent_count ?? 0
31
+ const totalConcerns = variantCount + crossPartyCount
32
+ if (totalConcerns === 0) return null
33
+
34
+ const variantsChecked = summary.groups_with_variant_check ?? 0
35
+ const crossPartyChecked = summary.groups_with_cross_party_check ?? 0
36
+
37
+ const concernPhrases: string[] = []
38
+ if (variantCount > 0) {
39
+ concernPhrases.push(
40
+ `${variantCount} group${variantCount === 1 ? "" : "s"} where models used different setups`,
41
+ )
42
+ }
43
+ if (crossPartyCount > 0) {
44
+ concernPhrases.push(
45
+ `${crossPartyCount} group${crossPartyCount === 1 ? "" : "s"} where reports come only from the model developer`,
46
+ )
47
+ }
48
+
49
+ const headline =
50
+ mode === "policy"
51
+ ? "Heads up: not every score here is directly comparable."
52
+ : "Apples-to-apples warning"
53
+
54
+ const body =
55
+ mode === "policy"
56
+ ? `${concernPhrases.join(" and ")}. Direct ranking comparisons may be misleading.`
57
+ : `${concernPhrases.join("; ")}. See the Comparability panel below for which models and which fields differ.`
58
+
59
+ return (
60
+ <div className="flex items-start gap-3 rounded-2xl border border-amber-300/70 bg-amber-50/70 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/25 dark:text-amber-100">
61
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
62
+ <div className="min-w-0 flex-1 space-y-1">
63
+ <div className="font-semibold leading-tight">{headline}</div>
64
+ <p className="text-[13px] leading-5">{body}</p>
65
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] opacity-90">
66
+ {variantCount > 0 && (
67
+ <SignalTooltip
68
+ content={`${variantCount} of ${variantsChecked} groups checked for setup divergence flagged a problem (e.g. different shots, prompts, or scoring).`}
69
+ >
70
+ <span className="inline-flex items-center gap-1 cursor-help">
71
+ <GitCompareArrows className="h-3 w-3" />
72
+ Setup divergence: {variantCount}/{variantsChecked || variantCount}
73
+ </span>
74
+ </SignalTooltip>
75
+ )}
76
+ {crossPartyCount > 0 && (
77
+ <SignalTooltip
78
+ content={`${crossPartyCount} of ${crossPartyChecked} groups checked for source divergence flagged a problem (only the model developer reported a number).`}
79
+ >
80
+ <span className="inline-flex items-center gap-1 cursor-help">
81
+ <UsersRound className="h-3 w-3" />
82
+ Source divergence: {crossPartyCount}/{crossPartyChecked || crossPartyCount}
83
+ </span>
84
+ </SignalTooltip>
85
+ )}
86
+ {detailsAnchorId && (
87
+ <a
88
+ href={`#${detailsAnchorId}`}
89
+ className="ml-auto font-medium underline-offset-4 hover:underline"
90
+ >
91
+ See details ↓
92
+ </a>
93
+ )}
94
+ </div>
95
+ </div>
96
+ </div>
97
+ )
98
+ }
components/eval-detail.tsx CHANGED
@@ -9,6 +9,7 @@ import { CompletenessPanel } from "@/components/signals/completeness-panel"
9
  import { ComparabilityPanel } from "@/components/signals/comparability-panel"
10
  import { ReproducibilityPanel } from "@/components/signals/reproducibility-panel"
11
  import { SignalsRowBadges } from "@/components/signals/signals-row-badges"
 
12
  import { SignalTooltip } from "@/components/signals/signal-tooltip"
13
  import { getCompletenessPopulatedCount } from "@/components/signals/signal-utils"
14
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
@@ -54,6 +55,12 @@ import {
54
  } from "lucide-react"
55
  import type { BenchmarkCard } from "@/lib/benchmark-schema"
56
  import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
 
 
 
 
 
 
57
 
58
  interface EvalDetailProps {
59
  summary: BenchmarkEvalSummary
@@ -523,6 +530,11 @@ export function EvalDetail({ summary }: EvalDetailProps) {
523
  (summary.leaderboard_metrics?.length ?? 0) > 1 &&
524
  (summary.leaderboard_rows?.length ?? 0) > 0
525
  const [overviewOpen, setOverviewOpen] = useState(true)
 
 
 
 
 
526
  const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({})
527
  const [leaderboardPage, setLeaderboardPage] = useState(1)
528
  const [minParamStep, setMinParamStep] = useState(0)
@@ -636,6 +648,7 @@ export function EvalDetail({ summary }: EvalDetailProps) {
636
 
637
  return (
638
  <div className="space-y-6">
 
639
  <Card className="overflow-hidden">
640
  <Collapsible open={overviewOpen} onOpenChange={setOverviewOpen}>
641
  <CollapsibleTrigger asChild>
@@ -645,7 +658,7 @@ export function EvalDetail({ summary }: EvalDetailProps) {
645
  >
646
  <div className="min-w-0 space-y-1">
647
  <div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
648
- {isResearchView ? "Benchmark overview" : "Reading overview"}
649
  </div>
650
  <div className="flex flex-wrap items-center gap-2">
651
  <span className="text-base font-semibold tracking-tight sm:text-lg">{summary.evaluation_name}</span>
@@ -710,11 +723,6 @@ export function EvalDetail({ summary }: EvalDetailProps) {
710
  {summary.metric_config.evaluation_description}
711
  </p>
712
 
713
- {!isResearchView && (
714
- <p className="max-w-3xl text-sm leading-6 text-muted-foreground">
715
- {`${summary.benchmark_card?.purpose_and_intended_users?.goal ?? "This benchmark reports a capability result."} Scores should be read alongside benchmark scope, metric definitions, and the source dataset context.`}
716
- </p>
717
- )}
718
  </div>
719
 
720
  <div className="grid w-full grid-cols-2 gap-2 xl:grid-cols-4">
@@ -890,6 +898,14 @@ export function EvalDetail({ summary }: EvalDetailProps) {
890
  isResearchView={isResearchView}
891
  defaultOpen
892
  defaultRisksOpen={!isResearchView}
 
 
 
 
 
 
 
 
893
  />
894
  )}
895
  </CardContent>
@@ -901,7 +917,11 @@ export function EvalDetail({ summary }: EvalDetailProps) {
901
  <MultiMetricLeaderboard summary={summary} isResearchView={isResearchView} />
902
  ) : (
903
  <Card className="overflow-hidden">
904
- <CardHeader className="border-b bg-muted/10">
 
 
 
 
905
  <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
906
  <div className="space-y-2">
907
  <div className="flex items-center gap-2">
@@ -1034,7 +1054,6 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1034
  {isResearchView ? "Evaluator" : "Reporting Org"}
1035
  </TableHead>
1036
  <TableHead className="hidden min-w-[120px] lg:table-cell">Updated</TableHead>
1037
- <TableHead className="w-16 px-4 text-right">Details</TableHead>
1038
  </TableRow>
1039
  </TableHeader>
1040
  <TableBody>
@@ -1044,6 +1063,7 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1044
  ? Object.entries(modelResult.score_details.details).filter(([, value]) => typeof value === "number")
1045
  : []
1046
  const hasExpandableDetails =
 
1047
  (modelResult.aggregate_components && modelResult.aggregate_components.length > 1) ||
1048
  subtasks.length > 1
1049
 
@@ -1075,7 +1095,18 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1075
 
1076
  <TableCell className="whitespace-normal">
1077
  <div className="space-y-1">
1078
- <div className="font-semibold leading-tight">
 
 
 
 
 
 
 
 
 
 
 
1079
  <Link
1080
  href={`/models/${getModelFamilyRouteId(modelResult.model_info)}`}
1081
  className="underline decoration-dotted underline-offset-4 hover:text-primary"
@@ -1113,8 +1144,10 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1113
  </TableCell>
1114
 
1115
  <TableCell className="text-right">
1116
- <div className="text-xl font-semibold tabular-nums">{formatRawScore(modelResult.score, summary.metric_config.unit)}</div>
1117
- <SignalsRowBadges annotations={rowAnnotations} />
 
 
1118
  </TableCell>
1119
 
1120
  {isResearchView ? (
@@ -1160,24 +1193,11 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1160
  <TableCell className="hidden lg:table-cell">
1161
  <div className="text-sm text-muted-foreground">{formatDate(modelResult.evaluation_timestamp)}</div>
1162
  </TableCell>
1163
-
1164
- <TableCell className="px-4 text-right">
1165
- {hasExpandableDetails && (
1166
- <Button
1167
- variant="ghost"
1168
- size="icon"
1169
- aria-label={isExpanded ? "Collapse details" : "Expand details"}
1170
- onClick={() => toggleRow(key)}
1171
- >
1172
- {isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
1173
- </Button>
1174
- )}
1175
- </TableCell>
1176
  </TableRow>
1177
 
1178
  {isExpanded && (
1179
  <TableRow className="hover:bg-transparent">
1180
- <TableCell colSpan={8} className="bg-muted/10 px-0 py-0">
1181
  <div className="space-y-5 px-4 py-5 sm:px-6">
1182
  <div className="grid gap-4 xl:grid-cols-3">
1183
  <DetailPanel
@@ -1244,13 +1264,15 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1244
  )}
1245
  </DetailPanel>
1246
 
1247
- <ReproducibilityPanel gap={rowAnnotations?.reproducibility_gap} />
 
 
1248
 
1249
  <DetailPanel
1250
  title={isResearchView ? "Score Breakdown" : "Metric Summary"}
1251
  subtitle={
1252
  isResearchView
1253
- ? "Raw metric values and uncertainty details."
1254
  : "Raw performance plus uncertainty and sample details."
1255
  }
1256
  >
@@ -1260,19 +1282,23 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1260
  />
1261
  <MetaRow label="Score Type" value={modelResult.result.metric_config.score_type} />
1262
  <MetaRow label="Range" value={`${minScore} - ${maxScore}`} />
1263
- <MetaRow
1264
- label="Sample Size"
1265
- value={modelResult.score_details.sample_size ?? "Unknown"}
1266
- />
1267
- <MetaRow
1268
- label="Standard Error"
1269
- value={modelResult.score_details.standard_error ?? "Unknown"}
1270
- />
1271
- {modelResult.score_details.confidence_interval && (
1272
- <MetaRow
1273
- label="Confidence Interval"
1274
- value={`${modelResult.score_details.confidence_interval.lower} - ${modelResult.score_details.confidence_interval.upper} (${modelResult.score_details.confidence_interval.confidence_level}%)`}
1275
- />
 
 
 
 
1276
  )}
1277
  </DetailPanel>
1278
  </div>
@@ -1334,53 +1360,74 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1334
  </div>
1335
  )}
1336
 
1337
- {modelResult.result.generation_config && (
1338
  <div className="space-y-3">
1339
- <div>
1340
- <div className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
1341
- Generation Config
1342
- </div>
1343
- <div className="text-sm text-muted-foreground">
1344
- Evaluation-time generation parameters.
1345
- </div>
 
 
 
 
 
 
 
 
1346
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1347
 
1348
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
1349
- {modelResult.result.generation_config.generation_args &&
1350
- Object.entries(modelResult.result.generation_config.generation_args).map(([key, value]) => (
1351
- <div key={key} className="rounded-xl border bg-background/70 p-4">
1352
  <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
1353
- {key.replace(/_/g, " ")}
1354
  </div>
1355
- <div className="mt-2 text-sm font-medium">
1356
- {formatMetadataValue(value)}
1357
  </div>
1358
  </div>
1359
- ))}
1360
 
1361
- {modelResult.result.generation_config.additional_details && (
1362
- <div className="rounded-xl border bg-background/70 p-4 md:col-span-2 xl:col-span-3">
1363
- <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
1364
- Additional Details
1365
- </div>
1366
- <div className="mt-2 text-sm font-medium whitespace-pre-wrap">
1367
- {formatMetadataValue(modelResult.result.generation_config.additional_details)}
1368
- </div>
1369
- </div>
1370
- )}
1371
-
1372
- {modelResult.result.generation_config.prompt_template && (
1373
- <div className="rounded-xl border bg-background/70 p-4 md:col-span-2 xl:col-span-3">
1374
- <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
1375
- Prompt Template
1376
- </div>
1377
- <div className="mt-2 text-sm font-medium whitespace-pre-wrap">
1378
- {formatMetadataValue(modelResult.result.generation_config.prompt_template)}
1379
  </div>
1380
- </div>
1381
- )}
1382
  </div>
1383
- </div>
1384
  )}
1385
  </div>
1386
  </TableCell>
@@ -1391,7 +1438,7 @@ export function EvalDetail({ summary }: EvalDetailProps) {
1391
  })}
1392
  {leaderboardRows.length === 0 && (
1393
  <TableRow>
1394
- <TableCell colSpan={8} className="px-6 py-12 text-center text-sm text-muted-foreground">
1395
  No leaderboard entries match the selected parameter range.
1396
  </TableCell>
1397
  </TableRow>
@@ -1429,6 +1476,32 @@ function MultiMetricLeaderboard({
1429
  const [activeSubtaskTab, setActiveSubtaskTab] = useState<string>("all")
1430
  const [minParamStep, setMinParamStep] = useState(0)
1431
  const [maxParamStep, setMaxParamStep] = useState(PARAM_RANGE_VALUES.length - 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1432
  const leaderboardMetrics = summary.leaderboard_metrics ?? []
1433
  const leaderboardRows = summary.leaderboard_rows ?? []
1434
  const allMetricKeys = useMemo(() => leaderboardMetrics.map((metric) => metric.column_key), [leaderboardMetrics])
@@ -1703,7 +1776,11 @@ function MultiMetricLeaderboard({
1703
 
1704
  return (
1705
  <Card className="overflow-hidden">
1706
- <CardHeader className="border-b bg-muted/10">
 
 
 
 
1707
  <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
1708
  <div className="space-y-2">
1709
  <div className="flex items-center gap-2">
@@ -1940,9 +2017,13 @@ function MultiMetricLeaderboard({
1940
  <TableBody>
1941
  {pagedRows.map((row) => {
1942
  const rank = rankByModelId.get(row.model_info.id) ?? 0
 
 
 
1943
 
1944
  return (
1945
- <TableRow key={row.model_info.id} className="hover:bg-muted/10">
 
1946
  <TableCell className="px-4">
1947
  <div
1948
  className={cn(
@@ -1955,13 +2036,28 @@ function MultiMetricLeaderboard({
1955
  </TableCell>
1956
  <TableCell className="px-4 whitespace-normal">
1957
  <div className="space-y-1">
1958
- <div className="font-semibold leading-tight">
 
 
 
 
 
 
 
 
 
 
 
1959
  <Link
1960
  href={`/models/${getModelFamilyRouteId(row.model_info)}`}
1961
  className="underline decoration-dotted underline-offset-4 hover:text-primary"
1962
  >
1963
  {row.model_info.name}
1964
  </Link>
 
 
 
 
1965
  </div>
1966
  <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
1967
  {row.model_info.parameter_count && (
@@ -1976,12 +2072,6 @@ function MultiMetricLeaderboard({
1976
  )}
1977
  <span className="lg:hidden">{row.model_info.developer ?? "Unknown developer"}</span>
1978
  </div>
1979
- <SignalsRowBadges
1980
- annotations={getRowLevelAnnotations(row, visibleMetrics)}
1981
- variant="row"
1982
- className="mt-1 justify-start"
1983
- hideOnMobile={false}
1984
- />
1985
  </div>
1986
  </TableCell>
1987
 
@@ -2016,6 +2106,34 @@ function MultiMetricLeaderboard({
2016
  {formatDate(row.evaluation_timestamp)}
2017
  </TableCell>
2018
  </TableRow>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2019
  )})}
2020
 
2021
  {filteredRows.length === 0 && (
@@ -2046,11 +2164,13 @@ function BenchmarkCardCollapsible({
2046
  isResearchView,
2047
  defaultOpen = true,
2048
  defaultRisksOpen = false,
 
2049
  }: {
2050
  card: BenchmarkCard
2051
  isResearchView: boolean
2052
  defaultOpen?: boolean
2053
  defaultRisksOpen?: boolean
 
2054
  }) {
2055
  const [open, setOpen] = useState(defaultOpen)
2056
  return (
@@ -2079,6 +2199,7 @@ function BenchmarkCardCollapsible({
2079
  card={card}
2080
  isResearchView={isResearchView}
2081
  defaultRisksOpen={defaultRisksOpen}
 
2082
  />
2083
  </CollapsibleContent>
2084
  </Collapsible>
@@ -2095,12 +2216,12 @@ function DetailPanel({
2095
  children: React.ReactNode
2096
  }) {
2097
  return (
2098
- <div className="rounded-2xl border bg-background/70 p-4">
2099
  <div className="mb-4">
2100
  <div className="font-semibold">{title}</div>
2101
  <div className="text-sm text-muted-foreground">{subtitle}</div>
2102
  </div>
2103
- <div className="space-y-2.5">{children}</div>
2104
  </div>
2105
  )
2106
  }
@@ -2129,9 +2250,28 @@ function MetaRow({
2129
  }
2130
  }
2131
  return (
2132
- <div className="flex gap-3 text-sm">
2133
- <span className="w-32 shrink-0 text-muted-foreground">{label}</span>
2134
- <span className="min-w-0 flex-1 break-words font-medium">{value}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2135
  </div>
2136
  )
2137
  }
@@ -2183,10 +2323,12 @@ function BenchmarkCardPanel({
2183
  card,
2184
  isResearchView,
2185
  defaultRisksOpen = false,
 
2186
  }: {
2187
  card: BenchmarkCard
2188
  isResearchView: boolean
2189
  defaultRisksOpen?: boolean
 
2190
  }) {
2191
  const [risksOpen, setRisksOpen] = useState(defaultRisksOpen)
2192
  const details = card.benchmark_details
@@ -2236,6 +2378,8 @@ function BenchmarkCardPanel({
2236
  </CardHeader>
2237
 
2238
  <CardContent className="space-y-6 p-5 sm:p-6">
 
 
2239
  {/* Overview + domains */}
2240
  <div className="space-y-3">
2241
  <p className="text-sm leading-6 text-muted-foreground">{details.overview}</p>
@@ -2288,6 +2432,52 @@ function BenchmarkCardPanel({
2288
  </div>
2289
  </div>
2290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2291
  {/* Research-only: methodology + dataset details */}
2292
  {isResearchView && (
2293
  <div className="grid gap-4 sm:grid-cols-2">
@@ -2339,8 +2529,12 @@ function BenchmarkCardPanel({
2339
  </div>
2340
  )}
2341
 
2342
- {/* Risks (collapsible) */}
2343
- {risks.length > 0 && (
 
 
 
 
2344
  <Collapsible open={risksOpen} onOpenChange={setRisksOpen}>
2345
  <CollapsibleTrigger asChild>
2346
  <button
 
9
  import { ComparabilityPanel } from "@/components/signals/comparability-panel"
10
  import { ReproducibilityPanel } from "@/components/signals/reproducibility-panel"
11
  import { SignalsRowBadges } from "@/components/signals/signals-row-badges"
12
+ import { RowSignalsCompact } from "@/components/signals/row-signals-compact"
13
  import { SignalTooltip } from "@/components/signals/signal-tooltip"
14
  import { getCompletenessPopulatedCount } from "@/components/signals/signal-utils"
15
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
 
55
  } from "lucide-react"
56
  import type { BenchmarkCard } from "@/lib/benchmark-schema"
57
  import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
58
+ import { PolicyOverview } from "@/components/policy-overview"
59
+ import { ResearcherReproducibilityCard } from "@/components/researcher-reproducibility-card"
60
+ import { KnownIssuesPanel } from "@/components/known-issues-panel"
61
+ import { getKnownIssues, type KnownIssue } from "@/lib/known-issues"
62
+ import { ApplesToApplesBanner } from "@/components/apples-to-apples-banner"
63
+ import { FlagScoreButton } from "@/components/flag-score-button"
64
 
65
  interface EvalDetailProps {
66
  summary: BenchmarkEvalSummary
 
530
  (summary.leaderboard_metrics?.length ?? 0) > 1 &&
531
  (summary.leaderboard_rows?.length ?? 0) > 0
532
  const [overviewOpen, setOverviewOpen] = useState(true)
533
+ // Collapse the dense technical overview by default in policy mode; expand
534
+ // for researchers. Reset whenever the user switches modes.
535
+ useEffect(() => {
536
+ setOverviewOpen(isResearchView)
537
+ }, [isResearchView])
538
  const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({})
539
  const [leaderboardPage, setLeaderboardPage] = useState(1)
540
  const [minParamStep, setMinParamStep] = useState(0)
 
648
 
649
  return (
650
  <div className="space-y-6">
651
+ {!isResearchView && <PolicyOverview summary={summary} />}
652
  <Card className="overflow-hidden">
653
  <Collapsible open={overviewOpen} onOpenChange={setOverviewOpen}>
654
  <CollapsibleTrigger asChild>
 
658
  >
659
  <div className="min-w-0 space-y-1">
660
  <div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
661
+ {isResearchView ? "Benchmark overview" : "Technical details"}
662
  </div>
663
  <div className="flex flex-wrap items-center gap-2">
664
  <span className="text-base font-semibold tracking-tight sm:text-lg">{summary.evaluation_name}</span>
 
723
  {summary.metric_config.evaluation_description}
724
  </p>
725
 
 
 
 
 
 
726
  </div>
727
 
728
  <div className="grid w-full grid-cols-2 gap-2 xl:grid-cols-4">
 
898
  isResearchView={isResearchView}
899
  defaultOpen
900
  defaultRisksOpen={!isResearchView}
901
+ knownIssues={getKnownIssues(
902
+ summary.evaluation_name,
903
+ summary.composite_benchmark_name,
904
+ summary.composite_benchmark_key,
905
+ summary.benchmark_family_key,
906
+ summary.benchmark_leaf_key,
907
+ summary.benchmark_card.benchmark_details?.name,
908
+ )}
909
  />
910
  )}
911
  </CardContent>
 
917
  <MultiMetricLeaderboard summary={summary} isResearchView={isResearchView} />
918
  ) : (
919
  <Card className="overflow-hidden">
920
+ <CardHeader className="border-b bg-muted/10 space-y-3">
921
+ <ApplesToApplesBanner
922
+ summary={summary.comparability_summary}
923
+ detailsAnchorId="comparability-panel"
924
+ />
925
  <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
926
  <div className="space-y-2">
927
  <div className="flex items-center gap-2">
 
1054
  {isResearchView ? "Evaluator" : "Reporting Org"}
1055
  </TableHead>
1056
  <TableHead className="hidden min-w-[120px] lg:table-cell">Updated</TableHead>
 
1057
  </TableRow>
1058
  </TableHeader>
1059
  <TableBody>
 
1063
  ? Object.entries(modelResult.score_details.details).filter(([, value]) => typeof value === "number")
1064
  : []
1065
  const hasExpandableDetails =
1066
+ isResearchView ||
1067
  (modelResult.aggregate_components && modelResult.aggregate_components.length > 1) ||
1068
  subtasks.length > 1
1069
 
 
1095
 
1096
  <TableCell className="whitespace-normal">
1097
  <div className="space-y-1">
1098
+ <div className="flex items-center gap-1.5 font-semibold leading-tight">
1099
+ {hasExpandableDetails && (
1100
+ <button
1101
+ type="button"
1102
+ onClick={() => toggleRow(key)}
1103
+ aria-label={isExpanded ? "Collapse details" : "Expand details"}
1104
+ aria-expanded={isExpanded}
1105
+ className="-ml-1 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
1106
+ >
1107
+ {isExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
1108
+ </button>
1109
+ )}
1110
  <Link
1111
  href={`/models/${getModelFamilyRouteId(modelResult.model_info)}`}
1112
  className="underline decoration-dotted underline-offset-4 hover:text-primary"
 
1144
  </TableCell>
1145
 
1146
  <TableCell className="text-right">
1147
+ <div className="flex items-center justify-end gap-1.5">
1148
+ <div className="text-xl font-semibold tabular-nums">{formatRawScore(modelResult.score, summary.metric_config.unit)}</div>
1149
+ <RowSignalsCompact annotations={rowAnnotations} />
1150
+ </div>
1151
  </TableCell>
1152
 
1153
  {isResearchView ? (
 
1193
  <TableCell className="hidden lg:table-cell">
1194
  <div className="text-sm text-muted-foreground">{formatDate(modelResult.evaluation_timestamp)}</div>
1195
  </TableCell>
 
 
 
 
 
 
 
 
 
 
 
 
 
1196
  </TableRow>
1197
 
1198
  {isExpanded && (
1199
  <TableRow className="hover:bg-transparent">
1200
+ <TableCell colSpan={7} className="bg-muted/10 px-0 py-0">
1201
  <div className="space-y-5 px-4 py-5 sm:px-6">
1202
  <div className="grid gap-4 xl:grid-cols-3">
1203
  <DetailPanel
 
1264
  )}
1265
  </DetailPanel>
1266
 
1267
+ {!isResearchView && (
1268
+ <ReproducibilityPanel gap={rowAnnotations?.reproducibility_gap} />
1269
+ )}
1270
 
1271
  <DetailPanel
1272
  title={isResearchView ? "Score Breakdown" : "Metric Summary"}
1273
  subtitle={
1274
  isResearchView
1275
+ ? "Raw score and scale."
1276
  : "Raw performance plus uncertainty and sample details."
1277
  }
1278
  >
 
1282
  />
1283
  <MetaRow label="Score Type" value={modelResult.result.metric_config.score_type} />
1284
  <MetaRow label="Range" value={`${minScore} - ${maxScore}`} />
1285
+ {!isResearchView && (
1286
+ <>
1287
+ <MetaRow
1288
+ label="Sample Size"
1289
+ value={modelResult.score_details.sample_size ?? "Unknown"}
1290
+ />
1291
+ <MetaRow
1292
+ label="Standard Error"
1293
+ value={modelResult.score_details.standard_error ?? "Unknown"}
1294
+ />
1295
+ {modelResult.score_details.confidence_interval && (
1296
+ <MetaRow
1297
+ label="Confidence Interval"
1298
+ value={`${modelResult.score_details.confidence_interval.lower} - ${modelResult.score_details.confidence_interval.upper} (${modelResult.score_details.confidence_interval.confidence_level}%)`}
1299
+ />
1300
+ )}
1301
+ </>
1302
  )}
1303
  </DetailPanel>
1304
  </div>
 
1360
  </div>
1361
  )}
1362
 
1363
+ {isResearchView ? (
1364
  <div className="space-y-3">
1365
+ <ResearcherReproducibilityCard
1366
+ modelResult={modelResult}
1367
+ benchmarkKey={summary.benchmark_leaf_key ?? summary.composite_benchmark_key}
1368
+ evalName={summary.evaluation_name}
1369
+ />
1370
+ <div className="flex justify-end">
1371
+ <FlagScoreButton
1372
+ modelName={modelResult.model_info.name}
1373
+ modelId={modelResult.model_info.id}
1374
+ benchmarkName={summary.evaluation_name}
1375
+ benchmarkId={summary.evaluation_id}
1376
+ score={formatRawScore(modelResult.score, summary.metric_config.unit)}
1377
+ sourceUrl={modelResult.source_metadata.source_url}
1378
+ sourceRecordUrl={modelResult.source_record_url}
1379
+ />
1380
  </div>
1381
+ </div>
1382
+ ) : (
1383
+ modelResult.result.generation_config && (
1384
+ <div className="space-y-3">
1385
+ <div>
1386
+ <div className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
1387
+ Generation Config
1388
+ </div>
1389
+ <div className="text-sm text-muted-foreground">
1390
+ Evaluation-time generation parameters.
1391
+ </div>
1392
+ </div>
1393
+
1394
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
1395
+ {modelResult.result.generation_config.generation_args &&
1396
+ Object.entries(modelResult.result.generation_config.generation_args).map(([key, value]) => (
1397
+ <div key={key} className="rounded-xl border bg-background/70 p-4">
1398
+ <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
1399
+ {key.replace(/_/g, " ")}
1400
+ </div>
1401
+ <div className="mt-2 text-sm font-medium">
1402
+ {formatMetadataValue(value)}
1403
+ </div>
1404
+ </div>
1405
+ ))}
1406
 
1407
+ {modelResult.result.generation_config.additional_details && (
1408
+ <div className="rounded-xl border bg-background/70 p-4 md:col-span-2 xl:col-span-3">
 
 
1409
  <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
1410
+ Additional Details
1411
  </div>
1412
+ <div className="mt-2 text-sm font-medium whitespace-pre-wrap">
1413
+ {formatMetadataValue(modelResult.result.generation_config.additional_details)}
1414
  </div>
1415
  </div>
1416
+ )}
1417
 
1418
+ {modelResult.result.generation_config.prompt_template && (
1419
+ <div className="rounded-xl border bg-background/70 p-4 md:col-span-2 xl:col-span-3">
1420
+ <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
1421
+ Prompt Template
1422
+ </div>
1423
+ <div className="mt-2 text-sm font-medium whitespace-pre-wrap">
1424
+ {formatMetadataValue(modelResult.result.generation_config.prompt_template)}
1425
+ </div>
 
 
 
 
 
 
 
 
 
 
1426
  </div>
1427
+ )}
1428
+ </div>
1429
  </div>
1430
+ )
1431
  )}
1432
  </div>
1433
  </TableCell>
 
1438
  })}
1439
  {leaderboardRows.length === 0 && (
1440
  <TableRow>
1441
+ <TableCell colSpan={7} className="px-6 py-12 text-center text-sm text-muted-foreground">
1442
  No leaderboard entries match the selected parameter range.
1443
  </TableCell>
1444
  </TableRow>
 
1476
  const [activeSubtaskTab, setActiveSubtaskTab] = useState<string>("all")
1477
  const [minParamStep, setMinParamStep] = useState(0)
1478
  const [maxParamStep, setMaxParamStep] = useState(PARAM_RANGE_VALUES.length - 1)
1479
+ const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({})
1480
+
1481
+ // Index ModelResultForBenchmark entries by model_info.id so we can power the
1482
+ // research-mode reproducibility card from a multi-metric row. There may be
1483
+ // several entries per model (one per metric); we prefer one with a recorded
1484
+ // generation_config so the card has the most data to show.
1485
+ const modelResultByModelId = useMemo(() => {
1486
+ const map = new Map<string, ModelResultForBenchmark>()
1487
+ for (const result of summary.model_results) {
1488
+ const id = result.model_info.id
1489
+ const existing = map.get(id)
1490
+ if (!existing) {
1491
+ map.set(id, result)
1492
+ continue
1493
+ }
1494
+ const existingHasGen = existing.result.generation_config != null
1495
+ const candidateHasGen = result.result.generation_config != null
1496
+ if (!existingHasGen && candidateHasGen) {
1497
+ map.set(id, result)
1498
+ }
1499
+ }
1500
+ return map
1501
+ }, [summary.model_results])
1502
+
1503
+ const toggleExpandedRow = (key: string) =>
1504
+ setExpandedRows((current) => ({ ...current, [key]: !current[key] }))
1505
  const leaderboardMetrics = summary.leaderboard_metrics ?? []
1506
  const leaderboardRows = summary.leaderboard_rows ?? []
1507
  const allMetricKeys = useMemo(() => leaderboardMetrics.map((metric) => metric.column_key), [leaderboardMetrics])
 
1776
 
1777
  return (
1778
  <Card className="overflow-hidden">
1779
+ <CardHeader className="border-b bg-muted/10 space-y-3">
1780
+ <ApplesToApplesBanner
1781
+ summary={summary.comparability_summary}
1782
+ detailsAnchorId="comparability-panel"
1783
+ />
1784
  <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
1785
  <div className="space-y-2">
1786
  <div className="flex items-center gap-2">
 
2017
  <TableBody>
2018
  {pagedRows.map((row) => {
2019
  const rank = rankByModelId.get(row.model_info.id) ?? 0
2020
+ const expandKey = row.model_info.id
2021
+ const isExpanded = expandedRows[expandKey] ?? false
2022
+ const matchingResult = modelResultByModelId.get(row.model_info.id)
2023
 
2024
  return (
2025
+ <Fragment key={row.model_info.id}>
2026
+ <TableRow className={cn("hover:bg-muted/10", isExpanded && "bg-muted/15")}>
2027
  <TableCell className="px-4">
2028
  <div
2029
  className={cn(
 
2036
  </TableCell>
2037
  <TableCell className="px-4 whitespace-normal">
2038
  <div className="space-y-1">
2039
+ <div className="flex items-center gap-1.5 font-semibold leading-tight">
2040
+ {isResearchView && matchingResult && (
2041
+ <button
2042
+ type="button"
2043
+ onClick={() => toggleExpandedRow(expandKey)}
2044
+ aria-label={isExpanded ? "Hide reproducibility" : "Show reproducibility"}
2045
+ aria-expanded={isExpanded}
2046
+ className="-ml-1 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
2047
+ >
2048
+ {isExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
2049
+ </button>
2050
+ )}
2051
  <Link
2052
  href={`/models/${getModelFamilyRouteId(row.model_info)}`}
2053
  className="underline decoration-dotted underline-offset-4 hover:text-primary"
2054
  >
2055
  {row.model_info.name}
2056
  </Link>
2057
+ <RowSignalsCompact
2058
+ annotations={getRowLevelAnnotations(row, visibleMetrics)}
2059
+ className="ml-1"
2060
+ />
2061
  </div>
2062
  <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
2063
  {row.model_info.parameter_count && (
 
2072
  )}
2073
  <span className="lg:hidden">{row.model_info.developer ?? "Unknown developer"}</span>
2074
  </div>
 
 
 
 
 
 
2075
  </div>
2076
  </TableCell>
2077
 
 
2106
  {formatDate(row.evaluation_timestamp)}
2107
  </TableCell>
2108
  </TableRow>
2109
+ {isResearchView && isExpanded && matchingResult && (
2110
+ <TableRow className="hover:bg-transparent">
2111
+ <TableCell
2112
+ colSpan={visibleMetrics.length + 5}
2113
+ className="bg-muted/10 px-4 py-5 sm:px-6"
2114
+ >
2115
+ <div className="space-y-3">
2116
+ <ResearcherReproducibilityCard
2117
+ modelResult={matchingResult}
2118
+ benchmarkKey={summary.benchmark_leaf_key ?? summary.composite_benchmark_key}
2119
+ evalName={summary.evaluation_name}
2120
+ />
2121
+ <div className="flex justify-end">
2122
+ <FlagScoreButton
2123
+ modelName={matchingResult.model_info.name}
2124
+ modelId={matchingResult.model_info.id}
2125
+ benchmarkName={summary.evaluation_name}
2126
+ benchmarkId={summary.evaluation_id}
2127
+ score={formatRawScore(matchingResult.score, summary.metric_config.unit)}
2128
+ sourceUrl={matchingResult.source_metadata.source_url}
2129
+ sourceRecordUrl={matchingResult.source_record_url}
2130
+ />
2131
+ </div>
2132
+ </div>
2133
+ </TableCell>
2134
+ </TableRow>
2135
+ )}
2136
+ </Fragment>
2137
  )})}
2138
 
2139
  {filteredRows.length === 0 && (
 
2164
  isResearchView,
2165
  defaultOpen = true,
2166
  defaultRisksOpen = false,
2167
+ knownIssues = [],
2168
  }: {
2169
  card: BenchmarkCard
2170
  isResearchView: boolean
2171
  defaultOpen?: boolean
2172
  defaultRisksOpen?: boolean
2173
+ knownIssues?: KnownIssue[]
2174
  }) {
2175
  const [open, setOpen] = useState(defaultOpen)
2176
  return (
 
2199
  card={card}
2200
  isResearchView={isResearchView}
2201
  defaultRisksOpen={defaultRisksOpen}
2202
+ knownIssues={knownIssues}
2203
  />
2204
  </CollapsibleContent>
2205
  </Collapsible>
 
2216
  children: React.ReactNode
2217
  }) {
2218
  return (
2219
+ <div className="min-w-0 rounded-2xl border bg-background/70 p-4">
2220
  <div className="mb-4">
2221
  <div className="font-semibold">{title}</div>
2222
  <div className="text-sm text-muted-foreground">{subtitle}</div>
2223
  </div>
2224
+ <div className="min-w-0 space-y-2.5">{children}</div>
2225
  </div>
2226
  )
2227
  }
 
2250
  }
2251
  }
2252
  return (
2253
+ <div
2254
+ className="text-sm"
2255
+ style={{
2256
+ display: "grid",
2257
+ gridTemplateColumns: "8rem minmax(0, 1fr)",
2258
+ columnGap: "0.75rem",
2259
+ width: "100%",
2260
+ minWidth: 0,
2261
+ }}
2262
+ >
2263
+ <div className="text-muted-foreground">{label}</div>
2264
+ <div
2265
+ className="font-medium"
2266
+ style={{
2267
+ minWidth: 0,
2268
+ maxWidth: "100%",
2269
+ overflowWrap: "anywhere",
2270
+ wordBreak: "break-word",
2271
+ }}
2272
+ >
2273
+ {value}
2274
+ </div>
2275
  </div>
2276
  )
2277
  }
 
2323
  card,
2324
  isResearchView,
2325
  defaultRisksOpen = false,
2326
+ knownIssues = [],
2327
  }: {
2328
  card: BenchmarkCard
2329
  isResearchView: boolean
2330
  defaultRisksOpen?: boolean
2331
+ knownIssues?: KnownIssue[]
2332
  }) {
2333
  const [risksOpen, setRisksOpen] = useState(defaultRisksOpen)
2334
  const details = card.benchmark_details
 
2378
  </CardHeader>
2379
 
2380
  <CardContent className="space-y-6 p-5 sm:p-6">
2381
+ {knownIssues.length > 0 && <KnownIssuesPanel issues={knownIssues} variant="full" />}
2382
+
2383
  {/* Overview + domains */}
2384
  <div className="space-y-3">
2385
  <p className="text-sm leading-6 text-muted-foreground">{details.overview}</p>
 
2432
  </div>
2433
  </div>
2434
 
2435
+ {(methodology.methods?.length > 0 ||
2436
+ (methodology.calculation && methodology.calculation !== "Not specified") ||
2437
+ (methodology.validation && methodology.validation !== "Not specified")) && (
2438
+ <div className="rounded-[1.25rem] border border-border/70 bg-muted/10 p-4">
2439
+ <div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
2440
+ How tasks were sourced and scored
2441
+ </div>
2442
+ <div className="grid gap-4 lg:grid-cols-2">
2443
+ {methodology.methods?.length > 0 && (
2444
+ <div>
2445
+ <div className="mb-1.5 text-xs font-semibold text-foreground/80">
2446
+ Task setup
2447
+ </div>
2448
+ <ol className="list-decimal space-y-1.5 pl-4 text-sm leading-5 text-muted-foreground">
2449
+ {methodology.methods.map((m, i) => (
2450
+ <li key={i}>{m}</li>
2451
+ ))}
2452
+ </ol>
2453
+ </div>
2454
+ )}
2455
+ <div className="space-y-3">
2456
+ {methodology.calculation && methodology.calculation !== "Not specified" && (
2457
+ <div>
2458
+ <div className="mb-1.5 text-xs font-semibold text-foreground/80">
2459
+ Score calculation
2460
+ </div>
2461
+ <p className="text-sm leading-5 text-muted-foreground">
2462
+ {methodology.calculation}
2463
+ </p>
2464
+ </div>
2465
+ )}
2466
+ {methodology.validation && methodology.validation !== "Not specified" && (
2467
+ <div>
2468
+ <div className="mb-1.5 text-xs font-semibold text-foreground/80">
2469
+ Validation
2470
+ </div>
2471
+ <p className="text-sm leading-5 text-muted-foreground">
2472
+ {methodology.validation}
2473
+ </p>
2474
+ </div>
2475
+ )}
2476
+ </div>
2477
+ </div>
2478
+ </div>
2479
+ )}
2480
+
2481
  {/* Research-only: methodology + dataset details */}
2482
  {isResearchView && (
2483
  <div className="grid gap-4 sm:grid-cols-2">
 
2529
  </div>
2530
  )}
2531
 
2532
+ {/* Generic IBM-style AI risks. These are boilerplate (per audit
2533
+ feedback: "least useful feature for policy users"), so in policy
2534
+ mode we hide them entirely — the curated known-issues panel above
2535
+ carries the benchmark-specific concerns. Researchers still get the
2536
+ full collapsible list. */}
2537
+ {risks.length > 0 && isResearchView && (
2538
  <Collapsible open={risksOpen} onOpenChange={setRisksOpen}>
2539
  <CollapsibleTrigger asChild>
2540
  <button
components/flag-score-button.tsx ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useMemo, useState } from "react"
4
+ import {
5
+ Copy,
6
+ ExternalLink,
7
+ FileSearch,
8
+ Flag,
9
+ GitPullRequestArrow,
10
+ MessageSquare,
11
+ } from "lucide-react"
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogHeader,
17
+ DialogTitle,
18
+ } from "@/components/ui/dialog"
19
+ import { Button } from "@/components/ui/button"
20
+
21
+ interface FlagScoreButtonProps {
22
+ modelName: string
23
+ modelId: string
24
+ benchmarkName: string
25
+ benchmarkId?: string
26
+ score: number | string
27
+ /** URL to the published source the score was extracted from (paper, blog, leaderboard). */
28
+ sourceUrl?: string
29
+ /** URL to the processed record JSON in the card_backend HF dataset. */
30
+ sourceRecordUrl?: string
31
+ /** Optional explicit upstream record URL in evaleval/EEE_datastore (raw source of truth). */
32
+ eeeRecordUrl?: string
33
+ }
34
+
35
+ interface DatasetLinks {
36
+ repoSlug: string
37
+ recordViewUrl: string | null
38
+ recordRawUrl: string | null
39
+ discussionsUrl: string
40
+ newDiscussionUrl: string
41
+ }
42
+
43
+ /**
44
+ * Given a /resolve/main/... HF dataset URL, derive the helpful sibling URLs
45
+ * for the dataset (file viewer, discussions list, new-discussion form). Returns
46
+ * null when the input doesn't look like a HF dataset URL.
47
+ */
48
+ function deriveDatasetLinks(recordUrl: string | undefined, prefilledTitle: string): DatasetLinks | null {
49
+ if (!recordUrl) return null
50
+ const m = recordUrl.match(
51
+ /^https:\/\/huggingface\.co\/datasets\/([^/]+\/[^/]+)\/(?:resolve|raw|blob)\/[^/]+\/(.*)$/,
52
+ )
53
+ if (!m) return null
54
+ const repoSlug = m[1]
55
+ const path = m[2]
56
+ const datasetBase = `https://huggingface.co/datasets/${repoSlug}`
57
+ const recordViewUrl = `${datasetBase}/blob/main/${path}`
58
+ const recordRawUrl = `${datasetBase}/resolve/main/${path}`
59
+ const discussionsUrl = `${datasetBase}/discussions`
60
+ const newDiscussionUrl = `${datasetBase}/discussions/new?title=${encodeURIComponent(prefilledTitle)}`
61
+ return { repoSlug, recordViewUrl, recordRawUrl, discussionsUrl, newDiscussionUrl }
62
+ }
63
+
64
+ /**
65
+ * "Flag this score" — researcher affordance that, instead of capturing the
66
+ * report ourselves, sends the user directly to the upstream HF dataset where
67
+ * the data lives. They can then file a discussion or submit a correction PR
68
+ * against the actual record. We also offer a copyable context snippet so the
69
+ * issue body has all the relevant identifiers without needing to retype them.
70
+ */
71
+ export function FlagScoreButton({
72
+ modelName,
73
+ modelId,
74
+ benchmarkName,
75
+ benchmarkId,
76
+ score,
77
+ sourceUrl,
78
+ sourceRecordUrl,
79
+ eeeRecordUrl,
80
+ }: FlagScoreButtonProps) {
81
+ const [open, setOpen] = useState(false)
82
+ const [copied, setCopied] = useState<"context" | null>(null)
83
+
84
+ const prefilledTitle = `Possible issue: ${modelName} on ${benchmarkName} (score ${score})`
85
+
86
+ const cardBackendLinks = useMemo(
87
+ () => deriveDatasetLinks(sourceRecordUrl, prefilledTitle),
88
+ [sourceRecordUrl, prefilledTitle],
89
+ )
90
+ const eeeLinks = useMemo(
91
+ () => deriveDatasetLinks(eeeRecordUrl, prefilledTitle),
92
+ [eeeRecordUrl, prefilledTitle],
93
+ )
94
+
95
+ // Prefer the EEE upstream as the "correction venue" when known — that's
96
+ // where raw evaluation records live. Fall back to card_backend (the
97
+ // pipeline output) when EEE isn't directly addressable for this row.
98
+ const correctionLinks = eeeLinks ?? cardBackendLinks
99
+
100
+ const contextSnippet = [
101
+ `Model: ${modelName} (${modelId})`,
102
+ `Benchmark: ${benchmarkName}${benchmarkId ? ` (${benchmarkId})` : ""}`,
103
+ `Reported score: ${score}`,
104
+ sourceUrl ? `Original source URL: ${sourceUrl}` : null,
105
+ sourceRecordUrl ? `Pipeline record: ${sourceRecordUrl}` : null,
106
+ eeeRecordUrl ? `EEE upstream record: ${eeeRecordUrl}` : null,
107
+ ]
108
+ .filter(Boolean)
109
+ .join("\n")
110
+
111
+ const handleCopyContext = async () => {
112
+ try {
113
+ await navigator.clipboard.writeText(contextSnippet)
114
+ setCopied("context")
115
+ setTimeout(() => setCopied(null), 1800)
116
+ } catch {
117
+ // Clipboard might be blocked; ignore — the user can still select text manually.
118
+ }
119
+ }
120
+
121
+ return (
122
+ <>
123
+ <Button
124
+ type="button"
125
+ size="sm"
126
+ variant="outline"
127
+ className="h-7 gap-1.5 text-xs"
128
+ onClick={() => setOpen(true)}
129
+ >
130
+ <Flag className="h-3 w-3" />
131
+ Flag this score
132
+ </Button>
133
+
134
+ <Dialog open={open} onOpenChange={setOpen}>
135
+ <DialogContent className="sm:max-w-lg">
136
+ <DialogHeader>
137
+ <DialogTitle>Flag this score</DialogTitle>
138
+ <DialogDescription>
139
+ Take this report directly to the dataset where the record lives. You can file a
140
+ discussion or open a correction PR against the actual file.
141
+ </DialogDescription>
142
+ </DialogHeader>
143
+
144
+ <div className="space-y-4">
145
+ <div className="rounded-xl border bg-muted/20 p-3 text-xs">
146
+ <div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
147
+ Flagging
148
+ </div>
149
+ <div className="mt-1">
150
+ <span className="font-medium">{modelName}</span>{" "}
151
+ <span className="text-muted-foreground">on</span>{" "}
152
+ <span className="font-medium">{benchmarkName}</span>
153
+ </div>
154
+ <div className="text-muted-foreground tabular-nums">Score: {score}</div>
155
+ </div>
156
+
157
+ <div className="space-y-2">
158
+ {correctionLinks?.recordViewUrl && (
159
+ <a
160
+ href={correctionLinks.recordViewUrl}
161
+ target="_blank"
162
+ rel="noreferrer"
163
+ className="flex items-center justify-between rounded-xl border border-border/70 bg-background px-3 py-2.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted/20"
164
+ >
165
+ <div className="flex items-center gap-2">
166
+ <FileSearch className="h-4 w-4 text-muted-foreground" />
167
+ <div>
168
+ <div className="font-semibold">View the underlying record</div>
169
+ <div className="text-xs text-muted-foreground">
170
+ Opens the JSON file on{" "}
171
+ <span className="font-mono">{correctionLinks.repoSlug}</span>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ <ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
176
+ </a>
177
+ )}
178
+
179
+ {correctionLinks && (
180
+ <a
181
+ href={correctionLinks.newDiscussionUrl}
182
+ target="_blank"
183
+ rel="noreferrer"
184
+ className="flex items-center justify-between rounded-xl border border-border/70 bg-background px-3 py-2.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted/20"
185
+ >
186
+ <div className="flex items-center gap-2">
187
+ <MessageSquare className="h-4 w-4 text-muted-foreground" />
188
+ <div>
189
+ <div className="font-semibold">Open a discussion</div>
190
+ <div className="text-xs text-muted-foreground">
191
+ Pre-filled title; paste the context snippet into the body.
192
+ </div>
193
+ </div>
194
+ </div>
195
+ <ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
196
+ </a>
197
+ )}
198
+
199
+ {correctionLinks && (
200
+ <a
201
+ href={correctionLinks.discussionsUrl}
202
+ target="_blank"
203
+ rel="noreferrer"
204
+ className="flex items-center justify-between rounded-xl border border-border/70 bg-background px-3 py-2.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted/20"
205
+ >
206
+ <div className="flex items-center gap-2">
207
+ <GitPullRequestArrow className="h-4 w-4 text-muted-foreground" />
208
+ <div>
209
+ <div className="font-semibold">Browse existing discussions</div>
210
+ <div className="text-xs text-muted-foreground">
211
+ Check whether someone already filed a similar correction.
212
+ </div>
213
+ </div>
214
+ </div>
215
+ <ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
216
+ </a>
217
+ )}
218
+
219
+ {!correctionLinks && (
220
+ <div className="rounded-xl border border-dashed border-amber-300/60 bg-amber-50/40 px-3 py-2.5 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/20 dark:text-amber-200">
221
+ No upstream record URL is recorded for this row, so we can't link directly to
222
+ the dataset. Copy the context below and file an issue at{" "}
223
+ <a
224
+ className="underline-offset-4 hover:underline"
225
+ href="https://huggingface.co/datasets/evaleval/EEE_datastore/discussions"
226
+ target="_blank"
227
+ rel="noreferrer"
228
+ >
229
+ evaleval/EEE_datastore/discussions
230
+ </a>
231
+ .
232
+ </div>
233
+ )}
234
+ </div>
235
+
236
+ <div className="space-y-1.5">
237
+ <div className="flex items-center justify-between">
238
+ <label className="text-xs font-semibold text-muted-foreground">
239
+ Context to paste into the issue
240
+ </label>
241
+ <Button
242
+ type="button"
243
+ size="sm"
244
+ variant="ghost"
245
+ onClick={handleCopyContext}
246
+ className="h-7 gap-1.5 text-xs"
247
+ >
248
+ <Copy className="h-3 w-3" />
249
+ {copied === "context" ? "Copied" : "Copy"}
250
+ </Button>
251
+ </div>
252
+ <pre className="max-h-40 overflow-auto whitespace-pre-wrap break-all rounded-xl border bg-muted/10 p-3 text-[11px] leading-5 text-muted-foreground">
253
+ {contextSnippet}
254
+ </pre>
255
+ </div>
256
+
257
+ <div className="flex justify-end pt-1">
258
+ <Button type="button" variant="ghost" onClick={() => setOpen(false)}>
259
+ Close
260
+ </Button>
261
+ </div>
262
+ </div>
263
+ </DialogContent>
264
+ </Dialog>
265
+ </>
266
+ )
267
+ }
components/known-issues-panel.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { AlertOctagon, AlertTriangle, ExternalLink, Info } from "lucide-react"
4
+ import type { KnownIssue } from "@/lib/known-issues"
5
+
6
+ interface KnownIssuesPanelProps {
7
+ issues: KnownIssue[]
8
+ /**
9
+ * "compact" — single-line summary chip suitable for surfacing at the top of
10
+ * the policy overview. "full" — full bordered list with descriptions.
11
+ */
12
+ variant?: "compact" | "full"
13
+ }
14
+
15
+ const SEVERITY_STYLE: Record<KnownIssue["severity"], { wrap: string; icon: React.ComponentType<{ className?: string }>; label: string }> = {
16
+ info: {
17
+ wrap: "border-sky-300/60 bg-sky-50/60 text-sky-900 dark:border-sky-900/50 dark:bg-sky-950/20 dark:text-sky-100",
18
+ icon: Info,
19
+ label: "Note",
20
+ },
21
+ warning: {
22
+ wrap: "border-amber-300/60 bg-amber-50/60 text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/20 dark:text-amber-100",
23
+ icon: AlertTriangle,
24
+ label: "Known issue",
25
+ },
26
+ critical: {
27
+ wrap: "border-rose-300/60 bg-rose-50/60 text-rose-900 dark:border-rose-900/50 dark:bg-rose-950/20 dark:text-rose-100",
28
+ icon: AlertOctagon,
29
+ label: "Critical issue",
30
+ },
31
+ }
32
+
33
+ export function KnownIssuesPanel({ issues, variant = "full" }: KnownIssuesPanelProps) {
34
+ if (issues.length === 0) return null
35
+
36
+ const sorted = [...issues].sort((a, b) => severityRank(b.severity) - severityRank(a.severity))
37
+ const headlineSeverity = sorted[0].severity
38
+ const Style = SEVERITY_STYLE[headlineSeverity]
39
+ const Icon = Style.icon
40
+
41
+ if (variant === "compact") {
42
+ return (
43
+ <div className={`flex items-start gap-2 rounded-2xl border px-3 py-2 text-sm ${Style.wrap}`}>
44
+ <Icon className="mt-0.5 h-4 w-4 shrink-0" />
45
+ <div className="min-w-0">
46
+ <span className="font-semibold">
47
+ {issues.length} known issue{issues.length === 1 ? "" : "s"} documented
48
+ </span>
49
+ <span className="ml-1 text-muted-foreground">— see below for detail.</span>
50
+ </div>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ return (
56
+ <section className="space-y-2">
57
+ <div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
58
+ Known issues with this benchmark
59
+ </div>
60
+ <ul className="space-y-2">
61
+ {sorted.map((issue, idx) => {
62
+ const S = SEVERITY_STYLE[issue.severity]
63
+ const I = S.icon
64
+ return (
65
+ <li
66
+ key={`${issue.title}-${idx}`}
67
+ className={`rounded-2xl border px-3.5 py-3 ${S.wrap}`}
68
+ >
69
+ <div className="flex items-start gap-2">
70
+ <I className="mt-0.5 h-4 w-4 shrink-0" />
71
+ <div className="min-w-0 flex-1">
72
+ <div className="flex flex-wrap items-baseline gap-2">
73
+ <span className="text-[10px] font-semibold uppercase tracking-[0.18em] opacity-80">
74
+ {S.label}
75
+ </span>
76
+ <span className="font-semibold leading-tight">{issue.title}</span>
77
+ </div>
78
+ <p className="mt-1 text-sm leading-5 opacity-90">{issue.summary}</p>
79
+ {(issue.source_url || issue.published) && (
80
+ <div className="mt-2 flex flex-wrap items-center gap-3 text-xs opacity-80">
81
+ {issue.source_url && (
82
+ <a
83
+ href={issue.source_url}
84
+ target="_blank"
85
+ rel="noreferrer"
86
+ className="inline-flex items-center gap-1 underline-offset-4 hover:underline"
87
+ >
88
+ Source <ExternalLink className="h-3 w-3" />
89
+ </a>
90
+ )}
91
+ {issue.published && <span>Published {issue.published}</span>}
92
+ </div>
93
+ )}
94
+ </div>
95
+ </div>
96
+ </li>
97
+ )
98
+ })}
99
+ </ul>
100
+ </section>
101
+ )
102
+ }
103
+
104
+ function severityRank(s: KnownIssue["severity"]): number {
105
+ if (s === "critical") return 3
106
+ if (s === "warning") return 2
107
+ return 1
108
+ }
components/mode-switch.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import type { ReactNode } from "react"
4
+ import { useAudienceMode } from "@/components/audience-mode-provider"
5
+
6
+ interface ModeSwitchProps {
7
+ research?: ReactNode
8
+ policy?: ReactNode
9
+ }
10
+
11
+ /**
12
+ * Renders different content per audience mode. Use this instead of inline
13
+ * `mode === "research"` checks so that call sites stay readable and the
14
+ * two branches stay obviously parallel.
15
+ */
16
+ export function ModeSwitch({ research, policy }: ModeSwitchProps) {
17
+ const { mode } = useAudienceMode()
18
+ return <>{mode === "research" ? research ?? null : policy ?? null}</>
19
+ }
20
+
21
+ /**
22
+ * Hook variant for cases where ModeSwitch can't be used (e.g. choosing
23
+ * between two non-JSX values like a className or a number).
24
+ */
25
+ export function useModeValue<T>(values: { research: T; policy: T }): T {
26
+ const { mode } = useAudienceMode()
27
+ return mode === "research" ? values.research : values.policy
28
+ }
components/policy-overview.tsx ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useMemo, useState } from "react"
4
+ import { BookOpen, ChevronDown, ChevronUp, ExternalLink, FileText, Globe, Layers, ScrollText, Tag, Users } from "lucide-react"
5
+ import { SignalTooltip } from "@/components/signals/signal-tooltip"
6
+ import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
7
+ import { getKnownIssues } from "@/lib/known-issues"
8
+ import { KnownIssuesPanel } from "@/components/known-issues-panel"
9
+
10
+ interface PolicyOverviewProps {
11
+ summary: BenchmarkEvalSummary
12
+ }
13
+
14
+ const SUMMARY_PREVIEW_CHARS = 280
15
+
16
+ function classifyResource(url: string): { kind: "paper" | "dataset" | "leaderboard" | "site"; label: string } {
17
+ const lower = url.toLowerCase()
18
+ if (lower.includes("arxiv.org") || lower.endsWith(".pdf") || lower.includes("/papers/")) {
19
+ return { kind: "paper", label: "Paper" }
20
+ }
21
+ if (lower.includes("huggingface.co/datasets") || lower.includes("/dataset")) {
22
+ return { kind: "dataset", label: "Dataset" }
23
+ }
24
+ if (lower.includes("leaderboard")) {
25
+ return { kind: "leaderboard", label: "Leaderboard" }
26
+ }
27
+ return { kind: "site", label: "Source" }
28
+ }
29
+
30
+ function shortHost(url: string) {
31
+ return url.replace(/^https?:\/\//, "").replace(/\/.*$/, "")
32
+ }
33
+
34
+ /**
35
+ * Plain-language summary surface shown at the top of the benchmark page in
36
+ * policy mode. Designed to answer three questions a non-technical reader has:
37
+ * what does this measure, who built it, and where does it come from?
38
+ *
39
+ * Technical detail (variants, metric specifications, score scales) lives in
40
+ * the existing overview card below this and is collapsed by default.
41
+ */
42
+ function normalizeId(value: string | undefined | null): string {
43
+ if (!value) return ""
44
+ return value
45
+ .toLowerCase()
46
+ .trim()
47
+ .replace(/[\s_\-/]+/g, "")
48
+ }
49
+
50
+ export function PolicyOverview({ summary }: PolicyOverviewProps) {
51
+ const card = summary.benchmark_card
52
+
53
+ // Defensive check: pipelines older than the "ancestor card leak" fix
54
+ // sometimes attach the parent suite's card to a leaf benchmark (e.g.
55
+ // helm_classic's card embedded under XSUM). Detect when the card's own
56
+ // name is clearly not this benchmark and ignore its narrative text — the
57
+ // synthesized fallback below produces something accurate instead.
58
+ const cardName = card?.benchmark_details?.name
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,
66
+ summary.evaluation_id,
67
+ ].map(normalizeId)
68
+ // Treat the card as belonging to this eval when its name fuzzily appears in
69
+ // any of the eval's identifiers (or vice versa). Otherwise the card is from
70
+ // a different (typically ancestor) benchmark.
71
+ const cardMatchesEval =
72
+ !cardName ||
73
+ evalIdentifiers.some(
74
+ (id) =>
75
+ id.length > 0 &&
76
+ cardNameNorm.length > 0 &&
77
+ (id.includes(cardNameNorm) || cardNameNorm.includes(id)),
78
+ )
79
+
80
+ const overview = cardMatchesEval ? card?.benchmark_details?.overview?.trim() || "" : ""
81
+ const goal = cardMatchesEval ? card?.purpose_and_intended_users?.goal?.trim() || "" : ""
82
+
83
+ // Detect "parent" benchmark pages — either an aggregated composite or a
84
+ // multi-metric matrix where each column is a subtask. In both cases the
85
+ // per-evaluation `metric_config.evaluation_description` belongs to whichever
86
+ // component was processed first (e.g. just the "airline" subset of Tau
87
+ // Bench 2) and would mislead a policy reader. Synthesize parent framing
88
+ // instead and surface the subtasks separately.
89
+ const isAggregated = summary.is_aggregated === true
90
+ const aggregateNames = (summary.aggregate_sources ?? [])
91
+ .map((s) => s.composite_benchmark_name)
92
+ .filter((s): s is string => typeof s === "string" && s.length > 0)
93
+
94
+ const subtaskLabels = useMemo(() => {
95
+ const seen = new Set<string>()
96
+ const labels: string[] = []
97
+ const add = (raw: string | undefined | null) => {
98
+ if (!raw) return
99
+ const trimmed = raw.trim()
100
+ if (!trimmed) return
101
+ const key = trimmed.toLowerCase()
102
+ if (seen.has(key)) return
103
+ seen.add(key)
104
+ labels.push(trimmed)
105
+ }
106
+ for (const subtask of summary.subtasks ?? []) {
107
+ add(subtask.display_name || subtask.subtask_name)
108
+ }
109
+ for (const metric of summary.leaderboard_metrics ?? []) {
110
+ if (metric.scope === "subtask") {
111
+ add(metric.subtask_name || metric.display_name)
112
+ }
113
+ }
114
+ for (const name of aggregateNames) add(name)
115
+ return labels
116
+ }, [summary.subtasks, summary.leaderboard_metrics, aggregateNames])
117
+
118
+ const isMatrix = (summary.leaderboard_metrics?.length ?? 0) > 1
119
+ const isParentPage = isAggregated || (isMatrix && subtaskLabels.length > 1)
120
+ const useComponentDescription = !isParentPage
121
+
122
+ const parentFallback = isParentPage && subtaskLabels.length > 1
123
+ ? `${summary.evaluation_name} reports results across ${subtaskLabels.length} ${
124
+ isAggregated ? "component benchmarks" : "subtasks"
125
+ }. Each is evaluated separately; the score shown is the ${
126
+ isAggregated ? "average" : "per-subtask result"
127
+ }.`
128
+ : null
129
+
130
+ const summaryText =
131
+ overview ||
132
+ goal ||
133
+ parentFallback ||
134
+ (useComponentDescription ? summary.metric_config.evaluation_description : summary.evaluation_name)
135
+
136
+ const [expanded, setExpanded] = useState(false)
137
+ const [subtasksOpen, setSubtasksOpen] = useState(false)
138
+ const isLong = summaryText.length > SUMMARY_PREVIEW_CHARS
139
+ const visibleText = expanded || !isLong
140
+ ? summaryText
141
+ : summaryText.slice(0, SUMMARY_PREVIEW_CHARS).replace(/\s+\S*$/, "") + "…"
142
+
143
+ const domains = useMemo(() => {
144
+ const fromTags = summary.tags?.domains ?? []
145
+ const fromCard = cardMatchesEval ? card?.benchmark_details?.domains ?? [] : []
146
+ return Array.from(new Set([...fromTags, ...fromCard].map((d) => d.trim()).filter(Boolean))).slice(0, 6)
147
+ }, [summary.tags?.domains, card?.benchmark_details?.domains, cardMatchesEval])
148
+
149
+ const languages = useMemo(() => {
150
+ const fromTags = summary.tags?.languages ?? []
151
+ const fromCard = cardMatchesEval ? card?.benchmark_details?.languages ?? [] : []
152
+ return Array.from(new Set([...fromTags, ...fromCard].map((d) => d.trim()).filter(Boolean))).slice(0, 4)
153
+ }, [summary.tags?.languages, card?.benchmark_details?.languages, cardMatchesEval])
154
+
155
+ const license = card?.ethical_and_legal_considerations?.data_licensing
156
+ const showLicense = license && license !== "Not specified"
157
+
158
+ const resources = useMemo(() => {
159
+ if (!cardMatchesEval) return []
160
+ const urls = (card?.benchmark_details?.resources ?? []).filter((r) => typeof r === "string" && r.startsWith("http"))
161
+ const seen = new Map<string, { kind: ReturnType<typeof classifyResource>["kind"]; label: string; url: string }>()
162
+ for (const url of urls) {
163
+ const c = classifyResource(url)
164
+ if (!seen.has(c.kind)) {
165
+ seen.set(c.kind, { ...c, url })
166
+ }
167
+ }
168
+ return Array.from(seen.values())
169
+ }, [card?.benchmark_details?.resources, cardMatchesEval])
170
+
171
+ const evaluators = (summary.evaluator_names ?? []).slice(0, 3)
172
+ const hasMoreEvaluators = (summary.evaluator_names?.length ?? 0) > evaluators.length
173
+
174
+ const knownIssues = useMemo(
175
+ () =>
176
+ getKnownIssues(
177
+ summary.evaluation_name,
178
+ summary.composite_benchmark_name,
179
+ summary.composite_benchmark_key,
180
+ summary.benchmark_family_key,
181
+ summary.benchmark_leaf_key,
182
+ card?.benchmark_details?.name,
183
+ ),
184
+ [
185
+ summary.evaluation_name,
186
+ summary.composite_benchmark_name,
187
+ summary.composite_benchmark_key,
188
+ summary.benchmark_family_key,
189
+ summary.benchmark_leaf_key,
190
+ card?.benchmark_details?.name,
191
+ ],
192
+ )
193
+
194
+ const directionLabel = summary.metric_config.lower_is_better
195
+ ? "Lower scores are better"
196
+ : "Higher scores are better"
197
+
198
+ return (
199
+ <section className="rounded-3xl border bg-card p-5 sm:p-6">
200
+ <header className="mb-4 flex flex-wrap items-center gap-2">
201
+ <BookOpen className="h-5 w-5 text-primary" />
202
+ <h2 className="text-lg font-semibold tracking-tight">{summary.evaluation_name}</h2>
203
+ <span className="text-xs text-muted-foreground">In plain language</span>
204
+ </header>
205
+
206
+ {knownIssues.length > 0 && (
207
+ <div className="mb-3">
208
+ <KnownIssuesPanel issues={knownIssues} variant="compact" />
209
+ </div>
210
+ )}
211
+
212
+ <p className="text-base leading-7 text-foreground/90">
213
+ {visibleText}
214
+ {isLong && (
215
+ <button
216
+ type="button"
217
+ onClick={() => setExpanded((v) => !v)}
218
+ className="ml-1 text-sm font-medium text-primary underline-offset-4 hover:underline"
219
+ >
220
+ {expanded ? "Show less" : "Read more"}
221
+ </button>
222
+ )}
223
+ </p>
224
+
225
+ <p className="mt-3 text-sm text-muted-foreground">
226
+ <SignalTooltip
227
+ content={
228
+ summary.metric_config.lower_is_better
229
+ ? "On this benchmark, a lower number means the model did better."
230
+ : "On this benchmark, a higher number means the model did better."
231
+ }
232
+ >
233
+ <span className="underline decoration-dotted decoration-muted-foreground/60 underline-offset-4 cursor-help">
234
+ {directionLabel}.
235
+ </span>
236
+ </SignalTooltip>{" "}
237
+ Compared across {summary.models_count} model{summary.models_count === 1 ? "" : "s"}.
238
+ </p>
239
+
240
+ {isParentPage && subtaskLabels.length > 1 && (
241
+ <div className="mt-4 rounded-2xl border border-border/60 bg-muted/10">
242
+ <button
243
+ type="button"
244
+ onClick={() => setSubtasksOpen((v) => !v)}
245
+ aria-expanded={subtasksOpen}
246
+ className="flex w-full items-center justify-between gap-3 px-3.5 py-2.5 text-left transition-colors hover:bg-muted/20 rounded-2xl"
247
+ >
248
+ <span className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
249
+ <Layers className="h-3.5 w-3.5" />
250
+ {isAggregated ? `Component benchmarks (${subtaskLabels.length})` : `Subtasks (${subtaskLabels.length})`}
251
+ </span>
252
+ {subtasksOpen ? (
253
+ <ChevronUp className="h-4 w-4 text-muted-foreground" />
254
+ ) : (
255
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
256
+ )}
257
+ </button>
258
+ {subtasksOpen && (
259
+ <ul className="grid list-disc gap-x-6 gap-y-1 px-3.5 pb-3.5 pl-9 text-sm sm:grid-cols-2 lg:grid-cols-3">
260
+ {subtaskLabels.map((name) => (
261
+ <li key={name} className="capitalize">
262
+ {name}
263
+ </li>
264
+ ))}
265
+ </ul>
266
+ )}
267
+ </div>
268
+ )}
269
+
270
+ {(domains.length > 0 || languages.length > 0 || showLicense) && (
271
+ <div className="mt-4 flex flex-wrap gap-2">
272
+ {domains.map((d) => (
273
+ <span
274
+ key={`d-${d}`}
275
+ className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-xs font-medium capitalize"
276
+ >
277
+ <Tag className="h-3 w-3 shrink-0 text-muted-foreground" />
278
+ {d}
279
+ </span>
280
+ ))}
281
+ {languages.map((l) => (
282
+ <span
283
+ key={`l-${l}`}
284
+ className="inline-flex items-center gap-1 rounded-full border border-sky-200/70 bg-sky-50/60 px-2.5 py-1 text-xs font-medium dark:border-sky-900/40 dark:bg-sky-950/20"
285
+ >
286
+ <Globe className="h-3 w-3 shrink-0 text-sky-600" />
287
+ {l}
288
+ </span>
289
+ ))}
290
+ {showLicense && (
291
+ <SignalTooltip content="The license under which the benchmark dataset is released.">
292
+ <span className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-background px-2.5 py-1 text-xs font-medium cursor-help">
293
+ <ScrollText className="h-3 w-3 shrink-0 text-muted-foreground" />
294
+ {license}
295
+ </span>
296
+ </SignalTooltip>
297
+ )}
298
+ </div>
299
+ )}
300
+
301
+ {(resources.length > 0 || evaluators.length > 0) && (
302
+ <div className="mt-5 border-t pt-4">
303
+ <div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
304
+ Where this comes from
305
+ </div>
306
+ <div className="flex flex-wrap items-center gap-3">
307
+ {resources.map((r) => (
308
+ <a
309
+ key={r.url}
310
+ href={r.url}
311
+ target="_blank"
312
+ rel="noreferrer"
313
+ className="inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
314
+ >
315
+ <FileText className="h-3 w-3 shrink-0" />
316
+ <span className="font-semibold text-foreground">{r.label}</span>
317
+ <span className="text-muted-foreground">{shortHost(r.url)}</span>
318
+ <ExternalLink className="h-3 w-3 shrink-0" />
319
+ </a>
320
+ ))}
321
+ {evaluators.length > 0 && (
322
+ <SignalTooltip
323
+ content={
324
+ <span className="block space-y-1">
325
+ <span className="block font-semibold">Who reported these scores</span>
326
+ <span className="block">{summary.evaluator_names?.join(", ")}</span>
327
+ {summary.third_party_ratio > 0 && (
328
+ <span className="block text-muted-foreground">
329
+ {Math.round(summary.third_party_ratio * 100)}% of results come from independent evaluators (not the model's own developer).
330
+ </span>
331
+ )}
332
+ </span>
333
+ }
334
+ >
335
+ <span className="inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-background px-3 py-1.5 text-xs font-medium cursor-help">
336
+ <Users className="h-3 w-3 shrink-0 text-muted-foreground" />
337
+ <span className="font-semibold text-foreground">Reported by</span>
338
+ <span className="text-muted-foreground">
339
+ {evaluators.join(", ")}
340
+ {hasMoreEvaluators ? ` +${(summary.evaluator_names?.length ?? 0) - evaluators.length} more` : ""}
341
+ </span>
342
+ </span>
343
+ </SignalTooltip>
344
+ )}
345
+ </div>
346
+ </div>
347
+ )}
348
+ </section>
349
+ )
350
+ }
components/researcher-reproducibility-card.tsx ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect, useState, type ReactNode } from "react"
4
+ import { AlertTriangle, ExternalLink, FlaskConical } from "lucide-react"
5
+ import { Term } from "@/components/term"
6
+ import { SignalTooltip } from "@/components/signals/signal-tooltip"
7
+ import type { ModelResultForBenchmark } from "@/lib/eval-processing"
8
+ import type { GenerationConfig, ScoreDetails } from "@/lib/benchmark-schema"
9
+
10
+ interface ResearcherReproducibilityCardProps {
11
+ modelResult: ModelResultForBenchmark
12
+ /**
13
+ * Benchmark identifier used to pick the right per-eval row when fetching
14
+ * enrichment from the model's full record. The eval-detail endpoint
15
+ * synthesizes leaderboard rows without `generation_config`, so we top up
16
+ * lazily on row expand from /api/eval-row-config.
17
+ */
18
+ benchmarkKey?: string
19
+ evalName?: string
20
+ }
21
+
22
+ const KNOWN_DECODING_KEYS = ["temperature", "top_p", "top_k", "max_tokens", "seed", "reasoning"] as const
23
+
24
+ // Keys that belong to agentic eval setups, surfaced in their own group rather
25
+ // than dumped under "decoding" extras as raw JSON.
26
+ const KNOWN_AGENT_KEYS = [
27
+ "agentic_eval_config",
28
+ "max_attempts",
29
+ "eval_limits",
30
+ "eval_plan",
31
+ "sandbox",
32
+ "max_turns",
33
+ "message_limit",
34
+ ] as const
35
+
36
+ const KEY_LABEL: Record<string, string> = {
37
+ temperature: "temperature",
38
+ top_p: "top-p",
39
+ top_k: "top-k",
40
+ max_tokens: "max tokens",
41
+ seed: "seed",
42
+ reasoning: "reasoning mode",
43
+ n: "samples per prompt",
44
+ best_of: "best-of-N",
45
+ num_samples: "samples per prompt",
46
+ num_runs: "runs",
47
+ n_shot: "n-shot",
48
+ num_fewshot: "few-shot examples",
49
+ fewshot: "few-shot examples",
50
+ agentic_eval_config: "tools available",
51
+ max_attempts: "max attempts",
52
+ eval_limits: "eval limits",
53
+ eval_plan: "eval plan",
54
+ sandbox: "sandbox",
55
+ max_turns: "max turns",
56
+ message_limit: "message limit",
57
+ }
58
+
59
+ /**
60
+ * Try to render a structured agentic config object as a short, readable
61
+ * string. Falls back to null so the caller can use the generic formatter.
62
+ */
63
+ function formatAgentValue(key: string, value: unknown): string | null {
64
+ if (value == null) return null
65
+ if (typeof value !== "object") return null
66
+
67
+ if (key === "agentic_eval_config") {
68
+ const tools = (value as { available_tools?: unknown }).available_tools
69
+ if (Array.isArray(tools)) {
70
+ const names = tools
71
+ .map((t) => (t && typeof t === "object" ? (t as { name?: unknown }).name : null))
72
+ .filter((n): n is string => typeof n === "string" && n.length > 0)
73
+ if (names.length === 0) return "no tools"
74
+ if (names.length <= 4) return `${names.length} tools: ${names.join(", ")}`
75
+ return `${names.length} tools: ${names.slice(0, 4).join(", ")} +${names.length - 4}`
76
+ }
77
+ }
78
+
79
+ if (key === "eval_limits") {
80
+ const obj = value as Record<string, unknown>
81
+ const parts: string[] = []
82
+ for (const k of ["message_limit", "max_messages", "token_limit", "max_tokens"]) {
83
+ if (typeof obj[k] === "number") parts.push(`${k.replace(/_/g, " ")}: ${obj[k]}`)
84
+ }
85
+ if (parts.length > 0) return parts.join(", ")
86
+ }
87
+
88
+ if (key === "eval_plan") {
89
+ const name = (value as { name?: unknown }).name
90
+ const steps = (value as { steps?: unknown }).steps
91
+ if (typeof name === "string" && Array.isArray(steps)) return `${name} (${steps.length} step${steps.length === 1 ? "" : "s"})`
92
+ if (typeof name === "string") return name
93
+ }
94
+
95
+ if (key === "sandbox") {
96
+ const keys = Object.keys(value as Record<string, unknown>)
97
+ if (keys.length === 0) return "default"
98
+ return keys.join(", ")
99
+ }
100
+
101
+ return null
102
+ }
103
+
104
+ function pickFromAdditional(value: unknown, keys: string[]): unknown | undefined {
105
+ if (!value || typeof value !== "object") return undefined
106
+ const obj = value as Record<string, unknown>
107
+ for (const k of keys) {
108
+ if (obj[k] !== undefined && obj[k] !== null) return obj[k]
109
+ }
110
+ return undefined
111
+ }
112
+
113
+ /**
114
+ * Renders any value as a short string for the parameter cards. Returns null
115
+ * when the value is empty so the caller can render "Not disclosed" instead.
116
+ */
117
+ function formatValue(value: unknown): string | null {
118
+ if (value === undefined || value === null) return null
119
+ if (typeof value === "boolean") return value ? "yes" : "no"
120
+ if (typeof value === "number") return Number.isInteger(value) ? value.toString() : value.toFixed(3)
121
+ if (typeof value === "string") {
122
+ const trimmed = value.trim()
123
+ return trimmed.length === 0 ? null : trimmed
124
+ }
125
+ try {
126
+ return JSON.stringify(value)
127
+ } catch {
128
+ return null
129
+ }
130
+ }
131
+
132
+ function ParamRow({
133
+ label,
134
+ value,
135
+ termKey,
136
+ hint,
137
+ }: {
138
+ label: string
139
+ value: ReactNode | null
140
+ termKey?: string
141
+ hint?: string
142
+ }) {
143
+ const isMissing = value === null || value === undefined
144
+ return (
145
+ <div className="flex items-baseline justify-between gap-3 border-b border-dashed border-border/50 py-1.5 text-sm last:border-0">
146
+ <span className="text-muted-foreground">
147
+ {termKey ? <Term term={termKey}>{label}</Term> : label}
148
+ </span>
149
+ {isMissing ? (
150
+ <SignalTooltip
151
+ content={
152
+ hint ??
153
+ "This parameter wasn't reported by the source. Without it, the result may not be exactly reproducible."
154
+ }
155
+ >
156
+ <span className="inline-flex items-center gap-1 text-xs font-medium text-amber-700 dark:text-amber-300 cursor-help">
157
+ <AlertTriangle className="h-3 w-3" /> Not disclosed
158
+ </span>
159
+ </SignalTooltip>
160
+ ) : (
161
+ <span className="font-medium tabular-nums">{value}</span>
162
+ )}
163
+ </div>
164
+ )
165
+ }
166
+
167
+ interface FieldSpec {
168
+ label: string
169
+ value: string | null
170
+ termKey?: string
171
+ hint?: string
172
+ }
173
+
174
+ interface FieldGroup {
175
+ title: string
176
+ fields: FieldSpec[]
177
+ }
178
+
179
+ /**
180
+ * Detailed reproducibility surface for researcher mode. Shown inside a
181
+ * leaderboard row's expanded panel. Adaptive: when most fields aren't disclosed
182
+ * (the common case today), it collapses to a compact "Limited disclosure"
183
+ * summary showing only fields that *are* present, with a button to reveal the
184
+ * full audit grid.
185
+ */
186
+ export function ResearcherReproducibilityCard({
187
+ modelResult,
188
+ benchmarkKey,
189
+ evalName,
190
+ }: ResearcherReproducibilityCardProps) {
191
+ const [enrichedGen, setEnrichedGen] = useState<GenerationConfig | null>(null)
192
+ const [enrichedScore, setEnrichedScore] = useState<ScoreDetails | null>(null)
193
+ const [loading, setLoading] = useState(false)
194
+
195
+ // Lazily top up generation_config / score_details from the model's full
196
+ // record. The eval-detail leaderboard endpoint omits these fields; we only
197
+ // pay the fetch cost when a researcher actually expands a row.
198
+ useEffect(() => {
199
+ const inlineGen = modelResult.result.generation_config
200
+ const inlineScoreOk =
201
+ modelResult.score_details.standard_error != null ||
202
+ modelResult.score_details.confidence_interval != null ||
203
+ modelResult.score_details.sample_size != null
204
+ const hasInlineArgs =
205
+ !!inlineGen &&
206
+ typeof inlineGen === "object" &&
207
+ "generation_args" in inlineGen &&
208
+ Object.keys((inlineGen as { generation_args?: Record<string, unknown> }).generation_args ?? {}).length > 0
209
+ if (hasInlineArgs && inlineScoreOk) return // nothing to fetch
210
+
211
+ const modelId = modelResult.model_info.id
212
+ if (!modelId) return
213
+
214
+ const params = new URLSearchParams({ model_id: modelId })
215
+ if (benchmarkKey) params.set("benchmark_key", benchmarkKey)
216
+ if (evalName) params.set("eval_name", evalName)
217
+
218
+ let cancelled = false
219
+ setLoading(true)
220
+ fetch(`/api/eval-row-config?${params.toString()}`)
221
+ .then((r) => (r.ok ? r.json() : null))
222
+ .then((data) => {
223
+ if (cancelled || !data) return
224
+ if (data.generation_config) setEnrichedGen(data.generation_config as GenerationConfig)
225
+ if (data.score_details) setEnrichedScore(data.score_details as ScoreDetails)
226
+ })
227
+ .catch(() => {})
228
+ .finally(() => {
229
+ if (!cancelled) setLoading(false)
230
+ })
231
+ return () => {
232
+ cancelled = true
233
+ }
234
+ }, [modelResult, benchmarkKey, evalName])
235
+
236
+ const gen = modelResult.result.generation_config ?? enrichedGen ?? undefined
237
+ const args = gen?.generation_args ?? {}
238
+ const scoreDetails = {
239
+ ...modelResult.score_details,
240
+ sample_size: modelResult.score_details.sample_size ?? enrichedScore?.sample_size,
241
+ standard_error: modelResult.score_details.standard_error ?? enrichedScore?.standard_error,
242
+ confidence_interval:
243
+ modelResult.score_details.confidence_interval ?? enrichedScore?.confidence_interval,
244
+ }
245
+ const additional =
246
+ typeof gen?.additional_details === "object" && gen?.additional_details !== null
247
+ ? (gen.additional_details as Record<string, unknown>)
248
+ : null
249
+
250
+ const argsKeys = Object.keys(args)
251
+ const extraDecodingKeys = argsKeys.filter(
252
+ (k) =>
253
+ !KNOWN_DECODING_KEYS.includes(k as (typeof KNOWN_DECODING_KEYS)[number]) &&
254
+ !KNOWN_AGENT_KEYS.includes(k as (typeof KNOWN_AGENT_KEYS)[number])
255
+ )
256
+ const agentKeysPresent = KNOWN_AGENT_KEYS.filter((k) => args[k] != null)
257
+ const hasAgentSetup = agentKeysPresent.length > 0
258
+
259
+ const shots = pickFromAdditional(additional, ["num_fewshot", "n_shot", "shots", "fewshot"])
260
+ const samplesPerPrompt = pickFromAdditional(additional, ["n", "num_samples", "samples_per_prompt"]) ?? args["n"]
261
+ const bestOf = pickFromAdditional(additional, ["best_of", "best_of_n"]) ?? args["best_of"]
262
+ const numRuns = pickFromAdditional(additional, ["num_runs", "n_runs", "runs"])
263
+ const scoringMethod = pickFromAdditional(additional, ["scoring", "scoring_method", "judge", "evaluator"])
264
+ const evalLibrary = pickFromAdditional(additional, ["eval_library", "harness", "framework"])
265
+ const evalLibraryVersion = pickFromAdditional(additional, ["eval_library_version", "harness_version"])
266
+ const promptTemplate = gen?.prompt_template?.trim() || null
267
+
268
+ const groups: FieldGroup[] = [
269
+ {
270
+ title: "Decoding",
271
+ fields: [
272
+ {
273
+ label: "temperature",
274
+ termKey: "temperature",
275
+ value: formatValue(args.temperature),
276
+ hint: "Temperature controls randomness — without it, others can't recreate the same outputs.",
277
+ },
278
+ { label: "top-p", termKey: "top-p", value: formatValue(args.top_p) },
279
+ { label: "top-k", termKey: "top-k", value: formatValue(args.top_k) },
280
+ { label: "max tokens", value: formatValue(args.max_tokens) },
281
+ { label: "seed", value: formatValue(args.seed) },
282
+ ...extraDecodingKeys.map((k) => ({
283
+ label: KEY_LABEL[k] ?? k.replace(/_/g, " "),
284
+ value: formatValue(args[k]),
285
+ })),
286
+ ],
287
+ },
288
+ {
289
+ title: "Sampling",
290
+ fields: [
291
+ {
292
+ label: "few-shot examples",
293
+ termKey: "few-shot",
294
+ value: formatValue(shots),
295
+ hint: "How many worked examples were included in the prompt before the question.",
296
+ },
297
+ {
298
+ label: "samples per prompt",
299
+ value: formatValue(samplesPerPrompt),
300
+ hint: "Number of completions generated per question.",
301
+ },
302
+ {
303
+ label: "best-of-N",
304
+ termKey: "best-of-n",
305
+ value: formatValue(bestOf),
306
+ hint: "Whether the score reflects the best of multiple attempts (inflates results vs. single-attempt).",
307
+ },
308
+ {
309
+ label: "runs averaged",
310
+ value: formatValue(numRuns),
311
+ hint: "How many evaluation runs were averaged to produce the reported number.",
312
+ },
313
+ {
314
+ label: "test instances",
315
+ value: formatValue(scoreDetails.sample_size),
316
+ hint: "Number of items in the test set the model was scored on.",
317
+ },
318
+ ],
319
+ },
320
+ ...(hasAgentSetup
321
+ ? [
322
+ {
323
+ title: "Agent setup",
324
+ fields: agentKeysPresent.map((k) => ({
325
+ label: KEY_LABEL[k] ?? k.replace(/_/g, " "),
326
+ value: formatAgentValue(k, args[k]) ?? formatValue(args[k]),
327
+ hint:
328
+ k === "agentic_eval_config"
329
+ ? "Tools the agent could call during the run."
330
+ : k === "max_attempts"
331
+ ? "Maximum independent attempts the agent gets per task."
332
+ : k === "eval_limits"
333
+ ? "Hard caps the harness enforced on the run (messages, tokens, etc.)."
334
+ : k === "eval_plan"
335
+ ? "Solver/plan the harness used to drive the agent."
336
+ : k === "sandbox"
337
+ ? "Environment in which the agent ran (e.g. docker, local)."
338
+ : undefined,
339
+ })),
340
+ } as FieldGroup,
341
+ ]
342
+ : []),
343
+ {
344
+ title: "Scoring & uncertainty",
345
+ fields: [
346
+ {
347
+ label: "scoring method",
348
+ value: formatValue(scoringMethod),
349
+ hint: "Exact match, LLM-as-judge, human grading, etc. Determines what 'correct' means.",
350
+ },
351
+ { label: "standard error", value: formatValue(scoreDetails.standard_error) },
352
+ {
353
+ label: "confidence interval",
354
+ value: scoreDetails.confidence_interval
355
+ ? `${scoreDetails.confidence_interval.lower}–${scoreDetails.confidence_interval.upper} (${scoreDetails.confidence_interval.confidence_level}%)`
356
+ : null,
357
+ },
358
+ { label: "eval library", value: formatValue(evalLibrary) },
359
+ { label: "library version", value: formatValue(evalLibraryVersion) },
360
+ ],
361
+ },
362
+ ]
363
+
364
+ const totalFields = groups.reduce((n, g) => n + g.fields.length, 0)
365
+ const disclosedFields = groups.reduce(
366
+ (n, g) => n + g.fields.filter((f) => f.value !== null).length,
367
+ 0
368
+ )
369
+ const disclosureRatio = totalFields > 0 ? disclosedFields / totalFields : 0
370
+ const disclosedGroups = groups
371
+ .map((g) => ({ ...g, fields: g.fields.filter((f) => f.value !== null) }))
372
+ .filter((g) => g.fields.length > 0)
373
+
374
+ // If fewer than ~30% of fields are disclosed (and at least one is missing),
375
+ // start in compact mode so the card isn't a wall of "Not disclosed".
376
+ const shouldStartCompact = disclosureRatio < 0.3 && disclosedFields < totalFields
377
+ const [showAll, setShowAll] = useState(!shouldStartCompact)
378
+ const isCompact = shouldStartCompact && !showAll
379
+
380
+ return (
381
+ <section className="rounded-2xl border bg-background/70 p-4">
382
+ <header className="mb-3 flex items-start justify-between gap-3">
383
+ <div className="flex items-start gap-2 min-w-0">
384
+ <FlaskConical className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
385
+ <div className="min-w-0">
386
+ <div className="text-sm font-semibold">Reproducibility</div>
387
+ <div className="text-xs text-muted-foreground">
388
+ {isCompact
389
+ ? `Limited disclosure — only ${disclosedFields} of ${totalFields} reproducibility fields are reported.`
390
+ : "Everything someone would need to re-run this evaluation. Missing fields are flagged."}
391
+ </div>
392
+ </div>
393
+ </div>
394
+ <span className="shrink-0 rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[11px] font-medium tabular-nums text-muted-foreground">
395
+ {loading ? "loading…" : `${disclosedFields}/${totalFields} disclosed`}
396
+ </span>
397
+ </header>
398
+
399
+ {isCompact ? (
400
+ <>
401
+ {disclosedGroups.length > 0 ? (
402
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
403
+ {disclosedGroups.map((g) => (
404
+ <div key={g.title}>
405
+ <div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
406
+ {g.title}
407
+ </div>
408
+ {g.fields.map((f) => (
409
+ <ParamRow key={f.label} label={f.label} termKey={f.termKey} value={f.value} hint={f.hint} />
410
+ ))}
411
+ </div>
412
+ ))}
413
+ </div>
414
+ ) : (
415
+ <div className="rounded-xl border border-dashed border-amber-300/60 bg-amber-50/40 px-3 py-2 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/20 dark:text-amber-200">
416
+ No reproducibility metadata was disclosed by the source.
417
+ </div>
418
+ )}
419
+ </>
420
+ ) : (
421
+ <div className="grid gap-4 lg:grid-cols-3">
422
+ {groups.map((g) => (
423
+ <div key={g.title}>
424
+ <div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
425
+ {g.title}
426
+ </div>
427
+ {g.fields.map((f) => (
428
+ <ParamRow key={f.label} label={f.label} termKey={f.termKey} value={f.value} hint={f.hint} />
429
+ ))}
430
+ </div>
431
+ ))}
432
+ </div>
433
+ )}
434
+
435
+ {shouldStartCompact && (
436
+ <button
437
+ type="button"
438
+ onClick={() => setShowAll((v) => !v)}
439
+ className="mt-3 inline-flex items-center text-xs font-medium text-primary underline-offset-4 hover:underline"
440
+ >
441
+ {showAll
442
+ ? "Hide undisclosed fields"
443
+ : `Show all ${totalFields} checked fields (${totalFields - disclosedFields} not disclosed)`}
444
+ </button>
445
+ )}
446
+
447
+ {promptTemplate && (
448
+ <details className="mt-4 rounded-xl border bg-muted/10">
449
+ <summary className="cursor-pointer px-3 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
450
+ Prompt template
451
+ </summary>
452
+ <pre className="max-h-[18rem] overflow-auto whitespace-pre-wrap break-words border-t bg-background/60 px-3 py-3 text-xs leading-5">
453
+ {promptTemplate}
454
+ </pre>
455
+ </details>
456
+ )}
457
+ {!promptTemplate && !isCompact && (
458
+ <div className="mt-4 flex items-center gap-1.5 rounded-xl border border-dashed border-amber-300/60 bg-amber-50/40 px-3 py-2 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/20 dark:text-amber-200">
459
+ <AlertTriangle className="h-3.5 w-3.5" />
460
+ <span>
461
+ Prompt template not disclosed by the source.
462
+ <SignalTooltip content="Without the prompt, scores can't be reliably reproduced because phrasing changes results.">
463
+ <span className="ml-1 underline decoration-dotted underline-offset-4 cursor-help">Why this matters</span>
464
+ </SignalTooltip>
465
+ </span>
466
+ </div>
467
+ )}
468
+
469
+ {modelResult.source_metadata.source_url && (
470
+ <div className="mt-3 text-xs">
471
+ <a
472
+ href={modelResult.source_metadata.source_url}
473
+ target="_blank"
474
+ rel="noreferrer"
475
+ className="inline-flex items-center gap-1 text-primary underline-offset-4 hover:underline"
476
+ >
477
+ View original source <ExternalLink className="h-3 w-3" />
478
+ </a>
479
+ </div>
480
+ )}
481
+ </section>
482
+ )
483
+ }
components/signals/comparability-panel.tsx CHANGED
@@ -1,11 +1,12 @@
1
  "use client"
2
 
3
  import type { ReactNode } from "react"
4
- import { ChevronDown, GitCompareArrows, UsersRound } from "lucide-react"
5
 
6
  import { useAudienceMode } from "@/components/audience-mode-provider"
7
  import { Badge } from "@/components/ui/badge"
8
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
 
9
  import type { BenchmarkComparability, ComparabilitySummary, DifferingSetupField } from "@/lib/backend-artifacts"
10
  import {
11
  formatFieldLabel,
@@ -31,7 +32,10 @@ export function ComparabilityPanel({
31
  }
32
 
33
  return (
34
- <section className="rounded-2xl border border-border/70 bg-background/70 p-4 sm:p-5">
 
 
 
35
  <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
36
  <div className="space-y-1">
37
  <div className="flex items-center gap-2">
@@ -60,44 +64,54 @@ export function ComparabilityPanel({
60
  </div>
61
  )}
62
 
63
- <div className="mt-4 grid gap-3 lg:grid-cols-2">
64
- {variantGroups.length > 0 && (
65
- <GroupList
66
- icon="variant"
67
- title="Variant divergence"
68
- count={variantGroups.length}
69
- >
70
- {variantGroups.slice(0, 8).map((group) => (
71
- <DivergenceGroupItem
72
- key={group.group_id}
73
- modelRouteId={group.model_route_id}
74
- magnitude={group.divergence_magnitude}
75
- threshold={group.threshold_used}
76
- fields={group.differing_setup_fields}
77
- />
78
- ))}
79
- </GroupList>
80
- )}
 
 
 
 
 
 
 
81
 
82
- {crossPartyGroups.length > 0 && (
83
- <GroupList
84
- icon="cross-party"
85
- title="Cross-party divergence"
86
- count={crossPartyGroups.length}
87
- >
88
- {crossPartyGroups.slice(0, 8).map((group) => (
89
- <DivergenceGroupItem
90
- key={group.group_id}
91
- modelRouteId={group.model_route_id}
92
- magnitude={group.divergence_magnitude}
93
- threshold={group.threshold_used}
94
- fields={group.differing_setup_fields}
95
- scoresByOrganization={group.scores_by_organization}
96
- />
97
- ))}
98
- </GroupList>
99
- )}
100
- </div>
 
 
 
101
  </section>
102
  )
103
  }
@@ -107,11 +121,13 @@ function GroupList({
107
  title,
108
  count,
109
  children,
 
110
  }: {
111
  icon: "variant" | "cross-party"
112
  title: string
113
  count: number
114
  children: ReactNode
 
115
  }) {
116
  const Icon = icon === "variant" ? GitCompareArrows : UsersRound
117
 
@@ -130,13 +146,58 @@ function GroupList({
130
  <ChevronDown className="h-4 w-4 text-muted-foreground" />
131
  </button>
132
  </CollapsibleTrigger>
133
- <CollapsibleContent className="mt-2 space-y-2">
134
  {children}
135
  </CollapsibleContent>
136
  </Collapsible>
137
  )
138
  }
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  function DivergenceGroupItem({
141
  modelRouteId,
142
  magnitude,
@@ -150,31 +211,80 @@ function DivergenceGroupItem({
150
  fields: DifferingSetupField[]
151
  scoresByOrganization?: Record<string, number>
152
  }) {
 
 
 
 
 
 
 
 
 
 
153
  return (
154
  <a
155
  href={`#row-${modelRouteId}`}
156
- className="block rounded-xl border border-border/60 bg-background px-3 py-2 text-sm transition-colors hover:bg-muted/20"
157
  >
158
  <div className="flex items-start justify-between gap-3">
159
  <div className="min-w-0">
160
- <div className="font-medium">{modelRouteId}</div>
 
 
 
 
 
161
  <div className="mt-1 text-xs text-muted-foreground">
162
- Divergence {formatSignalNumber(magnitude)}; threshold {formatSignalNumber(threshold)}
163
  </div>
164
  </div>
165
  <span className="shrink-0 text-xs font-medium text-primary">Jump to row</span>
166
  </div>
167
 
168
- {fields.length > 0 && (
169
- <div className="mt-2 space-y-1 text-xs text-muted-foreground">
170
- {fields.slice(0, 3).map((field) => (
171
- <div key={field.field}>
172
- <span className="font-medium text-foreground">{formatFieldLabel(field.field)}:</span>{" "}
173
- {field.values.map(formatSignalValue).join(", ")}
 
174
  </div>
175
- ))}
176
- </div>
177
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  {scoresByOrganization && Object.keys(scoresByOrganization).length > 0 && (
180
  <div className="mt-2 flex flex-wrap gap-1.5">
 
1
  "use client"
2
 
3
  import type { ReactNode } from "react"
4
+ import { ChevronDown, GitCompareArrows, Info, UsersRound } from "lucide-react"
5
 
6
  import { useAudienceMode } from "@/components/audience-mode-provider"
7
  import { Badge } from "@/components/ui/badge"
8
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
9
+ import { SignalTooltip } from "@/components/signals/signal-tooltip"
10
  import type { BenchmarkComparability, ComparabilitySummary, DifferingSetupField } from "@/lib/backend-artifacts"
11
  import {
12
  formatFieldLabel,
 
32
  }
33
 
34
  return (
35
+ <section
36
+ id="comparability-panel"
37
+ className="rounded-2xl border border-border/70 bg-background/70 p-4 sm:p-5 scroll-mt-24"
38
+ >
39
  <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
40
  <div className="space-y-1">
41
  <div className="flex items-center gap-2">
 
64
  </div>
65
  )}
66
 
67
+ {(() => {
68
+ const onlyOne =
69
+ (variantGroups.length > 0 ? 1 : 0) + (crossPartyGroups.length > 0 ? 1 : 0) === 1
70
+ const sectionClass = onlyOne ? "" : "lg:grid lg:grid-cols-2 lg:gap-3"
71
+ const itemsClass = onlyOne ? "grid gap-2 md:grid-cols-2" : "space-y-2"
72
+ return (
73
+ <div className={`mt-4 ${sectionClass}`}>
74
+ {variantGroups.length > 0 && (
75
+ <GroupList
76
+ icon="variant"
77
+ title="Variant divergence"
78
+ count={variantGroups.length}
79
+ itemsClassName={itemsClass}
80
+ >
81
+ {variantGroups.slice(0, 8).map((group) => (
82
+ <DivergenceGroupItem
83
+ key={group.group_id}
84
+ modelRouteId={group.model_route_id}
85
+ magnitude={group.divergence_magnitude}
86
+ threshold={group.threshold_used}
87
+ fields={group.differing_setup_fields}
88
+ />
89
+ ))}
90
+ </GroupList>
91
+ )}
92
 
93
+ {crossPartyGroups.length > 0 && (
94
+ <GroupList
95
+ icon="cross-party"
96
+ title="Cross-party divergence"
97
+ count={crossPartyGroups.length}
98
+ itemsClassName={itemsClass}
99
+ >
100
+ {crossPartyGroups.slice(0, 8).map((group) => (
101
+ <DivergenceGroupItem
102
+ key={group.group_id}
103
+ modelRouteId={group.model_route_id}
104
+ magnitude={group.divergence_magnitude}
105
+ threshold={group.threshold_used}
106
+ fields={group.differing_setup_fields}
107
+ scoresByOrganization={group.scores_by_organization}
108
+ />
109
+ ))}
110
+ </GroupList>
111
+ )}
112
+ </div>
113
+ )
114
+ })()}
115
  </section>
116
  )
117
  }
 
121
  title,
122
  count,
123
  children,
124
+ itemsClassName = "space-y-2",
125
  }: {
126
  icon: "variant" | "cross-party"
127
  title: string
128
  count: number
129
  children: ReactNode
130
+ itemsClassName?: string
131
  }) {
132
  const Icon = icon === "variant" ? GitCompareArrows : UsersRound
133
 
 
146
  <ChevronDown className="h-4 w-4 text-muted-foreground" />
147
  </button>
148
  </CollapsibleTrigger>
149
+ <CollapsibleContent className={`mt-2 ${itemsClassName}`}>
150
  {children}
151
  </CollapsibleContent>
152
  </Collapsible>
153
  )
154
  }
155
 
156
+ /**
157
+ * Try to extract a human-readable label from a structured setup-field value.
158
+ * Common shape from agentic evals: { additional_details: { agent_name, agent_framework } }.
159
+ * Falls back to picking the first short string property, or null when the
160
+ * value can't be summarized cleanly.
161
+ */
162
+ function extractFriendlyLabel(value: unknown): string | null {
163
+ if (value == null) return null
164
+ if (typeof value === "string") return value.length > 60 ? null : value
165
+ if (typeof value === "number" || typeof value === "boolean") return String(value)
166
+ if (typeof value !== "object") return null
167
+
168
+ const obj = value as Record<string, unknown>
169
+ const details = (obj.additional_details && typeof obj.additional_details === "object")
170
+ ? (obj.additional_details as Record<string, unknown>)
171
+ : obj
172
+
173
+ const agentName = typeof details.agent_name === "string" ? details.agent_name : null
174
+ const agentFramework = typeof details.agent_framework === "string" ? details.agent_framework : null
175
+ if (agentName) {
176
+ return agentFramework && agentFramework !== agentName ? `${agentName} (${agentFramework})` : agentName
177
+ }
178
+
179
+ for (const key of ["name", "label", "id", "title"]) {
180
+ const v = details[key]
181
+ if (typeof v === "string" && v.length <= 60) return v
182
+ }
183
+
184
+ return null
185
+ }
186
+
187
+ function chipsForFieldValues(values: unknown[]): { label: string; raw: unknown }[] {
188
+ const seen = new Set<string>()
189
+ const result: { label: string; raw: unknown }[] = []
190
+ for (const v of values) {
191
+ const friendly = extractFriendlyLabel(v)
192
+ const label = friendly ?? formatSignalValue(v)
193
+ const truncated = label.length > 80 ? label.slice(0, 80) + "…" : label
194
+ if (seen.has(truncated)) continue
195
+ seen.add(truncated)
196
+ result.push({ label: truncated, raw: v })
197
+ }
198
+ return result
199
+ }
200
+
201
  function DivergenceGroupItem({
202
  modelRouteId,
203
  magnitude,
 
211
  fields: DifferingSetupField[]
212
  scoresByOrganization?: Record<string, number>
213
  }) {
214
+ const fieldLabels = fields.map((f) => formatFieldLabel(f.field))
215
+ const summarySentence =
216
+ fieldLabels.length === 0
217
+ ? `Reported scores diverge by ${formatSignalNumber(magnitude)}, above the ${formatSignalNumber(threshold)} threshold. The setup difference is not labelled.`
218
+ : `Reported scores diverge by ${formatSignalNumber(magnitude)} (threshold ${formatSignalNumber(threshold)}) because the runs differ on ${
219
+ fieldLabels.length === 1
220
+ ? fieldLabels[0]
221
+ : fieldLabels.slice(0, -1).join(", ") + " and " + fieldLabels[fieldLabels.length - 1]
222
+ }. The chips below show each variant.`
223
+
224
  return (
225
  <a
226
  href={`#row-${modelRouteId}`}
227
+ className="block rounded-xl border border-border/60 bg-background px-3 py-2.5 text-sm transition-colors hover:bg-muted/20"
228
  >
229
  <div className="flex items-start justify-between gap-3">
230
  <div className="min-w-0">
231
+ <div className="flex items-center gap-1.5">
232
+ <span className="font-medium">{modelRouteId}</span>
233
+ <SignalTooltip content={summarySentence}>
234
+ <Info className="h-3.5 w-3.5 shrink-0 cursor-help text-muted-foreground" />
235
+ </SignalTooltip>
236
+ </div>
237
  <div className="mt-1 text-xs text-muted-foreground">
238
+ Diverges by {formatSignalNumber(magnitude)} (threshold {formatSignalNumber(threshold)})
239
  </div>
240
  </div>
241
  <span className="shrink-0 text-xs font-medium text-primary">Jump to row</span>
242
  </div>
243
 
244
+ {fields.slice(0, 3).map((field) => {
245
+ const chips = chipsForFieldValues(field.values).slice(0, 6)
246
+ const overflow = field.values.length - chips.length
247
+ return (
248
+ <div key={field.field} className="mt-2">
249
+ <div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
250
+ Differs by {formatFieldLabel(field.field)}
251
  </div>
252
+ <div className="flex flex-wrap gap-1.5">
253
+ {chips.map((chip, idx) => {
254
+ const friendly = extractFriendlyLabel(chip.raw)
255
+ const tooltipBody = friendly ? formatSignalValue(chip.raw) : null
256
+ const pill = (
257
+ <span
258
+ className="inline-flex max-w-[18rem] items-center rounded-full border border-border/60 bg-muted/20 px-2 py-0.5 text-[11px] text-foreground/90 truncate"
259
+ title={!tooltipBody ? chip.label : undefined}
260
+ >
261
+ {chip.label}
262
+ </span>
263
+ )
264
+ return tooltipBody ? (
265
+ <SignalTooltip
266
+ key={`${field.field}-${idx}`}
267
+ content={
268
+ <span className="block max-w-[24rem] break-all font-mono text-[10px] leading-snug">
269
+ {tooltipBody}
270
+ </span>
271
+ }
272
+ >
273
+ {pill}
274
+ </SignalTooltip>
275
+ ) : (
276
+ <span key={`${field.field}-${idx}`}>{pill}</span>
277
+ )
278
+ })}
279
+ {overflow > 0 && (
280
+ <span className="inline-flex items-center rounded-full border border-dashed border-border/60 px-2 py-0.5 text-[11px] text-muted-foreground">
281
+ +{overflow} more
282
+ </span>
283
+ )}
284
+ </div>
285
+ </div>
286
+ )
287
+ })}
288
 
289
  {scoresByOrganization && Object.keys(scoresByOrganization).length > 0 && (
290
  <div className="mt-2 flex flex-wrap gap-1.5">
components/signals/reproducibility-panel.tsx CHANGED
@@ -22,7 +22,7 @@ export function ReproducibilityPanel({
22
  <div className="rounded-2xl border bg-background/70 p-4">
23
  <div className="mb-4 flex items-start gap-2">
24
  <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-300" />
25
- <div>
26
  <div className="font-semibold">
27
  {isResearchView ? "Reproducibility" : "Re-runnability"}
28
  </div>
 
22
  <div className="rounded-2xl border bg-background/70 p-4">
23
  <div className="mb-4 flex items-start gap-2">
24
  <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-300" />
25
+ <div className="min-w-0 flex-1">
26
  <div className="font-semibold">
27
  {isResearchView ? "Reproducibility" : "Re-runnability"}
28
  </div>
components/signals/row-signals-compact.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { AlertTriangle, ShieldCheck } from "lucide-react"
4
+ import { useAudienceMode } from "@/components/audience-mode-provider"
5
+ import type { RowAnnotations } from "@/lib/backend-artifacts"
6
+ import { cn } from "@/lib/utils"
7
+ import { SignalTooltip } from "./signal-tooltip"
8
+ import { formatMissingField } from "./signal-utils"
9
+ import { getRelationshipShortLabel } from "./provenance-badge"
10
+
11
+ /**
12
+ * Compact, single-icon row signal indicator. Shows a warning icon when any
13
+ * row-level concern fires (reproducibility gap, first-party-only reporting),
14
+ * with a tooltip listing the specifics. Replaces the two full coloured badges
15
+ * that were taking up vertical space under every model name.
16
+ *
17
+ * Returns null when there are no concerns, so green rows stay quiet.
18
+ */
19
+ export function RowSignalsCompact({
20
+ annotations,
21
+ className,
22
+ showWhenClean = false,
23
+ }: {
24
+ annotations?: RowAnnotations | null
25
+ className?: string
26
+ showWhenClean?: boolean
27
+ }) {
28
+ const { mode } = useAudienceMode()
29
+
30
+ if (!annotations) return null
31
+
32
+ const reproGap = annotations.reproducibility_gap
33
+ const provenance = annotations.provenance
34
+ const hasReproGap = reproGap?.has_reproducibility_gap === true
35
+ const firstPartyOnly = provenance?.first_party_only === true
36
+
37
+ const concerns: { title: string; detail: string }[] = []
38
+
39
+ if (hasReproGap && reproGap) {
40
+ const missing = reproGap.missing_fields.map(formatMissingField)
41
+ concerns.push({
42
+ title: mode === "policy" ? "Setup not documented" : "Reproducibility gap",
43
+ detail:
44
+ mode === "policy"
45
+ ? `${reproGap.populated_field_count} of ${reproGap.required_field_count} setup fields recorded. This score may be hard to re-run.`
46
+ : `Missing: ${missing.join(", ") || "none listed"}. ${reproGap.populated_field_count} of ${reproGap.required_field_count} setup fields recorded.`,
47
+ })
48
+ }
49
+
50
+ if (firstPartyOnly) {
51
+ concerns.push({
52
+ title:
53
+ mode === "policy" ? "Only model developer reported" : `${getRelationshipShortLabel("first_party", mode)} only`,
54
+ detail:
55
+ mode === "policy"
56
+ ? "Only the model developer reported this score; no independent replication is recorded."
57
+ : "First-party only — no independent replication is recorded for this group.",
58
+ })
59
+ }
60
+
61
+ if (concerns.length === 0) {
62
+ if (!showWhenClean) return null
63
+ return (
64
+ <SignalTooltip content="No row-level concerns flagged for this score.">
65
+ <span className={cn("inline-flex h-5 w-5 items-center justify-center", className)}>
66
+ <ShieldCheck className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
67
+ </span>
68
+ </SignalTooltip>
69
+ )
70
+ }
71
+
72
+ return (
73
+ <SignalTooltip
74
+ content={
75
+ <span className="block space-y-1.5">
76
+ <span className="block font-semibold">{concerns.length === 1 ? "1 concern" : `${concerns.length} concerns`}</span>
77
+ {concerns.map((c) => (
78
+ <span key={c.title} className="block">
79
+ <span className="font-medium">{c.title}.</span>{" "}
80
+ <span className="text-muted-foreground">{c.detail}</span>
81
+ </span>
82
+ ))}
83
+ </span>
84
+ }
85
+ >
86
+ <span
87
+ className={cn(
88
+ "inline-flex h-5 items-center gap-1 rounded-full bg-amber-50 px-1.5 text-amber-700 dark:bg-amber-950/40 dark:text-amber-200 cursor-help",
89
+ className
90
+ )}
91
+ aria-label={`${concerns.length} reporting concern${concerns.length === 1 ? "" : "s"} for this row`}
92
+ >
93
+ <AlertTriangle className="h-3 w-3" />
94
+ {concerns.length > 1 && <span className="text-[10px] font-semibold leading-none">{concerns.length}</span>}
95
+ </span>
96
+ </SignalTooltip>
97
+ )
98
+ }
components/term.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import type { ReactNode } from "react"
4
+ import { lookupTerm } from "@/lib/glossary"
5
+ import { SignalTooltip } from "@/components/signals/signal-tooltip"
6
+
7
+ interface TermProps {
8
+ /**
9
+ * Glossary key to look up. Defaults to the visible text.
10
+ */
11
+ term?: string
12
+ children: ReactNode
13
+ /**
14
+ * Override the tooltip body. If omitted, glossary entry is used.
15
+ */
16
+ explain?: ReactNode
17
+ className?: string
18
+ }
19
+
20
+ export function Term({ term, children, explain, className }: TermProps) {
21
+ const key = term ?? (typeof children === "string" ? children : undefined)
22
+ const entry = key ? lookupTerm(key) : undefined
23
+
24
+ const content = explain ?? (entry ? (
25
+ <span className="block space-y-1">
26
+ <span className="block">{entry.short}</span>
27
+ {entry.long ? <span className="block text-muted-foreground">{entry.long}</span> : null}
28
+ </span>
29
+ ) : null)
30
+
31
+ if (!content) {
32
+ return <span className={className}>{children}</span>
33
+ }
34
+
35
+ return (
36
+ <SignalTooltip content={content}>
37
+ <span
38
+ className={
39
+ "underline decoration-dotted decoration-muted-foreground/60 underline-offset-4 cursor-help " +
40
+ (className ?? "")
41
+ }
42
+ >
43
+ {children}
44
+ </span>
45
+ </SignalTooltip>
46
+ )
47
+ }
lib/eval-processing.ts CHANGED
@@ -117,6 +117,8 @@ export interface ModelResultForBenchmark {
117
  source_metadata: SourceMetadata
118
  source_data: BenchmarkEvaluation['source_data']
119
  result: EvaluationResult
 
 
120
  aggregate_components?: Array<{
121
  evaluation_id: string
122
  composite_benchmark_key: string
 
117
  source_metadata: SourceMetadata
118
  source_data: BenchmarkEvaluation['source_data']
119
  result: EvaluationResult
120
+ /** URL to the underlying record JSON in the upstream HF dataset, when known. */
121
+ source_record_url?: string
122
  aggregate_components?: Array<{
123
  evaluation_id: string
124
  composite_benchmark_key: string
lib/glossary.ts ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface GlossaryEntry {
2
+ short: string
3
+ long?: string
4
+ }
5
+
6
+ const ENTRIES: Record<string, GlossaryEntry> = {
7
+ "0-shot": {
8
+ short: "Model answers without seeing any worked examples first.",
9
+ long: "The benchmark gives the model only the question. Tests how the model performs cold, without in-context demonstrations.",
10
+ },
11
+ "zero-shot": {
12
+ short: "Model answers without seeing any worked examples first.",
13
+ },
14
+ "1-shot": {
15
+ short: "Model sees one worked example before answering.",
16
+ },
17
+ "5-shot": {
18
+ short: "Model sees five worked examples before answering.",
19
+ long: "The five examples are included in the prompt as demonstrations. More shots usually raise scores; comparing a 0-shot result to a 5-shot result is not apples-to-apples.",
20
+ },
21
+ "few-shot": {
22
+ short: "Model sees a small number of worked examples before answering.",
23
+ },
24
+ "n-shot": {
25
+ short: "Number of worked examples shown to the model in the prompt.",
26
+ },
27
+ "pass@1": {
28
+ short: "The model gets one attempt; counted correct only if that single attempt passes.",
29
+ long: "Common in code benchmarks. Stricter than pass@k for k>1, since the model cannot retry.",
30
+ },
31
+ "pass@16": {
32
+ short: "The model gets 16 attempts; counted correct if any one of them passes.",
33
+ long: "Higher numbers tend to be inflated relative to pass@1 because the model has many tries.",
34
+ },
35
+ "pass@k": {
36
+ short: "The model gets k attempts; counted correct if any one of them passes.",
37
+ },
38
+ "majority voting": {
39
+ short: "The model answers many times; the most common answer is taken as the final answer.",
40
+ long: "Also called self-consistency. Tends to raise scores on reasoning tasks but costs more compute per question.",
41
+ },
42
+ "self-consistency": {
43
+ short: "The model answers many times; the most common answer is taken as the final answer.",
44
+ },
45
+ "best-of-n": {
46
+ short: "The model produces N answers; the best one (by a scorer or oracle) is reported.",
47
+ long: "Inflates scores compared to a single attempt. Worth flagging when comparing models.",
48
+ },
49
+ "exact match": {
50
+ short: "The model's answer is counted correct only if it matches the reference string exactly.",
51
+ long: "Strict — small formatting differences fail. Often used for short-answer tasks.",
52
+ },
53
+ "llm-as-judge": {
54
+ short: "Another language model grades the answers.",
55
+ long: "Faster than human grading but the judge model can have biases (length, style, self-preference). Worth knowing the judge identity.",
56
+ },
57
+ "llm as judge": {
58
+ short: "Another language model grades the answers.",
59
+ },
60
+ "human-in-the-loop": {
61
+ short: "Human graders score or check the model's answers.",
62
+ },
63
+ temperature: {
64
+ short: "Controls randomness in the model's output.",
65
+ long: "Lower (e.g. 0) is deterministic; higher (e.g. 1) is more varied. Affects scores — different temperatures make scores hard to compare.",
66
+ },
67
+ "top-p": {
68
+ short: "Limits the model to sampling from the most likely next tokens (nucleus sampling).",
69
+ },
70
+ "top-k": {
71
+ short: "Limits the model to sampling from the K most likely next tokens.",
72
+ },
73
+ perplexity: {
74
+ short: "How surprised the model is by the text. Lower is better.",
75
+ },
76
+ "average normalized score": {
77
+ short: "Each benchmark's score is rescaled to a 0–1 range, then averaged.",
78
+ long: "Lets you combine benchmarks that use different scales. The exact rescaling rule matters — check the methodology.",
79
+ },
80
+ agentic: {
81
+ short: "The model uses tools, takes multiple steps, and acts on an environment to complete a task.",
82
+ },
83
+ "agent budget": {
84
+ short: "The maximum compute, tokens, or steps the agent is allowed before it must stop.",
85
+ },
86
+ "f1 score": {
87
+ short: "A single number combining precision and recall. Higher is better.",
88
+ },
89
+ bleu: {
90
+ short: "A score for how close generated text is to a reference, by overlapping word sequences.",
91
+ },
92
+ rouge: {
93
+ short: "A score for how much a generated summary overlaps with a reference summary.",
94
+ },
95
+ benchmark: {
96
+ short: "A standardized test used to compare models on a specific capability.",
97
+ },
98
+ }
99
+
100
+ const KEY_ALIASES: Record<string, string> = {
101
+ "0 shot": "0-shot",
102
+ "1 shot": "1-shot",
103
+ "5 shot": "5-shot",
104
+ "few shot": "few-shot",
105
+ "n shot": "n-shot",
106
+ "zero shot": "zero-shot",
107
+ "best of n": "best-of-n",
108
+ "best-of-N": "best-of-n",
109
+ "LLM-as-judge": "llm-as-judge",
110
+ "LLM as judge": "llm as judge",
111
+ "self consistency": "self-consistency",
112
+ "Pass@1": "pass@1",
113
+ "Pass@16": "pass@16",
114
+ "Pass@K": "pass@k",
115
+ "F1": "f1 score",
116
+ "F1 score": "f1 score",
117
+ "BLEU": "bleu",
118
+ "ROUGE": "rouge",
119
+ "Agentic": "agentic",
120
+ "Temperature": "temperature",
121
+ "Perplexity": "perplexity",
122
+ }
123
+
124
+ export function lookupTerm(term: string): GlossaryEntry | undefined {
125
+ const trimmed = term.trim()
126
+ if (!trimmed) return undefined
127
+ const direct = ENTRIES[trimmed] ?? ENTRIES[trimmed.toLowerCase()]
128
+ if (direct) return direct
129
+ const aliased = KEY_ALIASES[trimmed] ?? KEY_ALIASES[trimmed.toLowerCase()]
130
+ if (aliased) return ENTRIES[aliased]
131
+ return undefined
132
+ }
133
+
134
+ export function hasTerm(term: string): boolean {
135
+ return lookupTerm(term) !== undefined
136
+ }
lib/known-issues.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import knownIssuesData from "@/metadata/benchmark_known_issues.json"
2
+
3
+ export interface KnownIssue {
4
+ title: string
5
+ summary: string
6
+ severity: "info" | "warning" | "critical"
7
+ source_url?: string
8
+ published?: string
9
+ }
10
+
11
+ const RAW_ISSUES = (knownIssuesData as { issues?: Record<string, KnownIssue[]> }).issues ?? {}
12
+
13
+ function normalizeKey(value: string): string {
14
+ return value
15
+ .toLowerCase()
16
+ .trim()
17
+ .replace(/[\s_\-/]+/g, " ")
18
+ .replace(/\s+/g, " ")
19
+ }
20
+
21
+ const NORMALIZED_LOOKUP = new Map<string, KnownIssue[]>()
22
+ for (const [rawKey, issues] of Object.entries(RAW_ISSUES)) {
23
+ if (!Array.isArray(issues) || issues.length === 0) continue
24
+ NORMALIZED_LOOKUP.set(normalizeKey(rawKey), issues)
25
+ // Also index a hyphenated variant since some benchmark keys use dashes
26
+ NORMALIZED_LOOKUP.set(normalizeKey(rawKey).replace(/\s+/g, "-"), issues)
27
+ }
28
+
29
+ /**
30
+ * Look up curated known issues for a benchmark by trying any of the supplied
31
+ * names/keys (e.g. evaluation_name, composite_benchmark_key, family_key).
32
+ * Returns the first matching list, or an empty array if nothing is recorded.
33
+ */
34
+ export function getKnownIssues(...candidates: Array<string | undefined | null>): KnownIssue[] {
35
+ for (const candidate of candidates) {
36
+ if (!candidate) continue
37
+ const key = normalizeKey(candidate)
38
+ const direct = NORMALIZED_LOOKUP.get(key)
39
+ if (direct) return direct
40
+ // Try collapsed (no spaces/dashes) form too, since some sources mash names together
41
+ const collapsed = key.replace(/\s+/g, "")
42
+ for (const [registered, issues] of NORMALIZED_LOOKUP) {
43
+ if (registered.replace(/\s+/g, "") === collapsed) return issues
44
+ }
45
+ }
46
+ return []
47
+ }
lib/model-data.ts CHANGED
@@ -749,6 +749,7 @@ function toModelResultsForMetric(
749
  },
750
  source_data: detail.source_data ?? { dataset_name: benchmarkKey },
751
  result: evaluationResult,
 
752
  }
753
  })
754
  }
 
749
  },
750
  source_data: detail.source_data ?? { dataset_name: benchmarkKey },
751
  result: evaluationResult,
752
+ source_record_url: mr.source_record_url,
753
  }
754
  })
755
  }
metadata/benchmark_known_issues.json ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_doc": "Curated known issues for specific benchmarks. Keyed by lowercased benchmark identifier. Severity is info | warning | critical. Each entry: title, summary, severity, optional source_url, optional published date (YYYY-MM-DD). This registry is for benchmark-specific concerns documented in academic work or by maintainers — not generic AI risks. Lookup is case-insensitive and tolerant of separator differences (spaces, dashes, underscores).",
3
+ "issues": {
4
+ "mmlu": [
5
+ {
6
+ "title": "Test-set contamination in pretraining corpora",
7
+ "summary": "Multiple analyses find MMLU questions and exact answers in publicly scraped pretraining data, inflating scores for models trained on that data.",
8
+ "severity": "warning",
9
+ "source_url": "https://arxiv.org/abs/2310.16787",
10
+ "published": "2023-10-25"
11
+ },
12
+ {
13
+ "title": "Errors and ambiguities in test items",
14
+ "summary": "Independent audits found mislabeled answers, ambiguous wording, and duplicated questions across MMLU subjects, capping reliable accuracy below 100%.",
15
+ "severity": "warning",
16
+ "source_url": "https://arxiv.org/abs/2406.04127"
17
+ }
18
+ ],
19
+ "mmlu-pro": [
20
+ {
21
+ "title": "Inherits MMLU contamination concerns for some subjects",
22
+ "summary": "MMLU-Pro reuses items from MMLU and other public sources; subjects overlapping with the MMLU origin may exhibit similar pretraining-data leakage.",
23
+ "severity": "info",
24
+ "source_url": "https://arxiv.org/abs/2406.01574"
25
+ }
26
+ ],
27
+ "gsm8k": [
28
+ {
29
+ "title": "Memorization on the test split",
30
+ "summary": "Studies show large models reproduce verbatim GSM8K test problems, suggesting training-time exposure rather than reasoning. Independent replication on perturbed variants (GSM-Symbolic, GSM-1k) gives lower scores.",
31
+ "severity": "warning",
32
+ "source_url": "https://arxiv.org/abs/2410.05229"
33
+ }
34
+ ],
35
+ "hellaswag": [
36
+ {
37
+ "title": "Annotation noise and outdated cultural references",
38
+ "summary": "A non-trivial fraction of HellaSwag items are mislabeled or rely on now-stale references, capping ceiling performance and complicating cross-year comparisons.",
39
+ "severity": "info"
40
+ }
41
+ ],
42
+ "humaneval": [
43
+ {
44
+ "title": "Limited and saturated test set",
45
+ "summary": "Only 164 problems; top models exceed 90% pass@1 and the benchmark is widely viewed as saturated. Use newer code benchmarks (LiveCodeBench, SWE-bench) for current capability claims.",
46
+ "severity": "warning"
47
+ },
48
+ {
49
+ "title": "Test-case adequacy",
50
+ "summary": "HumanEval+ found that the original tests miss many incorrect solutions; reported scores can overstate real correctness.",
51
+ "severity": "info",
52
+ "source_url": "https://arxiv.org/abs/2305.01210"
53
+ }
54
+ ],
55
+ "narrativeqa": [
56
+ {
57
+ "title": "Largely deprecated in current evaluations",
58
+ "summary": "Few recent leading models report NarrativeQA. Treat scores as historical context rather than current capability evidence.",
59
+ "severity": "info"
60
+ }
61
+ ],
62
+ "boolq": [
63
+ {
64
+ "title": "Largely saturated",
65
+ "summary": "Top models score above 90%; small differences are noise. Use as a sanity check, not a discriminator.",
66
+ "severity": "info"
67
+ }
68
+ ],
69
+ "ifeval": [
70
+ {
71
+ "title": "Narrow instruction taxonomy",
72
+ "summary": "IFEval covers a fixed set of verifiable instruction types. High scores do not generalise to open-ended instruction following.",
73
+ "severity": "info"
74
+ }
75
+ ],
76
+ "humanity's last exam": [
77
+ {
78
+ "title": "Marketing framing vs. methodological scope",
79
+ "summary": "The name implies a definitive ceiling for AI capability; the underlying benchmark is a 3,000-item exam in selected academic disciplines and does not measure general intelligence or safety. Read scores in context.",
80
+ "severity": "warning"
81
+ }
82
+ ]
83
+ }
84
+ }