evijit HF Staff Claude Opus 4.7 (1M context) commited on
Commit
aacebd7
Β·
1 Parent(s): 0b45710

Add rule-based policy-mode summaries for model & eval views

Browse files

Generates plain-language summaries from the existing signal data via
templating, so policy mode never needs runtime LLM inference. Model
pages get a new <ModelPolicyOverview> with explicit category coverage
and gap callouts; reproducibility and comparability panels swap their
field-level detail for narrative caveats in policy mode.

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

components/benchmark-detail.tsx CHANGED
@@ -54,6 +54,8 @@ import type {
54
  SubmissionAxis,
55
  } from "@/lib/backend-artifacts"
56
  import { fetchPeerRanks } from "@/lib/dashboard-data-client"
 
 
57
  import {
58
  buildHierarchyEvalIndex,
59
  type HierarchyEvalLocation,
@@ -2218,6 +2220,40 @@ export function BenchmarkDetail({
2218
  summary.model_info.name,
2219
  ])
2220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2221
  const benchmarkGroups = useMemo(
2222
  () => buildBenchmarkGroups(allCategoryResults, benchmarkCards, currentDetailHref),
2223
  [allCategoryResults, benchmarkCards, currentDetailHref]
@@ -4629,22 +4665,12 @@ export function BenchmarkDetail({
4629
  )}
4630
  </div>
4631
  ) : (
4632
- <div className="space-y-3 max-w-[64rem]">
4633
- <p className="text-[16px] leading-[1.7] text-[color:var(--fg)]">
4634
- {policySummary.testedByCopy}
4635
- </p>
4636
- {policySummary.reproducibilityCopy && (
4637
- <div className="border border-[color:var(--border-soft)] bg-[color:var(--bg-warm)] px-4 py-3">
4638
- <span className="kicker kicker-accent mr-2">Reproducibility gap</span>
4639
- <span className="text-[13px] leading-[1.6] text-[color:var(--fg)]">
4640
- {policySummary.reproducibilityCopy}
4641
- </span>
4642
- </div>
4643
- )}
4644
- <p className="text-[13px] leading-[1.7] text-[color:var(--fg-muted)]">
4645
- {policySummary.comparabilityCopy}
4646
- {policySummary.sizeCaveat ? ` ${policySummary.sizeCaveat}` : ""}
4647
- </p>
4648
  </div>
4649
  )}
4650
  </section>
 
54
  SubmissionAxis,
55
  } from "@/lib/backend-artifacts"
56
  import { fetchPeerRanks } from "@/lib/dashboard-data-client"
57
+ import { ModelPolicyOverview } from "@/components/model-policy-overview"
58
+ import { buildModelPolicySummary } from "@/lib/policy-summaries"
59
  import {
60
  buildHierarchyEvalIndex,
61
  type HierarchyEvalLocation,
 
2220
  summary.model_info.name,
2221
  ])
2222
 
2223
+ /**
2224
+ * Structured plain-language summary used by <ModelPolicyOverview>.
2225
+ * Pure rule-based templating β€” see lib/policy-summaries.ts. No LLM is
2226
+ * invoked at runtime, so the same input always produces the same prose.
2227
+ */
2228
+ const modelPolicySummary = useMemo(() => {
2229
+ const reportedCategories = Array.from(
2230
+ new Set(allCategoryResults.map((entry) => entry.category as unknown as string)),
2231
+ )
2232
+ const benchmarkCount = new Set(
2233
+ allCategoryResults.map(
2234
+ (entry) =>
2235
+ entry.evaluation.benchmark ||
2236
+ entry.evaluation.benchmark_parent_name ||
2237
+ entry.evaluation.eval_summary_id ||
2238
+ getResultBenchmarkName(entry.evaluation, entry.result),
2239
+ ),
2240
+ ).size
2241
+ return buildModelPolicySummary({
2242
+ summary,
2243
+ thirdPartyEvaluations: reportingStats.thirdPartyEvaluations,
2244
+ organizationCount: reportingStats.organizationCount,
2245
+ organizationNames: reportingStats.organizationNames,
2246
+ benchmarkCount,
2247
+ reportedCategories,
2248
+ })
2249
+ }, [
2250
+ allCategoryResults,
2251
+ reportingStats.thirdPartyEvaluations,
2252
+ reportingStats.organizationCount,
2253
+ reportingStats.organizationNames,
2254
+ summary,
2255
+ ])
2256
+
2257
  const benchmarkGroups = useMemo(
2258
  () => buildBenchmarkGroups(allCategoryResults, benchmarkCards, currentDetailHref),
2259
  [allCategoryResults, benchmarkCards, currentDetailHref]
 
4665
  )}
4666
  </div>
