general-eval-card / lib /policy-summaries.ts
j-chim's picture
Update wording
6d05aa6
Raw
History Blame
19.4 kB
/**
* Rule-based plain-language summaries for the policy-mode views.
*
* Pure templating — no live LLM calls. Each function takes a structured
* data object (model summary, eval summary, signal block) and returns
* either a single sentence or a small struct of paragraph fragments.
*
* Templating rules of thumb:
* - Lead with the headline (numbers / coverage), then the caveat.
* - Pick exactly one phrasing per branch — readers should never see
* two stitched-together fragments that mean the same thing.
* - "Not specified" sentinels collapse silently (caller decides whether
* to render the row at all).
*/
import type { ModelSummaryCore, BenchmarkEvaluation, MetricConfig } from "@/lib/benchmark-schema"
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
import type { ProvenanceSummary, ReproducibilitySummary, ComparabilitySummary } from "@/lib/backend-artifacts"
import { formatTagLabel } from "@/lib/benchmark-tags"
// ---------------------------------------------------------------------------
// Sentence-list helpers (kept tiny & pure — no JSX, no React)
// ---------------------------------------------------------------------------
/** Oxford-comma list with "and". `["a","b","c"]` → `"a, b, and c"`. */
export function listAnd(items: readonly string[]): string {
if (items.length === 0) return ""
if (items.length === 1) return items[0]
if (items.length === 2) return `${items[0]} and ${items[1]}`
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`
}
/** Plural-aware count phrase. `(1, "result")` → `"1 result"`. */
export function pluralize(count: number, singular: string, plural?: string): string {
return `${count.toLocaleString()} ${count === 1 ? singular : plural ?? `${singular}s`}`
}
/**
* Categories the EvalCards taxonomy can express. We compare against this
* canonical list to surface what *isn't* reported, not just what is.
*
* Sourced from data/benchmarks/categories.json — top-level tags that the
* derivedTag pipeline produces. We list the headliners only; obscure
* categories ("multilingual_general", "video_understanding") aren't
* useful as gap-callouts on a policy summary.
*/
export const HEADLINE_POLICY_CATEGORIES = [
"general",
"knowledge",
"logical_reasoning",
"applied_reasoning",
"mathematics",
"coding",
"agentic",
"safety",
"multilingual_general",
"multimodal",
] as const
/**
* Map a derivedTag category into a small bucket of "policy-relevant"
* groupings, so e.g. logical_reasoning + applied_reasoning collapse to
* "Reasoning" for a non-technical reader. Returns null when the input is
* neither headlinable nor in the policy bucket map.
*/
const POLICY_BUCKETS: Record<string, string> = {
general: "General capability",
knowledge: "Knowledge",
logical_reasoning: "Reasoning",
applied_reasoning: "Reasoning",
commonsense_reasoning: "Reasoning",
mathematics: "Math",
coding: "Coding",
software_engineering: "Coding",
agentic: "Agentic",
safety: "Safety",
multilingual_general: "Multilingual",
multimodal: "Multimodal",
}
/** Group categories into ~6 policy-readable buckets. */
export function bucketCategories(tags: readonly string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const tag of tags) {
const bucket = POLICY_BUCKETS[tag]
if (bucket && !seen.has(bucket)) {
seen.add(bucket)
out.push(bucket)
}
}
return out
}
const HEADLINE_BUCKET_LIST = ["General capability", "Knowledge", "Reasoning", "Math", "Coding", "Agentic", "Safety"] as const
/** Buckets we'd expect a frontier general-purpose model to report on. */
function expectedBuckets(): readonly string[] {
return HEADLINE_BUCKET_LIST
}
// ---------------------------------------------------------------------------
// MODEL view — produces the Measures / Caveat / Coverage / Reporting block.
// ---------------------------------------------------------------------------
export interface ModelPolicySummary {
/** "Reported across N benchmarks in K categories." */
scopeSentence: string
/** "Coverage spans Reasoning, Knowledge, and Agentic." (or null when there's only one category) */
coverageSentence: string | null
/** "No Safety or Math evaluations have been reported." Returns null when nothing material is missing. */
gapSentence: string | null
/** "Reported by Anthropic (the developer) and one independent third party." */
reportingSentence: string
/** "How this model was prompted is documented for X of Y reported scores." or null when fully documented / no data. */
reproducibilitySentence: string | null
/** "Comparing scores directly is limited because reporting setups differ." or null. */
comparabilitySentence: string | null
/** "Independently verified across N benchmarks." used as the optional headline tag. */
verificationLabel: string | null
}
interface ModelPolicyInputs {
/** Accepts any ModelSummaryCore-shaped object — the model page passes
* either the family summary or a selected variant. We only read the
* signal-summary blocks plus `total_evaluations`. */
summary: ModelSummaryCore
/** Pre-computed third-party tally from caller (cheap to compute, but
* caller already has it in benchmark-detail). */
thirdPartyEvaluations: number
/** Denominator for the third-party share. MUST be counted from the same
* population as `thirdPartyEvaluations` (the caller's flattened evaluation
* list), not the warehouse's distinct `total_evaluations` — those have
* different grains, which made the share exceed 100%. */
reportedEvaluationCount: number
organizationCount: number
organizationNames: string[]
/** Distinct benchmark count derived from group reduction. */
benchmarkCount: number
/** Categories actually reported, derived-tag form (snake_case). */
reportedCategories: readonly string[]
}
export function buildModelPolicySummary({
summary,
thirdPartyEvaluations,
reportedEvaluationCount,
organizationCount,
organizationNames,
benchmarkCount,
reportedCategories,
}: ModelPolicyInputs): ModelPolicySummary {
const totalEvals = summary.total_evaluations
// Denominator for the third-party share, counted from the same population as
// the numerator so the ratio stays within 0–100%. Falls back to totalEvals
// only if the caller passed nothing.
const thirdPartyBase = reportedEvaluationCount > 0 ? reportedEvaluationCount : totalEvals
const repro = summary.reproducibility_summary
const reproGap = repro?.has_reproducibility_gap_count ?? 0
const reproTotal = repro?.results_total ?? totalEvals
const provenance = summary.provenance_summary
const comparability = summary.comparability_summary
// ── 1. Scope ────────────────────────────────────────────────────────────
const scopeSentence =
benchmarkCount === 0
? "No benchmark evaluations have been reported for this model."
: `Reported across ${pluralize(benchmarkCount, "benchmark")}` +
(totalEvals > benchmarkCount
? ` (${pluralize(totalEvals, "result")} total).`
: ".")
// ── 2. Coverage / Gap (the "missing categories" piece the user wanted) ──
const reportedBuckets = bucketCategories(reportedCategories)
let coverageSentence: string | null = null
let gapSentence: string | null = null
if (reportedBuckets.length > 1) {
coverageSentence = `Coverage spans ${listAnd(reportedBuckets)}.`
} else if (reportedBuckets.length === 1) {
coverageSentence = `Coverage is concentrated in ${reportedBuckets[0]} only.`
}
if (reportedBuckets.length > 0) {
const reportedSet = new Set(reportedBuckets)
const missing = expectedBuckets().filter((b) => !reportedSet.has(b))
// Only flag a gap when there's a meaningful absence — at least one
// category reported AND at least one common bucket missing. We cap the
// list at three to stay readable.
if (missing.length > 0 && missing.length < expectedBuckets().length) {
const head = missing.slice(0, 3)
const trail = missing.length > 3 ? ` (and ${missing.length - 3} other categories)` : ""
gapSentence =
head.length === 1
? `No ${head[0]} evaluations have been reported.`
: `No ${listAnd(head)} evaluations have been reported${trail}.`
}
}
// ── 3. Reporting (provenance) ───────────────────────────────────────────
const firstPartyOnly =
provenance?.first_party_only_groups != null && provenance.total_groups > 0
? provenance.first_party_only_groups === provenance.total_groups
: null
const allThirdParty = thirdPartyBase > 0 && thirdPartyEvaluations === thirdPartyBase
const noThirdParty = thirdPartyEvaluations === 0 && thirdPartyBase > 0
const lead = organizationNames[0]
let reportingSentence: string
if (organizationCount === 0) {
reportingSentence = "No reporting organization is recorded."
} else if (organizationCount === 1 && lead) {
reportingSentence = allThirdParty
? `Tested independently by ${lead} (a third party, not the model's developer).`
: noThirdParty
? `Reported only by ${lead}; no independent third-party scores are available.`
: `Reported by ${lead}.`
} else if (lead) {
const others = organizationCount - 1
reportingSentence = allThirdParty
? `Tested independently by ${lead} and ${pluralize(others, "other organization")}.`
: noThirdParty
? `Reported by ${lead} and ${pluralize(others, "other organization")}, but no independent third-party scores are available.`
: `Reported by ${lead} and ${pluralize(others, "other organization")}.`
} else {
reportingSentence = `Reported by ${pluralize(organizationCount, "organization")}.`
}
// ── 4. Reproducibility gap (plain language, no field names) ────────────
let reproducibilitySentence: string | null = null
if (reproTotal > 0) {
if (reproGap === 0) {
reproducibilitySentence = "How this model was prompted during testing is documented for every reported score."
} else if (reproGap === reproTotal) {
reproducibilitySentence =
"How this model was prompted during testing is not documented. Scores cannot be independently re-run as reported."
} else {
const documented = reproTotal - reproGap
const pct = Math.round((documented / reproTotal) * 100)
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.`
}
}
// ── 5. Comparability caveat (no field names) ──────────────────────────
let comparabilitySentence: string | null = null
if (comparability) {
const variantHits = comparability.variant_divergent_count
const crossPartyHits = comparability.cross_party_divergent_count
const noCrossPartyChecks = comparability.groups_with_cross_party_check === 0
if (variantHits === 0 && crossPartyHits === 0 && !noCrossPartyChecks) {
comparabilitySentence = "Where multiple reports are available, scores agree closely across setups and reporters."
} else if (variantHits > 0 && crossPartyHits > 0) {
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.`
} else if (variantHits > 0) {
comparabilitySentence = `Scores diverge across reporting setups in ${pluralize(variantHits, "case")}; apparent score gaps may partly reflect those setup choices.`
} else if (crossPartyHits > 0) {
comparabilitySentence = `Different reporters disagree on ${pluralize(crossPartyHits, "score")}; treat headline numbers as a range rather than a single value.`
} else if (noCrossPartyChecks) {
comparabilitySentence = "No third-party reports are available to cross-check the developer's numbers."
}
} else if (firstPartyOnly === true) {
comparabilitySentence = "Only the model's developer has reported these scores; cross-party comparison is not possible."
}
// ── 6. Verification headline ─────────────────────────────────────────
let verificationLabel: string | null = null
if (allThirdParty && thirdPartyBase > 0) {
verificationLabel = "100% third party"
} else if (thirdPartyEvaluations > 0 && thirdPartyBase > 0) {
const pct = Math.min(100, Math.round((thirdPartyEvaluations / thirdPartyBase) * 100))
verificationLabel = `${pct}% third party`
} else if (noThirdParty) {
verificationLabel = "Developer-reported only"
}
return {
scopeSentence,
coverageSentence,
gapSentence,
reportingSentence,
reproducibilitySentence,
comparabilitySentence,
verificationLabel,
}
}
// ---------------------------------------------------------------------------
// EVAL view — produces single-sentence narrative for each of the four
// interpretive signals (used by the policy-mode panel renderers).
// ---------------------------------------------------------------------------
export interface EvalPolicySignals {
metricSentence: string
reproducibilitySentence: string | null
provenanceSentence: string | null
comparabilitySentence: string | null
}
export function buildEvalPolicySignals(summary: BenchmarkEvalSummary): EvalPolicySignals {
const cfg = summary.metric_config
const metricSentence = formatMetricSentence(cfg)
const reproducibilitySentence = formatReproducibilitySentence(summary.reproducibility_summary)
const provenanceSentence = formatProvenanceSentence(summary)
const comparabilitySentence = formatComparabilitySentence(summary.comparability_summary)
return {
metricSentence,
reproducibilitySentence,
provenanceSentence,
comparabilitySentence,
}
}
function formatMetricSentence(cfg: MetricConfig): string {
const lower = cfg.lower_is_better
const min = cfg.min_score
const max = cfg.max_score
const direction = lower
? "Lower scores indicate better performance"
: "Higher scores indicate better performance"
// Only mention the scale when both ends are documented and look like a
// tidy interval. Otherwise the sentence collapses to direction-only.
if (typeof min === "number" && typeof max === "number" && max > min) {
if (min === 0 && max === 1) return `${direction}, on a 0 to 1 scale.`
if (min === 0 && max === 100) return `${direction}, on a 0 to 100 scale.`
return `${direction}, on a ${min} to ${max} scale.`
}
return `${direction}.`
}
function formatReproducibilitySentence(repro?: ReproducibilitySummary): string | null {
if (!repro || repro.results_total === 0) return null
const total = repro.results_total
const gap = repro.has_reproducibility_gap_count
if (gap === 0) {
return "How models were prompted during testing is documented for every reported score."
}
if (gap === total) {
return "How models were prompted during testing is not documented. Scores cannot be independently re-run as reported."
}
const documented = total - gap
const pct = Math.round((documented / total) * 100)
return `Prompting setup is documented for ${pct}% of reported scores (${documented} of ${total}).`
}
function formatProvenanceSentence(summary: BenchmarkEvalSummary): string | null {
const prov: ProvenanceSummary | undefined = summary.provenance_summary
if (!prov) {
// Fall back to coarser ratio when the summary isn't attached.
if (summary.third_party_ratio === 0) {
return "These scores were reported only by the model developers themselves."
}
if (summary.third_party_ratio === 1) {
return "These scores were reported by independent third parties, not the model developers."
}
return null
}
const total = prov.total_groups
if (total === 0) return null
const firstPartyOnly = prov.first_party_only_groups
const multi = prov.multi_source_groups
if (firstPartyOnly === total) {
return "Every reported score on this benchmark comes only from the model's own developer; no independent third-party numbers are available."
}
if (firstPartyOnly === 0 && multi === 0) {
return "All reported scores come from independent third parties rather than the model developers."
}
if (multi > 0) {
const pctMulti = Math.round((multi / total) * 100)
return `${pctMulti}% of reported scores have been corroborated by more than one reporting organization.`
}
return null
}
function formatComparabilitySentence(comp?: ComparabilitySummary): string | null {
if (!comp || comp.total_groups === 0) return null
const variant = comp.variant_divergent_count
const crossParty = comp.cross_party_divergent_count
const noCrossPartyChecks = comp.groups_with_cross_party_check === 0
if (variant === 0 && crossParty === 0 && !noCrossPartyChecks) {
return "Where multiple reports exist, the scores agree closely; direct comparison across reports is reasonable."
}
if (variant > 0 && crossParty > 0) {
return "These scores have been reported under different setups and by different organizations, which may explain some of the variation seen across reports."
}
if (variant > 0) {
return "These scores have been reported under different evaluation setups, which may explain some of the variation across reports."
}
if (crossParty > 0) {
return "Different organizations have reported notably different numbers for the same model on this benchmark."
}
if (noCrossPartyChecks) {
return "No independent third-party reports are available to cross-check the developer's numbers."
}
return null
}
// ---------------------------------------------------------------------------
// Helpers used by the existing policySummary lede in benchmark-detail.tsx,
// re-exported so the inline copy can be replaced.
// ---------------------------------------------------------------------------
/**
* Pull derived-tag categories off a flat list of evaluations. Useful when the
* caller already has the per-result entries grouped in the page.
*/
export function collectReportedCategoriesFromEvals(
evaluations: readonly BenchmarkEvaluation[],
resolveCategory: (e: BenchmarkEvaluation) => string | null | undefined
): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const evaluation of evaluations) {
const cat = resolveCategory(evaluation)
if (!cat) continue
if (!seen.has(cat)) {
seen.add(cat)
out.push(cat)
}
}
return out
}
/** "Reasoning, Knowledge, Agentic" — for compact category badge rows. */
export function formatPolicyBucketsCompact(tags: readonly string[]): string {
return bucketCategories(tags).join(" · ")
}
/** Pretty-print a derivedTag for the rare case the policy bucket falls
* through (we still want a friendly word, not snake_case). */
export function formatTagAsPolicyLabel(tag: string): string {
return formatTagLabel(tag)
}