4667
  ) : (
4668
+ <div className="max-w-[64rem]">
4669
+ <ModelPolicyOverview
4670
+ modelName={getModelDisplayName(summary.model_info.name)}
4671
+ policySummary={modelPolicySummary}
4672
+ scaleNote={policySummary.sizeCaveat}
4673
+ />
 
 
 
 
 
 
 
 
 
 
4674
  </div>
4675
  )}
4676
  </section>
components/model-policy-overview.tsx ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { BookOpen, ShieldCheck, AlertTriangle, GitCompareArrows, Users, Layers } from "lucide-react"
4
+ import { SignalTooltip } from "@/components/signals/signal-tooltip"
5
+ import type { ModelPolicySummary } from "@/lib/policy-summaries"
6
+
7
+ interface ModelPolicyOverviewProps {
8
+ /** Display name of the model β€” surfaced in the kicker. */
9
+ modelName: string
10
+ policySummary: ModelPolicySummary
11
+ /** Optional one-line model-scale framing (params / size context). */
12
+ scaleNote?: string | null
13
+ }
14
+
15
+ /**
16
+ * Plain-language policy note for the model detail page. Mirrors the structure
17
+ * of `<PolicyOverview>` for the eval page but reframes around model-level
18
+ * questions: how broad is the evidence, who reported it, can it be re-run,
19
+ * can it be compared.
20
+ *
21
+ * All copy is rule-based β€” see lib/policy-summaries.ts. There is no live
22
+ * LLM inference at runtime.
23
+ */
24
+ export function ModelPolicyOverview({ modelName, policySummary, scaleNote }: ModelPolicyOverviewProps) {
25
+ const {
26
+ scopeSentence,
27
+ coverageSentence,
28
+ gapSentence,
29
+ reportingSentence,
30
+ reproducibilitySentence,
31
+ comparabilitySentence,
32
+ verificationLabel,
33
+ } = policySummary
34
+
35
+ return (
36
+ <section className="ec-card warm" style={{ padding: "20px 24px" }}>
37
+ <header className="mb-3 flex flex-wrap items-center gap-3">
38
+ <BookOpen className="h-4 w-4" style={{ color: "var(--fg-muted)" }} />
39
+ <span className="kicker kicker-fg" style={{ fontSize: 12, letterSpacing: "0.16em" }}>
40
+ Policy note
41
+ </span>
42
+ <span
43
+ className="font-mono text-[10px] uppercase tracking-[0.12em]"
44
+ style={{ color: "var(--fg-subtle)" }}
45
+ >
46
+ {modelName} Β· in plain language
47
+ </span>
48
+ {verificationLabel && (
49
+ <span
50
+ className="ec-tag outline ml-auto"
51
+ style={{ textTransform: "uppercase" }}
52
+ title="Whether scores have been reported by parties other than the model's developer."
53
+ >
54
+ <ShieldCheck className="h-3 w-3 shrink-0" />
55
+ {verificationLabel}
56
+ </span>
57
+ )}
58
+ </header>
59
+
60
+ <dl
61
+ className="grid gap-y-3 text-[14px]"
62
+ style={{ gridTemplateColumns: "max-content 1fr", columnGap: 24 }}
63
+ >
64
+ <Row icon="layers" label="Reported on">
65
+ {scopeSentence}
66
+ {coverageSentence ? <> {coverageSentence}</> : null}
67
+ </Row>
68
+
69
+ {gapSentence && (
70
+ <Row icon="alert" label="Gap" tone="accent">
71
+ {gapSentence}
72
+ </Row>
73
+ )}
74
+
75
+ <Row icon="users" label="Reported by">
76
+ {reportingSentence}
77
+ </Row>
78
+
79
+ {reproducibilitySentence && (
80
+ <Row icon="alert" label="Re-runnable">
81
+ <SignalTooltip content="Whether someone could re-run this evaluation with the information available.">
82
+ <span
83
+ className="underline decoration-dotted underline-offset-4 cursor-help"
84
+ style={{ textDecorationColor: "var(--fg-subtle)" }}
85
+ >
86
+ {reproducibilitySentence}
87
+ </span>
88
+ </SignalTooltip>
89
+ </Row>
90
+ )}
91
+
92
+ {comparabilitySentence && (
93
+ <Row icon="compare" label="Comparable">
94
+ <SignalTooltip content="Flags when score differences may come from setup choices or different reporting sources.">
95
+ <span
96
+ className="underline decoration-dotted underline-offset-4 cursor-help"
97
+ style={{ textDecorationColor: "var(--fg-subtle)" }}
98
+ >
99
+ {comparabilitySentence}
100
+ </span>
101
+ </SignalTooltip>
102
+ </Row>
103
+ )}
104
+
105
+ {scaleNote && (
106
+ <Row label="Scale note">
107
+ {scaleNote}
108
+ </Row>
109
+ )}
110
+ </dl>
111
+ </section>
112
+ )
113
+ }
114
+
115
+ function Row({
116
+ icon,
117
+ label,
118
+ tone,
119
+ children,
120
+ }: {
121
+ icon?: "layers" | "alert" | "compare" | "users"
122
+ label: string
123
+ tone?: "accent"
124
+ children: React.ReactNode
125
+ }) {
126
+ const Icon =
127
+ icon === "layers"
128
+ ? Layers
129
+ : icon === "alert"
130
+ ? AlertTriangle
131
+ : icon === "compare"
132
+ ? GitCompareArrows
133
+ : icon === "users"
134
+ ? Users
135
+ : null
136
+
137
+ return (
138
+ <>
139
+ <dt
140
+ className="font-mono uppercase tracking-[0.14em] inline-flex items-center gap-1.5"
141
+ style={{
142
+ fontSize: 10,
143
+ color: tone === "accent" ? "var(--accent)" : "var(--fg-subtle)",
144
+ paddingTop: 3,
145
+ }}
146
+ >
147
+ {Icon && <Icon className="h-3 w-3 shrink-0" />}
148
+ {label}
149
+ </dt>
150
+ <dd style={{ color: "var(--fg)", lineHeight: 1.6, margin: 0 }}>
151
+ {children}
152
+ </dd>
153
+ </>
154
+ )
155
+ }
components/signals/comparability-panel.tsx CHANGED
@@ -60,11 +60,19 @@ export function ComparabilityPanel({
60
 
61
  {showNoCrossPartyNote && (
62
  <div className="mt-4 rounded-xl border border-dashed border-border/70 bg-muted/10 px-3 py-2 text-sm text-muted-foreground">
63
- No third-party reports are available for cross-party comparison.
 
 
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"
@@ -116,6 +124,36 @@ export function ComparabilityPanel({
116
  )
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  function GroupList({
120
  icon,
121
  title,
 
60
 
61
  {showNoCrossPartyNote && (
62
  <div className="mt-4 rounded-xl border border-dashed border-border/70 bg-muted/10 px-3 py-2 text-sm text-muted-foreground">
63
+ {isResearchView
64
+ ? "No third-party reports are available for cross-party comparison."
65
+ : "No independent third-party reports are available to cross-check the developer's numbers on this benchmark."}
66
  </div>
67
  )}
68
 
69
+ {!isResearchView && (variantGroups.length > 0 || crossPartyGroups.length > 0) && (
70
+ <div className="mt-4 rounded-xl border border-border/70 bg-muted/5 px-4 py-3 text-sm leading-relaxed text-foreground/90">
71
+ {buildPolicyComparabilitySentence(variantGroups.length, crossPartyGroups.length)}
72
+ </div>
73
+ )}
74
+
75
+ {isResearchView && (() => {
76
  const onlyOne =
77
  (variantGroups.length > 0 ? 1 : 0) + (crossPartyGroups.length > 0 ? 1 : 0) === 1
78
  const sectionClass = onlyOne ? "" : "lg:grid lg:grid-cols-2 lg:gap-3"
 
124
  )
125
  }
126
 
127
+ /**
128
+ * Policy-mode caveat. Hides field names and divergence magnitudes (per the
129
+ * policy spec) and rolls the counts into a single narrative line.
130
+ */
131
+ function buildPolicyComparabilitySentence(variantCount: number, crossPartyCount: number): string {
132
+ const variantPhrase =
133
+ variantCount === 0
134
+ ? null
135
+ : variantCount === 1
136
+ ? "one model has been reported under different evaluation setups"
137
+ : `${variantCount} models have been reported under different evaluation setups`
138
+ const crossPartyPhrase =
139
+ crossPartyCount === 0
140
+ ? null
141
+ : crossPartyCount === 1
142
+ ? "one model has different scores reported by different organizations"
143
+ : `${crossPartyCount} models have different scores reported by different organizations`
144
+
145
+ if (variantPhrase && crossPartyPhrase) {
146
+ return `${variantPhrase[0].toUpperCase()}${variantPhrase.slice(1)}, and ${crossPartyPhrase}. Some apparent score differences may reflect those choices rather than capability.`
147
+ }
148
+ if (variantPhrase) {
149
+ return `${variantPhrase[0].toUpperCase()}${variantPhrase.slice(1)}, which may explain some of the variation seen in reported numbers.`
150
+ }
151
+ if (crossPartyPhrase) {
152
+ return `${crossPartyPhrase[0].toUpperCase()}${crossPartyPhrase.slice(1)} β€” treat the headline number as a range rather than a single value.`
153
+ }
154
+ return ""
155
+ }
156
+
157
  function GroupList({
158
  icon,
159
  title,
components/signals/reproducibility-panel.tsx CHANGED
@@ -6,6 +6,26 @@ import { useAudienceMode } from "@/components/audience-mode-provider"
6
  import type { ReproducibilityGap } from "@/lib/backend-artifacts"
7
  import { formatMissingField } from "./signal-utils"
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  export function ReproducibilityPanel({
10
  gap,
11
  }: {
@@ -34,18 +54,24 @@ export function ReproducibilityPanel({
34
  </div>
35
  </div>
36
 
37
- <div className="space-y-2.5 text-sm">
38
- <PanelRow
39
- label="Setup fields recorded"
40
- value={`${gap.populated_field_count} of ${gap.required_field_count}`}
41
- />
42
- {gap.missing_fields.length > 0 && (
43
  <PanelRow
44
- label="Missing"
45
- value={gap.missing_fields.map(formatMissingField).join(", ")}
46
  />
47
- )}
48
- </div>
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
  )
51
  }
 
6
  import type { ReproducibilityGap } from "@/lib/backend-artifacts"
7
  import { formatMissingField } from "./signal-utils"
8
 
9
+ /**
10
+ * Policy mode hides field-level detail (per the policy spec) and renders a
11
+ * single plain-language sentence built from the same gap counts that drive
12
+ * the research-mode rows. Pure rule-based templating β€” no LLM at runtime.
13
+ */
14
+ function buildPolicyReproducibilitySentence(gap: ReproducibilityGap): string {
15
+ const total = gap.required_field_count
16
+ const populated = gap.populated_field_count
17
+ if (total === 0) {
18
+ return "Setup documentation is not applicable for this result."
19
+ }
20
+ if (populated === total) {
21
+ return "How this model was prompted during testing is fully documented for this result."
22
+ }
23
+ if (populated === 0) {
24
+ return "How this model was prompted during testing is not documented. This score cannot be independently re-run as reported."
25
+ }
26
+ return `${populated} of ${total} setup fields are recorded; the rest are missing, which means the score cannot be re-run exactly as reported.`
27
+ }
28
+
29
  export function ReproducibilityPanel({
30
  gap,
31
  }: {
 
54
  </div>
55
  </div>
56
 
57
+ {isResearchView ? (
58
+ <div className="space-y-2.5 text-sm">
 
 
 
 
59
  <PanelRow
60
+ label="Setup fields recorded"
61
+ value={`${gap.populated_field_count} of ${gap.required_field_count}`}
62
  />
63
+ {gap.missing_fields.length > 0 && (
64
+ <PanelRow
65
+ label="Missing"
66
+ value={gap.missing_fields.map(formatMissingField).join(", ")}
67
+ />
68
+ )}
69
+ </div>
70
+ ) : (
71
+ <p className="text-sm leading-relaxed text-foreground/90">
72
+ {buildPolicyReproducibilitySentence(gap)}
73
+ </p>
74
+ )}
75
  </div>
76
  )
77
  }
lib/policy-summaries.ts ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Rule-based plain-language summaries for the policy-mode views.
3
+ *
4
+ * Pure templating β€” no live LLM calls. Each function takes a structured
5
+ * data object (model summary, eval summary, signal block) and returns
6
+ * either a single sentence or a small struct of paragraph fragments.
7
+ *
8
+ * Templating rules of thumb:
9
+ * - Lead with the headline (numbers / coverage), then the caveat.
10
+ * - Pick exactly one phrasing per branch β€” readers should never see
11
+ * two stitched-together fragments that mean the same thing.
12
+ * - "Not specified" sentinels collapse silently (caller decides whether
13
+ * to render the row at all).
14
+ */
15
+ import type { ModelSummaryCore, BenchmarkEvaluation, MetricConfig } from "@/lib/benchmark-schema"
16
+ import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
17
+ import type { ProvenanceSummary, ReproducibilitySummary, ComparabilitySummary } from "@/lib/backend-artifacts"
18
+ import { formatTagLabel } from "@/lib/benchmark-tags"
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Sentence-list helpers (kept tiny & pure β€” no JSX, no React)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Oxford-comma list with "and". `["a","b","c"]` β†’ `"a, b, and c"`. */
25
+ export function listAnd(items: readonly string[]): string {
26
+ if (items.length === 0) return ""
27
+ if (items.length === 1) return items[0]
28
+ if (items.length === 2) return `${items[0]} and ${items[1]}`
29
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`
30
+ }
31
+
32
+ /** Plural-aware count phrase. `(1, "result")` β†’ `"1 result"`. */
33
+ export function pluralize(count: number, singular: string, plural?: string): string {
34
+ return `${count.toLocaleString()} ${count === 1 ? singular : plural ?? `${singular}s`}`
35
+ }
36
+
37
+ /**
38
+ * Categories the EvalCards taxonomy can express. We compare against this
39
+ * canonical list to surface what *isn't* reported, not just what is.
40
+ *
41
+ * Sourced from data/benchmarks/categories.json β€” top-level tags that the
42
+ * derivedTag pipeline produces. We list the headliners only; obscure
43
+ * categories ("multilingual_general", "video_understanding") aren't
44
+ * useful as gap-callouts on a policy summary.
45
+ */
46
+ export const HEADLINE_POLICY_CATEGORIES = [
47
+ "general",
48
+ "knowledge",
49
+ "logical_reasoning",
50
+ "applied_reasoning",
51
+ "mathematics",
52
+ "coding",
53
+ "agentic",
54
+ "safety",
55
+ "multilingual_general",
56
+ "multimodal",
57
+ ] as const
58
+
59
+ /**
60
+ * Map a derivedTag category into a small bucket of "policy-relevant"
61
+ * groupings, so e.g. logical_reasoning + applied_reasoning collapse to
62
+ * "Reasoning" for a non-technical reader. Returns null when the input is
63
+ * neither headlinable nor in the policy bucket map.
64
+ */
65
+ const POLICY_BUCKETS: Record<string, string> = {
66
+ general: "General capability",
67
+ knowledge: "Knowledge",
68
+ logical_reasoning: "Reasoning",
69
+ applied_reasoning: "Reasoning",
70
+ commonsense_reasoning: "Reasoning",
71
+ mathematics: "Math",
72
+ coding: "Coding",
73
+ software_engineering: "Coding",
74
+ agentic: "Agentic",
75
+ safety: "Safety",
76
+ multilingual_general: "Multilingual",
77
+ multimodal: "Multimodal",
78
+ }
79
+
80
+ /** Group categories into ~6 policy-readable buckets. */
81
+ export function bucketCategories(tags: readonly string[]): string[] {
82
+ const seen = new Set<string>()
83
+ const out: string[] = []
84
+ for (const tag of tags) {
85
+ const bucket = POLICY_BUCKETS[tag]
86
+ if (bucket && !seen.has(bucket)) {
87
+ seen.add(bucket)
88
+ out.push(bucket)
89
+ }
90
+ }
91
+ return out
92
+ }
93
+
94
+ const HEADLINE_BUCKET_LIST = ["General capability", "Knowledge", "Reasoning", "Math", "Coding", "Agentic", "Safety"] as const
95
+
96
+ /** Buckets we'd expect a frontier general-purpose model to report on. */
97
+ function expectedBuckets(): readonly string[] {
98
+ return HEADLINE_BUCKET_LIST
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // MODEL view β€” produces the Measures / Caveat / Coverage / Reporting block.
103
+ // ---------------------------------------------------------------------------
104
+
105
+ export interface ModelPolicySummary {
106
+ /** "Reported across N benchmarks in K categories." */
107
+ scopeSentence: string
108
+ /** "Coverage spans Reasoning, Knowledge, and Agentic." (or null when there's only one category) */
109
+ coverageSentence: string | null
110
+ /** "No Safety or Math evaluations have been reported." Returns null when nothing material is missing. */
111
+ gapSentence: string | null
112
+ /** "Reported by Anthropic (the developer) and one independent third party." */
113
+ reportingSentence: string
114
+ /** "How this model was prompted is documented for X of Y reported scores." or null when fully documented / no data. */
115
+ reproducibilitySentence: string | null
116
+ /** "Comparing scores directly is limited because reporting setups differ." or null. */
117
+ comparabilitySentence: string | null
118
+ /** "Independently verified across N benchmarks." used as the optional headline tag. */
119
+ verificationLabel: string | null
120
+ }
121
+
122
+ interface ModelPolicyInputs {
123
+ /** Accepts any ModelSummaryCore-shaped object β€” the model page passes
124
+ * either the family summary or a selected variant. We only read the
125
+ * signal-summary blocks plus `total_evaluations`. */
126
+ summary: ModelSummaryCore
127
+ /** Pre-computed third-party tally from caller (cheap to compute, but
128
+ * caller already has it in benchmark-detail). */
129
+ thirdPartyEvaluations: number
130
+ organizationCount: number
131
+ organizationNames: string[]
132
+ /** Distinct benchmark count derived from group reduction. */
133
+ benchmarkCount: number
134
+ /** Categories actually reported, derived-tag form (snake_case). */
135
+ reportedCategories: readonly string[]
136
+ }
137
+
138
+ export function buildModelPolicySummary({
139
+ summary,
140
+ thirdPartyEvaluations,
141
+ organizationCount,
142
+ organizationNames,
143
+ benchmarkCount,
144
+ reportedCategories,
145
+ }: ModelPolicyInputs): ModelPolicySummary {
146
+ const totalEvals = summary.total_evaluations
147
+ const repro = summary.reproducibility_summary
148
+ const reproGap = repro?.has_reproducibility_gap_count ?? 0
149
+ const reproTotal = repro?.results_total ?? totalEvals
150
+ const provenance = summary.provenance_summary
151
+ const comparability = summary.comparability_summary
152
+
153
+ // ── 1. Scope ────────────────────────────────────────────────────────────
154
+ const scopeSentence =
155
+ benchmarkCount === 0
156
+ ? "No benchmark evaluations have been reported for this model."
157
+ : `Reported across ${pluralize(benchmarkCount, "benchmark")}` +
158
+ (totalEvals > benchmarkCount
159
+ ? ` (${pluralize(totalEvals, "result")} total).`
160
+ : ".")
161
+
162
+ // ── 2. Coverage / Gap (the "missing categories" piece the user wanted) ──
163
+ const reportedBuckets = bucketCategories(reportedCategories)
164
+ let coverageSentence: string | null = null
165
+ let gapSentence: string | null = null
166
+
167
+ if (reportedBuckets.length > 1) {
168
+ coverageSentence = `Coverage spans ${listAnd(reportedBuckets)}.`
169
+ } else if (reportedBuckets.length === 1) {
170
+ coverageSentence = `Coverage is concentrated in ${reportedBuckets[0]} only.`
171
+ }
172
+
173
+ if (reportedBuckets.length > 0) {
174
+ const reportedSet = new Set(reportedBuckets)
175
+ const missing = expectedBuckets().filter((b) => !reportedSet.has(b))
176
+ // Only flag a gap when there's a meaningful absence β€” at least one
177
+ // category reported AND at least one common bucket missing. We cap the
178
+ // list at three to stay readable.
179
+ if (missing.length > 0 && missing.length < expectedBuckets().length) {
180
+ const head = missing.slice(0, 3)
181
+ const trail = missing.length > 3 ? ` (and ${missing.length - 3} other categories)` : ""
182
+ gapSentence =
183
+ head.length === 1
184
+ ? `No ${head[0]} evaluations have been reported.`
185
+ : `No ${listAnd(head)} evaluations have been reported${trail}.`
186
+ }
187
+ }
188
+
189
+ // ── 3. Reporting (provenance) ───────────────────────────────────────────
190
+ const firstPartyOnly =
191
+ provenance?.first_party_only_groups != null && provenance.total_groups > 0
192
+ ? provenance.first_party_only_groups === provenance.total_groups
193
+ : null
194
+ const allThirdParty = totalEvals > 0 && thirdPartyEvaluations === totalEvals
195
+ const noThirdParty = thirdPartyEvaluations === 0 && totalEvals > 0
196
+ const lead = organizationNames[0]
197
+
198
+ let reportingSentence: string
199
+ if (organizationCount === 0) {
200
+ reportingSentence = "No reporting organization is recorded."
201
+ } else if (organizationCount === 1 && lead) {
202
+ reportingSentence = allThirdParty
203
+ ? `Tested independently by ${lead} β€” a third party, not the model's developer.`
204
+ : noThirdParty
205
+ ? `Reported only by ${lead}; no independent third-party scores are available.`
206
+ : `Reported by ${lead}.`
207
+ } else if (lead) {
208
+ const others = organizationCount - 1
209
+ reportingSentence = allThirdParty
210
+ ? `Tested independently by ${lead} and ${pluralize(others, "other organization")}.`
211
+ : noThirdParty
212
+ ? `Reported by ${lead} and ${pluralize(others, "other organization")} β€” but no independent third-party scores are available.`
213
+ : `Reported by ${lead} and ${pluralize(others, "other organization")}.`
214
+ } else {
215
+ reportingSentence = `Reported by ${pluralize(organizationCount, "organization")}.`
216
+ }
217
+
218
+ // ── 4. Reproducibility gap (plain language, no field names) ────────────
219
+ let reproducibilitySentence: string | null = null
220
+ if (reproTotal > 0) {
221
+ if (reproGap === 0) {
222
+ reproducibilitySentence = "How this model was prompted during testing is documented for every reported score."
223
+ } else if (reproGap === reproTotal) {
224
+ reproducibilitySentence =
225
+ "How this model was prompted during testing is not documented. Scores cannot be independently re-run as reported."
226
+ } else {
227
+ const documented = reproTotal - reproGap
228
+ const pct = Math.round((documented / reproTotal) * 100)
229
+ reproducibilitySentence = `Prompting setup is documented for ${pct}% of reported scores (${documented} of ${reproTotal}); the rest are missing enough detail to be re-run as-is.`
230
+ }
231
+ }
232
+
233
+ // ── 5. Comparability caveat (no field names) ──────────────────────────
234
+ let comparabilitySentence: string | null = null
235
+ if (comparability) {
236
+ const variantHits = comparability.variant_divergent_count
237
+ const crossPartyHits = comparability.cross_party_divergent_count
238
+ const noCrossPartyChecks = comparability.groups_with_cross_party_check === 0
239
+ if (variantHits === 0 && crossPartyHits === 0 && !noCrossPartyChecks) {
240
+ comparabilitySentence = "Where multiple reports are available, scores agree closely across setups and reporters."
241
+ } else if (variantHits > 0 && crossPartyHits > 0) {
242
+ comparabilitySentence = `Scores diverge across reporting setups in ${pluralize(variantHits, "case")} and across different reporters in ${pluralize(crossPartyHits, "case")}; some apparent score gaps may reflect setup choices rather than capability.`
243
+ } else if (variantHits > 0) {
244
+ comparabilitySentence = `Scores diverge across reporting setups in ${pluralize(variantHits, "case")}; apparent score gaps may partly reflect those setup choices.`
245
+ } else if (crossPartyHits > 0) {
246
+ comparabilitySentence = `Different reporters disagree on ${pluralize(crossPartyHits, "score")}; treat headline numbers as a range rather than a single value.`
247
+ } else if (noCrossPartyChecks) {
248
+ comparabilitySentence = "No third-party reports are available to cross-check the developer's numbers."
249
+ }
250
+ } else if (firstPartyOnly === true) {
251
+ comparabilitySentence = "Only the model's developer has reported these scores; cross-party comparison is not possible."
252
+ }
253
+
254
+ // ── 6. Verification headline ─────────────────────────────────────────
255
+ let verificationLabel: string | null = null
256
+ if (allThirdParty && totalEvals > 0) {
257
+ verificationLabel = "Independently verified"
258
+ } else if (thirdPartyEvaluations > 0 && totalEvals > 0) {
259
+ const pct = Math.round((thirdPartyEvaluations / totalEvals) * 100)
260
+ verificationLabel = `${pct}% independently verified`
261
+ } else if (noThirdParty) {
262
+ verificationLabel = "Developer-reported only"
263
+ }
264
+
265
+ return {
266
+ scopeSentence,
267
+ coverageSentence,
268
+ gapSentence,
269
+ reportingSentence,
270
+ reproducibilitySentence,
271
+ comparabilitySentence,
272
+ verificationLabel,
273
+ }
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // EVAL view β€” produces single-sentence narrative for each of the four
278
+ // interpretive signals (used by the policy-mode panel renderers).
279
+ // ---------------------------------------------------------------------------
280
+
281
+ export interface EvalPolicySignals {
282
+ metricSentence: string
283
+ reproducibilitySentence: string | null
284
+ provenanceSentence: string | null
285
+ comparabilitySentence: string | null
286
+ }
287
+
288
+ export function buildEvalPolicySignals(summary: BenchmarkEvalSummary): EvalPolicySignals {
289
+ const cfg = summary.metric_config
290
+ const metricSentence = formatMetricSentence(cfg)
291
+
292
+ const reproducibilitySentence = formatReproducibilitySentence(summary.reproducibility_summary)
293
+ const provenanceSentence = formatProvenanceSentence(summary)
294
+ const comparabilitySentence = formatComparabilitySentence(summary.comparability_summary)
295
+
296
+ return {
297
+ metricSentence,
298
+ reproducibilitySentence,
299
+ provenanceSentence,
300
+ comparabilitySentence,
301
+ }
302
+ }
303
+
304
+ function formatMetricSentence(cfg: MetricConfig): string {
305
+ const lower = cfg.lower_is_better
306
+ const min = cfg.min_score
307
+ const max = cfg.max_score
308
+
309
+ const direction = lower
310
+ ? "Lower scores indicate better performance"
311
+ : "Higher scores indicate better performance"
312
+
313
+ // Only mention the scale when both ends are documented and look like a
314
+ // tidy interval. Otherwise the sentence collapses to direction-only.
315
+ if (typeof min === "number" && typeof max === "number" && max > min) {
316
+ if (min === 0 && max === 1) return `${direction}, on a 0 to 1 scale.`
317
+ if (min === 0 && max === 100) return `${direction}, on a 0 to 100 scale.`
318
+ return `${direction}, on a ${min} to ${max} scale.`
319
+ }
320
+ return `${direction}.`
321
+ }
322
+
323
+ function formatReproducibilitySentence(repro?: ReproducibilitySummary): string | null {
324
+ if (!repro || repro.results_total === 0) return null
325
+ const total = repro.results_total
326
+ const gap = repro.has_reproducibility_gap_count
327
+ if (gap === 0) {
328
+ return "How models were prompted during testing is documented for every reported score."
329
+ }
330
+ if (gap === total) {
331
+ return "How models were prompted during testing is not documented. Scores cannot be independently re-run as reported."
332
+ }
333
+ const documented = total - gap
334
+ const pct = Math.round((documented / total) * 100)
335
+ return `Prompting setup is documented for ${pct}% of reported scores (${documented} of ${total}).`
336
+ }
337
+
338
+ function formatProvenanceSentence(summary: BenchmarkEvalSummary): string | null {
339
+ const prov: ProvenanceSummary | undefined = summary.provenance_summary
340
+ if (!prov) {
341
+ // Fall back to coarser ratio when the summary isn't attached.
342
+ if (summary.third_party_ratio === 0) {
343
+ return "These scores were reported only by the model developers themselves."
344
+ }
345
+ if (summary.third_party_ratio === 1) {
346
+ return "These scores were reported by independent third parties, not the model developers."
347
+ }
348
+ return null
349
+ }
350
+ const total = prov.total_groups
351
+ if (total === 0) return null
352
+ const firstPartyOnly = prov.first_party_only_groups
353
+ const multi = prov.multi_source_groups
354
+ if (firstPartyOnly === total) {
355
+ return "Every reported score on this benchmark comes only from the model's own developer; no independent third-party numbers are available."
356
+ }
357
+ if (firstPartyOnly === 0 && multi === 0) {
358
+ return "All reported scores come from independent third parties rather than the model developers."
359
+ }
360
+ if (multi > 0) {
361
+ const pctMulti = Math.round((multi / total) * 100)
362
+ return `${pctMulti}% of reported scores have been corroborated by more than one reporting organization.`
363
+ }
364
+ return null
365
+ }
366
+
367
+ function formatComparabilitySentence(comp?: ComparabilitySummary): string | null {
368
+ if (!comp || comp.total_groups === 0) return null
369
+ const variant = comp.variant_divergent_count
370
+ const crossParty = comp.cross_party_divergent_count
371
+ const noCrossPartyChecks = comp.groups_with_cross_party_check === 0
372
+ if (variant === 0 && crossParty === 0 && !noCrossPartyChecks) {
373
+ return "Where multiple reports exist, the scores agree closely β€” direct comparison across reports is reasonable."
374
+ }
375
+ if (variant > 0 && crossParty > 0) {
376
+ return "These scores have been reported under different setups and by different organizations, which may explain some of the variation seen across reports."
377
+ }
378
+ if (variant > 0) {
379
+ return "These scores have been reported under different evaluation setups, which may explain some of the variation across reports."
380
+ }
381
+ if (crossParty > 0) {
382
+ return "Different organizations have reported notably different numbers for the same model on this benchmark."
383
+ }
384
+ if (noCrossPartyChecks) {
385
+ return "No independent third-party reports are available to cross-check the developer's numbers."
386
+ }
387
+ return null
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Helpers used by the existing policySummary lede in benchmark-detail.tsx,
392
+ // re-exported so the inline copy can be replaced.
393
+ // ---------------------------------------------------------------------------
394
+
395
+ /**
396
+ * Pull derived-tag categories off a flat list of evaluations. Useful when the
397
+ * caller already has the per-result entries grouped in the page.
398
+ */
399
+ export function collectReportedCategoriesFromEvals(
400
+ evaluations: readonly BenchmarkEvaluation[],
401
+ resolveCategory: (e: BenchmarkEvaluation) => string | null | undefined
402
+ ): string[] {
403
+ const seen = new Set<string>()
404
+ const out: string[] = []
405
+ for (const evaluation of evaluations) {
406
+ const cat = resolveCategory(evaluation)
407
+ if (!cat) continue
408
+ if (!seen.has(cat)) {
409
+ seen.add(cat)
410
+ out.push(cat)
411
+ }
412
+ }
413
+ return out
414
+ }
415
+
416
+ /** "Reasoning, Knowledge, Agentic" β€” for compact category badge rows. */
417
+ export function formatPolicyBucketsCompact(tags: readonly string[]): string {
418
+ return bucketCategories(tags).join(" Β· ")
419
+ }
420
+
421
+ /** Pretty-print a derivedTag for the rare case the policy bucket falls
422
+ * through (we still want a friendly word, not snake_case). */
423
+ export function formatTagAsPolicyLabel(tag: string): string {
424
+ return formatTagLabel(tag)
425
+ }