diff --git a/.gitignore b/.gitignore index 0f72f75..f27a0a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ /.quarto/ **/*.quarto_ipynb +node_modules/ +blog/posts/*/index-quarto.html +blog/posts/*/index-quarto_files/ +blog/posts/*/compare.html +blog/posts/*/component-demo.html diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..cb15471 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +# Quarto include partials are intentionally unbalanced HTML fragments +# (post-before/_header open tags that post-after closes). Formatters +# "fix" the unclosed tags and break the page shell. +_includes/ +blog/posts/*/_header.html +blog/posts/*/_head-extra.html +# rendered output — not source +blog/posts/*/index.html diff --git a/assets/gl-components/gl.js b/assets/gl-components/gl.js index f6fa085..b0c1c0d 100644 --- a/assets/gl-components/gl.js +++ b/assets/gl-components/gl.js @@ -15,13 +15,25 @@ // the helpers below so each component states only what is unique to it. export const palette = { - ink: "#1a1919", muted: "#5b5858", soft: "#837878", hairline: "#e6e6e6", - bg: "#fafafa", paper: "#f2f2f2", - mint: "#17cfb9", mintSoft: "#ecf8f5", mintEdge: "#c5e8e1", mintDeep: "#24857a", + ink: "#1a1919", + muted: "#5b5858", + soft: "#837878", + hairline: "#e6e6e6", + bg: "#fafafa", + paper: "#f2f2f2", + mint: "#17cfb9", + mintSoft: "#ecf8f5", + mintEdge: "#c5e8e1", + mintDeep: "#24857a", series: ["#24857a", "#d97757", "#7c4dbe", "#3a5fa8", "#eda100", "#d1495b"], - good: "#1baf7a", warn: "#eda100", warnInk: "#b07d00", - bad: "#d1495b", badSoft: "#faeceb", - neutral: "#7a93ad", neutralInk: "#5a748c", neutralSoft: "#edf1f5", + good: "#1baf7a", + warn: "#eda100", + warnInk: "#b07d00", + bad: "#d1495b", + badSoft: "#faeceb", + neutral: "#7a93ad", + neutralInk: "#5a748c", + neutralSoft: "#edf1f5", cleanFill: "#9fc3b4", }; @@ -30,7 +42,10 @@ const SVG_NS = "http://www.w3.org/2000/svg"; /* ---------------------------------------------------------------- helpers */ -const esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/ + String(s ?? "") + .replace(/&/g, "&") + .replace(/ - ({ background: "transparent", fontFamily: FONT, fontSize }); +const plotStyle = (fontSize = "13px") => ({ + background: "transparent", + fontFamily: FONT, + fontSize, +}); /** A centred x-axis config with enough offset to clear two-line tick labels. */ -const centredAxis = (label, labelOffset, extra = {}) => - ({ label, labelAnchor: "center", labelOffset, ...extra }); +const centredAxis = (label, labelOffset, extra = {}) => ({ + label, + labelAnchor: "center", + labelOffset, + ...extra, +}); /** An HTML x-axis label rendered below the plot (used where Plot's own label would collide with tick labels). */ -const labelBelow = (text, css) => el("div", - `text-align:center;font-family:${FONT};margin-top:.2rem;${css}`, esc(text)); +const labelBelow = (text, css) => + el( + "div", + `text-align:center;font-family:${FONT};margin-top:.2rem;${css}`, + esc(text), + ); /** Colour-swatch legend row rendered below a plot (house style: the model family legend sits under the graph, never above it). */ function legendRow(domain, range) { - return el("div", + return el( + "div", `display:flex;flex-wrap:wrap;justify-content:center;gap:.35rem 1.1rem;` + - `font-family:${FONT};font-size:12px;color:${palette.muted};margin-top:.4rem;`, - domain.map((name, i) => `` + - `${esc(name)}`).join("")); + `font-family:${FONT};font-size:12px;color:${palette.muted};margin-top:.4rem;`, + domain + .map( + (name, i) => + `` + + `${esc(name)}`, + ) + .join(""), + ); } /* -------------------------------------------------------------- glStatBar */ @@ -105,7 +141,9 @@ function legendRow(domain, range) { // items: [{ label, value, detail? }] export function glStatBar(_, items, spec = {}) { const { caption, provenance } = spec; - injectOnce("gl-statbar-css", ` + injectOnce( + "gl-statbar-css", + ` .gl-statbar { display:flex; flex-wrap:wrap; border-bottom:2px solid ${palette.hairline}; font-family:${FONT}; } @@ -121,15 +159,22 @@ export function glStatBar(_, items, spec = {}) { margin-top:.05rem; } .gl-statbar .detail { font-size:10.5px; color:${palette.muted}; margin-top:.05rem; line-height:1.3; } - `); + `, + ); const bar = el("div", null); bar.className = "gl-statbar"; for (const it of items) { - bar.appendChild(el("div", null, ` + bar.appendChild( + el( + "div", + null, + `
${esc(it.label)}
${esc(it.value)}
${it.detail ? `
${esc(it.detail)}
` : ""} - `)).className = "gl-stat"; + `, + ), + ).className = "gl-stat"; } return frame(bar, { caption, provenance }); } @@ -144,10 +189,22 @@ export function glStatBar(_, items, spec = {}) { // out to the latest x to make stagnation legible. export function glScatter({ Plot }, rows, spec = {}) { const { - x = "x", y = "y", label = "label", group = "group", - xLabel = "", yLabel = "", caption, provenance, - width = 700, height = 460, xFmt = (v) => v, yFmt = (v) => v, - vintage, dateKey = "date", lo, hi, + x = "x", + y = "y", + label = "label", + group = "group", + xLabel = "", + yLabel = "", + caption, + provenance, + width = 700, + height = 460, + xFmt = (v) => v, + yFmt = (v) => v, + vintage, + dateKey = "date", + lo, + hi, } = spec; const cut = vintage ? new Date(vintage) : null; const isNew = (d) => cut && new Date(d[dateKey]) > cut; @@ -161,49 +218,119 @@ export function glScatter({ Plot }, rows, spec = {}) { const steps = []; let best = -Infinity; for (const d of sorted) { - if (d[y] > best) { best = d[y]; onFrontier.add(d); steps.push({ _fx: d[x], _fy: best }); } + if (d[y] > best) { + best = d[y]; + onFrontier.add(d); + steps.push({ _fx: d[x], _fy: best }); + } } steps.push({ _fx: sorted[sorted.length - 1][x], _fy: best }); // hold flat to latest release - marks.push(Plot.line(steps, { x: "_fx", y: "_fy", curve: "step-after", - stroke: palette.soft, strokeWidth: 1.3, strokeDasharray: "2,4" })); + marks.push( + Plot.line(steps, { + x: "_fx", + y: "_fy", + curve: "step-after", + stroke: palette.soft, + strokeWidth: 1.3, + strokeDasharray: "2,4", + }), + ); } // with a frontier drawn, the field fades slightly so the frontier carries the story const dotAlpha = (d) => (!spec.frontier || onFrontier.has(d) ? 0.9 : 0.45); const ciAlpha = (d) => (!spec.frontier || onFrontier.has(d) ? 0.5 : 0.28); if (lo && hi && rows.some((d) => d[lo] != null)) { - marks.push(Plot.ruleX(rows, { x, y1: lo, y2: hi, stroke: group, strokeWidth: 1.4, strokeOpacity: ciAlpha })); + marks.push( + Plot.ruleX(rows, { + x, + y1: lo, + y2: hi, + stroke: group, + strokeWidth: 1.4, + strokeOpacity: ciAlpha, + }), + ); // CI end caps: short horizontal segments at lo and hi. const xs = rows.map((d) => +d[x]); const capW = (Math.max(...xs) - Math.min(...xs)) * 0.006 || 1; - const caps = rows.flatMap((d) => [ - { ...d, _cy: d[lo], _x1: +d[x] - capW, _x2: +d[x] + capW, _a: ciAlpha(d) }, - { ...d, _cy: d[hi], _x1: +d[x] - capW, _x2: +d[x] + capW, _a: ciAlpha(d) }, - ]).filter((d) => d._cy != null); - marks.push(Plot.ruleY(caps, { y: "_cy", x1: (d) => new Date(d._x1), x2: (d) => new Date(d._x2), - stroke: group, strokeWidth: 1.4, strokeOpacity: (d) => d._a })); + const caps = rows + .flatMap((d) => [ + { + ...d, + _cy: d[lo], + _x1: +d[x] - capW, + _x2: +d[x] + capW, + _a: ciAlpha(d), + }, + { + ...d, + _cy: d[hi], + _x1: +d[x] - capW, + _x2: +d[x] + capW, + _a: ciAlpha(d), + }, + ]) + .filter((d) => d._cy != null); + marks.push( + Plot.ruleY(caps, { + y: "_cy", + x1: (d) => new Date(d._x1), + x2: (d) => new Date(d._x2), + stroke: group, + strokeWidth: 1.4, + strokeOpacity: (d) => d._a, + }), + ); } - marks.push(Plot.dot(before, { x, y, fill: group, r: 5.5, fillOpacity: dotAlpha })); + marks.push( + Plot.dot(before, { x, y, fill: group, r: 5.5, fillOpacity: dotAlpha }), + ); if (after.length) marks.push( - Plot.dot(after, { x, y, stroke: palette.mint, strokeWidth: 2.5, r: 6.5, fill: palette.bg }), + Plot.dot(after, { + x, + y, + stroke: palette.mint, + strokeWidth: 2.5, + r: 6.5, + fill: palette.bg, + }), Plot.dot(after, { x, y, fill: palette.mint, r: 3 }), ); - marks.push(Plot.tip(rows, Plot.pointer({ x, y, maxRadius: 24, title: (d) => - `${d[label]}${isNew(d) ? " (added since publication)" : ""}\n${xFmt(d[x])} · ${yFmt(d[y])}` }))); + marks.push( + Plot.tip( + rows, + Plot.pointer({ + x, + y, + maxRadius: 24, + title: (d) => + `${d[label]}${isNew(d) ? " (added since publication)" : ""}\n${xFmt(d[x])} · ${yFmt(d[y])}`, + }), + ), + ); const node = Plot.plot({ - width, height, marginBottom: 58, + width, + height, + marginBottom: 58, style: plotStyle(), x: centredAxis(xLabel || null, 52, { grid: false, domain: spec.xDomain }), y: { label: yLabel || null, grid: true, domain: spec.yDomain }, - color: { legend: !spec.legendBelow, domain: spec.colorDomain, range: spec.colorRange ?? palette.series }, + color: { + legend: !spec.legendBelow, + domain: spec.colorDomain, + range: spec.colorRange ?? palette.series, + }, marks, }); let out = node; if (spec.legendBelow && spec.colorDomain) { out = el("div"); out.appendChild(node); - out.appendChild(legendRow(spec.colorDomain, spec.colorRange ?? palette.series)); + out.appendChild( + legendRow(spec.colorDomain, spec.colorRange ?? palette.series), + ); } const prov = cut ? `${provenance || ""} · ● as published ${vintage} · ◉ added since (live-refreshing)` @@ -216,30 +343,56 @@ export function glScatter({ Plot }, rows, spec = {}) { // rows: {value, series}; spec.thresholds may be a count or bin-edge array. export function glHistogram({ Plot }, rows, spec = {}) { const { - value = "value", series = "series", - xLabel = "", caption, provenance, - width = 688, height = 470, thresholds = 18, + value = "value", + series = "series", + xLabel = "", + caption, + provenance, + width = 688, + height = 470, + thresholds = 18, } = spec; const byValues = {}; - for (const r of rows) (byValues[r[series]] = byValues[r[series]] || []).push(r[value]); - const meanRows = Object.entries(byValues).map(([k, vs]) => - ({ [series]: k, m: vs.reduce((a, b) => a + b, 0) / vs.length })); + for (const r of rows) + (byValues[r[series]] = byValues[r[series]] || []).push(r[value]); + const meanRows = Object.entries(byValues).map(([k, vs]) => ({ + [series]: k, + m: vs.reduce((a, b) => a + b, 0) / vs.length, + })); const node = Plot.plot({ - width, height, + width, + height, style: plotStyle(), x: { label: null, grid: false }, y: { label: null, grid: true, insetTop: 16 }, - color: { legend: true, domain: spec.colorDomain, range: spec.colorRange ?? [palette.series[0], palette.series[1]] }, + color: { + legend: true, + domain: spec.colorDomain, + range: spec.colorRange ?? [palette.series[0], palette.series[1]], + }, marks: [ // y2 (not y) so the two distributions overlay instead of stacking. - Plot.rectY(rows, Plot.binX({ y2: "count" }, { x: value, fill: series, thresholds, fillOpacity: 0.5, inset: 0.5 })), - Plot.ruleX(meanRows, { x: "m", stroke: series, strokeWidth: 2.6, strokeDasharray: "5,3" }), + Plot.rectY( + rows, + Plot.binX( + { y2: "count" }, + { x: value, fill: series, thresholds, fillOpacity: 0.5, inset: 0.5 }, + ), + ), + Plot.ruleX(meanRows, { + x: "m", + stroke: series, + strokeWidth: 2.6, + strokeDasharray: "5,3", + }), ], }); const wrap = el("div"); wrap.appendChild(node); - wrap.appendChild(labelBelow(xLabel, `font-size:.95rem;color:${palette.muted};`)); + wrap.appendChild( + labelBelow(xLabel, `font-size:.95rem;color:${palette.muted};`), + ); return frame(wrap, { caption, provenance }); } @@ -250,29 +403,59 @@ export function glHistogram({ Plot }, rows, spec = {}) { // tooltip's value line. export function glBars({ Plot }, rows, spec = {}) { const { - value = "value", label = "label", group = "group", - xLabel = "", caption, provenance, width = 688, - valueFmt = (v) => v, sort = true, + value = "value", + label = "label", + group = "group", + xLabel = "", + caption, + provenance, + width = 688, + valueFmt = (v) => v, + sort = true, } = spec; const data = sort ? rows.slice().sort((a, b) => b[value] - a[value]) : rows; const height = Math.max(160, data.length * 16 + 80); const node = Plot.plot({ - width, height, marginLeft: 210, marginBottom: 46, + width, + height, + marginLeft: 210, + marginBottom: 46, style: plotStyle("12px"), x: centredAxis(xLabel, 38, { grid: true }), y: { label: null, domain: data.map((d) => d[label]) }, - color: { legend: false, domain: spec.colorDomain, range: spec.colorRange ?? palette.series }, + color: { + legend: false, + domain: spec.colorDomain, + range: spec.colorRange ?? palette.series, + }, marks: [ - Plot.barX(data, { y: label, x: value, fill: group, rx: 2, insetTop: 1.5, insetBottom: 1.5 }), + Plot.barX(data, { + y: label, + x: value, + fill: group, + rx: 2, + insetTop: 1.5, + insetBottom: 1.5, + }), // pointerY: hovering anywhere along a bar's row triggers that bar's tip. - Plot.tip(data, Plot.pointerY({ y: label, x: value, title: (d) => - `${d[label]}\n${spec.tipName || xLabel || value}: ${valueFmt(d[value])}` })), + Plot.tip( + data, + Plot.pointerY({ + y: label, + x: value, + title: (d) => + `${d[label]}\n${spec.tipName || xLabel || value}: ${valueFmt(d[value])}`, + }), + ), ], }); const wrap = el("div"); wrap.appendChild(node); - if (spec.colorDomain) wrap.appendChild(legendRow(spec.colorDomain, spec.colorRange ?? palette.series)); + if (spec.colorDomain) + wrap.appendChild( + legendRow(spec.colorDomain, spec.colorRange ?? palette.series), + ); return frame(wrap, { caption, provenance }); } @@ -284,19 +467,38 @@ export function glSolveDist({ Plot }, rows, spec = {}) { const { caption, provenance, width = 688, height = 340 } = spec; const nModels = rows[0]?.n_models ?? 49; const counts = new Map(); - for (const r of rows) counts.set(r.n_solved, (counts.get(r.n_solved) || 0) + 1); - const bins = Array.from({ length: nModels + 1 }, (_, k) => ({ k, n: counts.get(k) || 0 })); + for (const r of rows) + counts.set(r.n_solved, (counts.get(r.n_solved) || 0) + 1); + const bins = Array.from({ length: nModels + 1 }, (_, k) => ({ + k, + n: counts.get(k) || 0, + })); const node = Plot.plot({ - width, height, marginBottom: 48, + width, + height, + marginBottom: 48, style: plotStyle(), x: centredAxis(`models solving the question (of ${nModels})`, 40), y: { label: "questions", grid: true, insetTop: 14 }, marks: [ - Plot.rectY(bins, { x1: (d) => d.k - 0.45, x2: (d) => d.k + 0.45, y: "n", - fill: (d) => (d.k === 0 ? palette.bad : palette.mintDeep), fillOpacity: 0.85, rx: 1.5 }), - Plot.tip(bins, Plot.pointerX({ x: "k", y: "n", title: (d) => - `${d.n} questions solved by exactly ${d.k} model${d.k === 1 ? "" : "s"}` })), + Plot.rectY(bins, { + x1: (d) => d.k - 0.45, + x2: (d) => d.k + 0.45, + y: "n", + fill: (d) => (d.k === 0 ? palette.bad : palette.mintDeep), + fillOpacity: 0.85, + rx: 1.5, + }), + Plot.tip( + bins, + Plot.pointerX({ + x: "k", + y: "n", + title: (d) => + `${d.n} questions solved by exactly ${d.k} model${d.k === 1 ? "" : "s"}`, + }), + ), ], }); return frame(node, { caption, provenance }); @@ -311,44 +513,112 @@ export function glSolveDist({ Plot }, rows, spec = {}) { // rows: {label, hex, nat_score, for_score, nat_se, for_se, nat_abst, for_abst} export function glForcingPairs({ Plot }, rows, spec = {}) { const { caption, provenance, width = 688 } = spec; - const order = rows.slice() - .sort((a, b) => (b.for_score - b.nat_score) - (a.for_score - a.nat_score)) + const order = rows + .slice() + .sort((a, b) => b.for_score - b.nat_score - (a.for_score - a.nat_score)) .map((d) => d.label); const panel = (aKey, bKey, seA, seB, yLabel, withCI) => { const flat = rows.flatMap((d) => [ - { label: d.label, arm: "natural", v: d[aKey], se: seA ? d[seA] : null, hex: d.hex }, - { label: d.label, arm: "forced", v: d[bKey], se: seB ? d[seB] : null, hex: d.hex }, + { + label: d.label, + arm: "natural", + v: d[aKey], + se: seA ? d[seA] : null, + hex: d.hex, + }, + { + label: d.label, + arm: "forced", + v: d[bKey], + se: seB ? d[seB] : null, + hex: d.hex, + }, ]); - const tips = rows.map((d) => ({ label: d.label, a: d[aKey], b: d[bKey], - top: Math.max(d[aKey], d[bKey]) })); + const tips = rows.map((d) => ({ + label: d.label, + a: d[aKey], + b: d[bKey], + top: Math.max(d[aKey], d[bKey]), + })); return Plot.plot({ - width, height: 330, marginBottom: 64, marginLeft: 56, + width, + height: 330, + marginBottom: 64, + marginLeft: 56, style: plotStyle("12.5px"), - fx: { label: null, domain: order, tickRotate: -28, padding: 0.25, paddingOuter: 0.5, align: 0.6 }, + fx: { + label: null, + domain: order, + tickRotate: -28, + padding: 0.25, + paddingOuter: 0.5, + align: 0.6, + }, x: { axis: null, domain: ["natural", "forced"], padding: 0.12 }, y: { label: yLabel, grid: true, insetTop: 14 }, marks: [ - Plot.barY(flat, { fx: "label", x: "arm", y: "v", rx: 2, - fill: (d) => d.hex, fillOpacity: (d) => (d.arm === "natural" ? 0.32 : 0.92) }), - ...(withCI ? [ - Plot.ruleX(flat.filter((d) => d.se), { fx: "label", x: "arm", - y1: (d) => d.v - 1.96 * d.se, y2: (d) => d.v + 1.96 * d.se, - stroke: palette.ink, strokeWidth: 1.3 }), - ] : []), + Plot.barY(flat, { + fx: "label", + x: "arm", + y: "v", + rx: 2, + fill: (d) => d.hex, + fillOpacity: (d) => (d.arm === "natural" ? 0.32 : 0.92), + }), + ...(withCI + ? [ + Plot.ruleX( + flat.filter((d) => d.se), + { + fx: "label", + x: "arm", + y1: (d) => d.v - 1.96 * d.se, + y2: (d) => d.v + 1.96 * d.se, + stroke: palette.ink, + strokeWidth: 1.3, + }, + ), + ] + : []), // one tip per model, anchored between the two bars (dx = half a band) - Plot.tip(tips, Plot.pointerX({ fx: "label", x: () => "natural", y: "top", dx: 15, title: (d) => - `${d.label}\nnatural: ${(100 * d.a).toFixed(1)}%\nforced: ${(100 * d.b).toFixed(1)}%` })), + Plot.tip( + tips, + Plot.pointerX({ + fx: "label", + x: () => "natural", + y: "top", + dx: 15, + title: (d) => + `${d.label}\nnatural: ${(100 * d.a).toFixed(1)}%\nforced: ${(100 * d.b).toFixed(1)}%`, + }), + ), ], }); }; const wrap = el("div"); - wrap.appendChild(el("div", `font-size:.8rem;color:${palette.muted};margin-bottom:.25rem;font-family:${FONT};`, - "pale = natural  ·  solid = forced answer")); - wrap.appendChild(panel("nat_score", "for_score", "nat_se", "for_se", "headline accuracy", true)); + wrap.appendChild( + el( + "div", + `font-size:.8rem;color:${palette.muted};margin-bottom:.25rem;font-family:${FONT};`, + "pale = natural  ·  solid = forced answer", + ), + ); + wrap.appendChild( + panel( + "nat_score", + "for_score", + "nat_se", + "for_se", + "headline accuracy", + true, + ), + ); wrap.appendChild(el("div", "height:14px;")); - wrap.appendChild(panel("nat_abst", "for_abst", null, null, "abstention rate", false)); + wrap.appendChild( + panel("nat_abst", "for_abst", null, null, "abstention rate", false), + ); return frame(wrap, { caption, provenance }); } @@ -360,55 +630,120 @@ export function glForcingPairs({ Plot }, rows, spec = {}) { // rows: {x: Date, label, group, } export function glTripletScatter({ Plot }, rows, spec = {}) { const { - x = "x", label = "label", group = "group", metrics = [], - xLabel = "", caption, provenance, - width = 688, height = 480, valueFmt = (v) => v.toFixed(3), + x = "x", + label = "label", + group = "group", + metrics = [], + xLabel = "", + caption, + provenance, + width = 688, + height = 480, + valueFmt = (v) => v.toFixed(3), } = spec; - const JITTER_DAYS = 1.3, DAY_MS = 86400000; - const jmap = new Map(rows.map((r) => { - const h = [...String(r[label])].reduce((a, c) => a + c.charCodeAt(0), 0); - return [r[label], new Date(+r[x] + ((h % 13) - 6) * JITTER_DAYS * DAY_MS)]; - })); + const JITTER_DAYS = 1.3, + DAY_MS = 86400000; + const jmap = new Map( + rows.map((r) => { + const h = [...String(r[label])].reduce((a, c) => a + c.charCodeAt(0), 0); + return [ + r[label], + new Date(+r[x] + ((h % 13) - 6) * JITTER_DAYS * DAY_MS), + ]; + }), + ); const jx = (d) => jmap.get(d[label]); const flat = []; - for (const r of rows) for (const m of metrics) - if (r[m] != null) flat.push({ ...r, metric: m, value: r[m] }); + for (const r of rows) + for (const m of metrics) + if (r[m] != null) flat.push({ ...r, metric: m, value: r[m] }); const heads = flat.filter((d) => d.metric === metrics[0]); const others = flat.filter((d) => d.metric !== metrics[0]); - const extent = (d, fn) => fn(...metrics.map((m) => d[m]).filter((v) => v != null)); + const extent = (d, fn) => + fn(...metrics.map((m) => d[m]).filter((v) => v != null)); const node = Plot.plot({ - width, height, marginBottom: 46, + width, + height, + marginBottom: 46, style: plotStyle(), x: { label: null, grid: false, domain: spec.xDomain }, y: { label: null, grid: true, domain: spec.yDomain }, - color: { legend: false, domain: spec.colorDomain, range: spec.colorRange ?? palette.series }, - symbol: { legend: false, domain: metrics, range: ["circle", "diamond2", "times"] }, + color: { + legend: false, + domain: spec.colorDomain, + range: spec.colorRange ?? palette.series, + }, + symbol: { + legend: false, + domain: metrics, + range: ["circle", "diamond2", "times"], + }, marks: [ - Plot.ruleX(rows, { x: jx, y1: (d) => extent(d, Math.min), y2: (d) => extent(d, Math.max), - stroke: group, strokeWidth: 1.1, strokeOpacity: 0.35 }), - Plot.dot(heads, { x: jx, y: "value", fill: group, symbol: "circle", r: 4.5 }), - Plot.dot(others, { x: jx, y: "value", stroke: group, symbol: "metric", r: 4.5, strokeWidth: 1.8 }), - Plot.tip(flat, Plot.pointer({ x: jx, y: "value", title: (d) => - `${d[label]}\n${d.metric}: ${valueFmt(d.value)}` })), + Plot.ruleX(rows, { + x: jx, + y1: (d) => extent(d, Math.min), + y2: (d) => extent(d, Math.max), + stroke: group, + strokeWidth: 1.1, + strokeOpacity: 0.35, + }), + Plot.dot(heads, { + x: jx, + y: "value", + fill: group, + symbol: "circle", + r: 4.5, + }), + Plot.dot(others, { + x: jx, + y: "value", + stroke: group, + symbol: "metric", + r: 4.5, + strokeWidth: 1.8, + }), + Plot.tip( + flat, + Plot.pointer({ + x: jx, + y: "value", + title: (d) => `${d[label]}\n${d.metric}: ${valueFmt(d.value)}`, + }), + ), ], }); // metric key drawn inside the plot area (colour carries provider, shape carries metric) const GLYPH = ["●", "◇", "✕"]; - const key = el("div", + const key = el( + "div", `position:absolute;top:10px;left:52px;font-family:${FONT};font-size:12px;` + - `color:${palette.muted};display:flex;flex-direction:column;gap:.15rem;pointer-events:none;`, - metrics.map((m, i) => `${GLYPH[i] ?? "•"}${esc(m)}`).join("")); + `color:${palette.muted};display:flex;flex-direction:column;gap:.15rem;pointer-events:none;`, + metrics + .map( + (m, i) => + `${GLYPH[i] ?? "•"}${esc(m)}`, + ) + .join(""), + ); const plotBox = el("div", "position:relative;"); plotBox.appendChild(node); plotBox.appendChild(key); const wrap = el("div"); wrap.appendChild(plotBox); - wrap.appendChild(labelBelow(xLabel, `font-size:.92rem;font-weight:600;color:${palette.ink};margin-top:.15rem;`)); - if (spec.colorDomain) wrap.appendChild(legendRow(spec.colorDomain, spec.colorRange ?? palette.series)); + wrap.appendChild( + labelBelow( + xLabel, + `font-size:.92rem;font-weight:600;color:${palette.ink};margin-top:.15rem;`, + ), + ); + if (spec.colorDomain) + wrap.appendChild( + legendRow(spec.colorDomain, spec.colorRange ?? palette.series), + ); return frame(wrap, { caption, provenance }); } @@ -418,50 +753,90 @@ export function glTripletScatter({ Plot }, rows, spec = {}) { // spec: colors {model: hex}, rMin/rMax (radial domain), legend: false to // suppress the built-in legend (e.g. when two radars share one). export function glRadar(_, data, spec = {}) { - const { caption, provenance, width = 560, rMin = 0.4, rMax = 0.85, colors = {} } = spec; + const { + caption, + provenance, + width = 560, + rMin = 0.4, + rMax = 0.85, + colors = {}, + } = spec; const RING_VALUES = [0.5, 0.6, 0.7, 0.8]; - const LABEL_OVERHANG = 0.11; // axis labels sit just past rMax - const R_MARGIN = 58; // svg edge to polygon edge + const LABEL_OVERHANG = 0.11; // axis labels sit just past rMax + const R_MARGIN = 58; // svg edge to polygon edge const axes = data.axes; const models = [...new Set(data.rows.map((r) => r.model))]; - const size = width, cx = size / 2, cy = size / 2 + 6, R = size / 2 - R_MARGIN; + const size = width, + cx = size / 2, + cy = size / 2 + 6, + R = size / 2 - R_MARGIN; const ang = (i) => (Math.PI * 2 * i) / axes.length - Math.PI / 2; const rr = (v) => R * Math.max(0, Math.min(1, (v - rMin) / (rMax - rMin))); - const pt = (i, v) => [cx + rr(v) * Math.cos(ang(i)), cy + rr(v) * Math.sin(ang(i))]; + const pt = (i, v) => [ + cx + rr(v) * Math.cos(ang(i)), + cy + rr(v) * Math.sin(ang(i)), + ]; const svg = svgEl("svg", { viewBox: `0 0 ${size} ${size + 10}` }); svg.style.cssText = `width:100%;max-width:${size}px;height:auto;font-family:${FONT};display:block;margin:0 auto;`; for (const g of RING_VALUES) { - svg.appendChild(svgEl("polygon", { - points: axes.map((_, i) => pt(i, g).join(",")).join(" "), - fill: "none", stroke: palette.hairline })); + svg.appendChild( + svgEl("polygon", { + points: axes.map((_, i) => pt(i, g).join(",")).join(" "), + fill: "none", + stroke: palette.hairline, + }), + ); const [x, y] = pt(0, g); - const t = svgEl("text", { x: x + 4, y: y + 3, style: `font-size:9.5px;fill:${palette.soft};` }); + const t = svgEl("text", { + x: x + 4, + y: y + 3, + style: `font-size:9.5px;fill:${palette.soft};`, + }); t.textContent = g.toFixed(1); svg.appendChild(t); } axes.forEach((a, i) => { - const [x1, y1] = pt(i, rMin), [x2, y2] = pt(i, rMax); - svg.appendChild(svgEl("line", { x1, y1, x2, y2, stroke: palette.hairline })); + const [x1, y1] = pt(i, rMin), + [x2, y2] = pt(i, rMax); + svg.appendChild( + svgEl("line", { x1, y1, x2, y2, stroke: palette.hairline }), + ); const [lx, ly] = pt(i, rMax + (rMax - rMin) * LABEL_OVERHANG); - const anchor = Math.abs(Math.cos(ang(i))) < 0.35 ? "middle" : (Math.cos(ang(i)) > 0 ? "start" : "end"); - const t = svgEl("text", { x: lx, y: ly + 4, "text-anchor": anchor, - style: `font-size:11px;fill:${palette.muted};` }); + const anchor = + Math.abs(Math.cos(ang(i))) < 0.35 + ? "middle" + : Math.cos(ang(i)) > 0 + ? "start" + : "end"; + const t = svgEl("text", { + x: lx, + y: ly + 4, + "text-anchor": anchor, + style: `font-size:11px;fill:${palette.muted};`, + }); t.textContent = a; svg.appendChild(t); }); const byModel = {}; - for (const r of data.rows) (byModel[r.model] = byModel[r.model] || {})[r.axis] = r; + for (const r of data.rows) + (byModel[r.model] = byModel[r.model] || {})[r.axis] = r; models.forEach((mName, mi) => { const col = colors[mName] || palette.series[mi]; const pts = axes.map((a, i) => pt(i, byModel[mName][a]?.acc ?? rMin)); - svg.appendChild(svgEl("polygon", { - points: pts.map((p) => p.join(",")).join(" "), - fill: col, "fill-opacity": "0.13", - stroke: col, "stroke-width": "2.2", "stroke-linejoin": "round" })); + svg.appendChild( + svgEl("polygon", { + points: pts.map((p) => p.join(",")).join(" "), + fill: col, + "fill-opacity": "0.13", + stroke: col, + "stroke-width": "2.2", + "stroke-linejoin": "round", + }), + ); pts.forEach(([x, y], i) => { const c = svgEl("circle", { cx: x, cy: y, r: 3.6, fill: col }); const r = byModel[mName][axes[i]]; @@ -475,10 +850,18 @@ export function glRadar(_, data, spec = {}) { const wrap = el("div"); wrap.appendChild(svg); if (spec.legend !== false) { - wrap.appendChild(el("div", - `display:flex;gap:1.2rem;justify-content:center;font-size:.85rem;color:${palette.muted};font-family:${FONT};margin-top:.2rem;`, - models.map((mName, mi) => - ` ${mName}`).join(""))); + wrap.appendChild( + el( + "div", + `display:flex;gap:1.2rem;justify-content:center;font-size:.85rem;color:${palette.muted};font-family:${FONT};margin-top:.2rem;`, + models + .map( + (mName, mi) => + ` ${mName}`, + ) + .join(""), + ), + ); } return frame(wrap, { caption, provenance }); } @@ -493,29 +876,73 @@ export function glInfScale({ Plot }, data, spec = {}) { const flat = []; for (const s of data.series) { const model = s.name.replace(/ (serial|parallel).*$/, ""); - for (const p of s.points) flat.push({ ...p, model, arm: s.arm, series: s.name }); + for (const p of s.points) + flat.push({ ...p, model, arm: s.arm, series: s.name }); } const tipTitle = (d) => { - const detail = d.k ? ` (k=${d.k})` - : d.budget != null && d.arm === "serial" ? ` (budget ${d.budget || "off"})` : ""; + const detail = d.k + ? ` (k=${d.k})` + : d.budget != null && d.arm === "serial" + ? ` (budget ${d.budget || "off"})` + : ""; return `${d.model} · ${d.arm}${detail}\n~${d.x} tokens · ${(100 * d.y).toFixed(1)}%`; }; const node = Plot.plot({ - width, height, marginBottom: 48, + width, + height, + marginBottom: 48, style: plotStyle(), - x: centredAxis("median generated tokens per question", 40, - { type: spec.xType ?? "linear", grid: false, domain: spec.xDomain }), + x: centredAxis("median generated tokens per question", 40, { + type: spec.xType ?? "linear", + grid: false, + domain: spec.xDomain, + }), y: { label: "accuracy", grid: true, domain: [0, 0.85] }, - color: { legend: true, domain: Object.keys(colors), range: Object.values(colors) }, - symbol: { legend: true, domain: ["serial", "parallel"], range: ["times", "circle"] }, + color: { + legend: true, + domain: Object.keys(colors), + range: Object.values(colors), + }, + symbol: { + legend: true, + domain: ["serial", "parallel"], + range: ["times", "circle"], + }, marks: [ - Plot.line(flat, { x: "x", y: "y", z: "series", stroke: "model", strokeWidth: 2, - strokeDasharray: (d) => (d.arm === "parallel" ? "5,4" : null) }), - Plot.dot(flat.filter((d) => d.arm === "serial"), - { x: "x", y: "y", stroke: "model", symbol: "times", r: 5.5, strokeWidth: 2.2 }), - Plot.dot(flat.filter((d) => d.arm === "parallel"), - { x: "x", y: "y", stroke: "model", symbol: "circle", r: 5, strokeWidth: 2 }), - Plot.tip(flat, Plot.pointer({ x: "x", y: "y", maxRadius: 26, title: tipTitle })), + Plot.line(flat, { + x: "x", + y: "y", + z: "series", + stroke: "model", + strokeWidth: 2, + strokeDasharray: (d) => (d.arm === "parallel" ? "5,4" : null), + }), + Plot.dot( + flat.filter((d) => d.arm === "serial"), + { + x: "x", + y: "y", + stroke: "model", + symbol: "times", + r: 5.5, + strokeWidth: 2.2, + }, + ), + Plot.dot( + flat.filter((d) => d.arm === "parallel"), + { + x: "x", + y: "y", + stroke: "model", + symbol: "circle", + r: 5, + strokeWidth: 2, + }, + ), + Plot.tip( + flat, + Plot.pointer({ x: "x", y: "y", maxRadius: 26, title: tipTitle }), + ), ], }); return frame(node, { caption }); @@ -526,45 +953,79 @@ export function glInfScale({ Plot }, data, spec = {}) { // in mint, off-diagonal heat in red scaled by row share. // data: {flash: {labels, counts, n_runs}, mini: {...}} export function glConfusion(_, data, spec = {}) { - const { caption, titles = { - flash: "Flash-era runs, regraded by mini", - mini: "mini-era runs, regraded by mini (noise floor)", - } } = spec; + const { + caption, + titles = { + flash: "Flash-era runs, regraded by mini", + mini: "mini-era runs, regraded by mini (noise floor)", + }, + } = spec; const CELL = { w: 74, h: 52 }; - const HEAT_GAIN = 14; // off-diagonal share → red opacity + const HEAT_GAIN = 14; // off-diagonal share → red opacity - const wrap = el("div", `display:flex;gap:2rem;flex-wrap:wrap;justify-content:center;font-family:${FONT};`); + const wrap = el( + "div", + `display:flex;gap:2rem;flex-wrap:wrap;justify-content:center;font-family:${FONT};`, + ); for (const key of ["flash", "mini"]) { const d = data[key]; const G = d.labels; const rowTot = d.counts.map((r) => r.reduce((a, b) => a + b, 0)); const box = el("div"); - box.appendChild(el("div", `font-size:.85rem;color:${palette.muted};margin-bottom:.45rem;text-align:center;`, - esc(titles[key]))); + box.appendChild( + el( + "div", + `font-size:.85rem;color:${palette.muted};margin-bottom:.45rem;text-align:center;`, + esc(titles[key]), + ), + ); const tbl = el("table", "border-collapse:separate;border-spacing:3px;"); - tbl.appendChild(el("tr", null, - `` + G.map((g) => - `→ ${g}`).join(""))); + tbl.appendChild( + el( + "tr", + null, + `` + + G.map( + (g) => + `→ ${g}`, + ).join(""), + ), + ); d.counts.forEach((row, i) => { - const cells = row.map((v, j) => { - const share = rowTot[i] ? v / rowTot[i] : 0; - const diag = i === j; - const bg = diag - ? `rgba(23,207,185,${0.10 + 0.5 * share})` - : `rgba(209,73,91,${Math.min(0.85, share * HEAT_GAIN)})`; - const ink = (!diag && share * HEAT_GAIN > 0.45) ? "#fff" : palette.ink; - return `` + - `
${v.toLocaleString()}
` + - `
${(100 * share).toFixed(1)}%
`; - }).join(""); - tbl.appendChild(el("tr", null, - `${G[i]}` + cells)); + const cells = row + .map((v, j) => { + const share = rowTot[i] ? v / rowTot[i] : 0; + const diag = i === j; + const bg = diag + ? `rgba(23,207,185,${0.1 + 0.5 * share})` + : `rgba(209,73,91,${Math.min(0.85, share * HEAT_GAIN)})`; + const ink = !diag && share * HEAT_GAIN > 0.45 ? "#fff" : palette.ink; + return ( + `` + + `
${v.toLocaleString()}
` + + `
${(100 * share).toFixed(1)}%
` + ); + }) + .join(""); + tbl.appendChild( + el( + "tr", + null, + `${G[i]}` + + cells, + ), + ); }); box.appendChild(tbl); - box.appendChild(el("div", `font-size:.7rem;color:${palette.soft};margin-top:.3rem;text-align:center;`, - `rows: original grade · ${d.n_runs} runs`)); + box.appendChild( + el( + "div", + `font-size:.7rem;color:${palette.soft};margin-top:.3rem;text-align:center;`, + `rows: original grade · ${d.n_runs} runs`, + ), + ); wrap.appendChild(box); } return frame(wrap, { caption }); @@ -580,29 +1041,63 @@ export function glSankey(_, verdicts, spec = {}) { const { caption, provenance, width = 688, height = 500, total = 1000 } = spec; // Layout constants. - const BAR = 13; // band thickness - const GAP = 15; // vertical gap between bands - const MIN_BAND = 2.5; // slivers stay visible - const HIT_MIN = 30, HIT_PAD = 10; // invisible hover target size - const COL = { c1: 0.38, c2: 0.70 }; // column positions as width fractions - const M = { t: 18, b: 18, l: 142, r: 138 }; // l fits "1000 questions" even at hover font size + const BAR = 13; // band thickness + const GAP = 15; // vertical gap between bands + const MIN_BAND = 2.5; // slivers stay visible + const HIT_MIN = 30, + HIT_PAD = 10; // invisible hover target size + const COL = { c1: 0.38, c2: 0.7 }; // column positions as width fractions + const M = { t: 18, b: 18, l: 142, r: 138 }; // l fits "1000 questions" even at hover font size const count = (k) => verdicts.filter((v) => v.final === k).length; - const bitesOf = (k) => verdicts.filter((v) => v.final === k && v.bites === true).length; - const FLAG = verdicts.length, NON = total - FLAG; + const bitesOf = (k) => + verdicts.filter((v) => v.final === k && v.bites === true).length; + const FLAG = verdicts.length, + NON = total - FLAG; const terminals = [ - { id: "fine", name: "fine", n: count("ACTUALLY_FINE"), color: palette.good, side: "right" }, - { id: "unv", name: "unverifiable", n: count("UNVERIFIABLE"), color: palette.neutral, side: "right" }, - { id: "amb", name: "ambiguous", n: count("CONFIRMED_AMBIGUOUS"), color: palette.warn, side: "left" }, - { id: "wrong", name: "wrong", n: count("CONFIRMED_BAD"), color: palette.bad, side: "left" }, + { + id: "fine", + name: "fine", + n: count("ACTUALLY_FINE"), + color: palette.good, + side: "right", + }, + { + id: "unv", + name: "unverifiable", + n: count("UNVERIFIABLE"), + color: palette.neutral, + side: "right", + }, + { + id: "amb", + name: "ambiguous", + n: count("CONFIRMED_AMBIGUOUS"), + color: palette.warn, + side: "left", + }, + { + id: "wrong", + name: "wrong", + n: count("CONFIRMED_BAD"), + color: palette.bad, + side: "left", + }, ]; - const ambB = bitesOf("CONFIRMED_AMBIGUOUS"), ambH = count("CONFIRMED_AMBIGUOUS") - ambB; - const wrB = bitesOf("CONFIRMED_BAD"), wrH = count("CONFIRMED_BAD") - wrB; - const nCosts = ambB + wrB, nHarm = ambH + wrH; + const ambB = bitesOf("CONFIRMED_AMBIGUOUS"), + ambH = count("CONFIRMED_AMBIGUOUS") - ambB; + const wrB = bitesOf("CONFIRMED_BAD"), + wrH = count("CONFIRMED_BAD") - wrB; + const nCosts = ambB + wrB, + nHarm = ambH + wrH; - const W = width - M.l - M.r, H = height - M.t - M.b; + const W = width - M.l - M.r, + H = height - M.t - M.b; const sc = (v) => (v / total) * (H - 6 * GAP); - const x0 = M.l, x1 = M.l + W * COL.c1, x2 = M.l + W * COL.c2, x3 = M.l + W; + const x0 = M.l, + x1 = M.l + W * COL.c1, + x2 = M.l + W * COL.c2, + x3 = M.l + W; const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}` }); svg.style.cssText = `width:100%;height:auto;font-family:${FONT};`; @@ -624,59 +1119,149 @@ export function glSankey(_, verdicts, spec = {}) { return `M${xa},${a0} C${mx},${a0} ${mx},${b0} ${xb},${b0} L${xb},${b1} C${mx},${b1} ${mx},${a1} ${xa},${a1} Z`; }; - const groups = {}, ribbons = {}; + const groups = {}, + ribbons = {}; function band(id, x, y, h, color, name, n, side) { const g = svgEl("g", { class: "gl-band" }); g.dataset.id = id; const bandH = Math.max(h, MIN_BAND); const hitH = Math.max(bandH + HIT_PAD, HIT_MIN); - g.appendChild(svgEl("rect", { class: "hit", - x: x - 6, y: y + bandH / 2 - hitH / 2, width: BAR + 12, height: hitH })); - g.appendChild(svgEl("rect", { class: "viz", x, y, width: BAR, height: bandH, rx: 2, fill: color })); + g.appendChild( + svgEl("rect", { + class: "hit", + x: x - 6, + y: y + bandH / 2 - hitH / 2, + width: BAR + 12, + height: hitH, + }), + ); + g.appendChild( + svgEl("rect", { + class: "viz", + x, + y, + width: BAR, + height: bandH, + rx: 2, + fill: color, + }), + ); const t = svgEl("text", { y: y + bandH / 2 + 4.5, x: side === "left" ? x - 10 : x + BAR + 10, - "text-anchor": side === "left" ? "end" : "start" }); + "text-anchor": side === "left" ? "end" : "start", + }); t.innerHTML = n === "" ? name : `${name} ${n}`; g.appendChild(t); svg.appendChild(g); groups[id] = g; } function ribbon(id, xa, xb, a0, a1, b0, b1, color) { - const p = svgEl("path", { class: "gl-ribbon", d: ribbonPath(xa, xb, a0, a1, b0, b1), fill: color }); + const p = svgEl("path", { + class: "gl-ribbon", + d: ribbonPath(xa, xb, a0, a1, b0, b1), + fill: color, + }); p.dataset.id = id; - svg.insertBefore(p, svg.firstChild.nextSibling); // ribbons under bands + svg.insertBefore(p, svg.firstChild.nextSibling); // ribbons under bands (ribbons[id] = ribbons[id] || []).push(p); } // Column 0: source. - band("src", x0 - BAR, M.t, sc(total) + GAP, palette.soft, "1000 questions", "", "left"); + band( + "src", + x0 - BAR, + M.t, + sc(total) + GAP, + palette.soft, + "1000 questions", + "", + "left", + ); // Column 1: not flagged on top, flagged below. - const nY = M.t, nH = sc(NON); - const fY = nY + nH + GAP, fH = sc(FLAG); + const nY = M.t, + nH = sc(NON); + const fY = nY + nH + GAP, + fH = sc(FLAG); band("non", x1, nY, nH, palette.good, "not flagged", NON, "right"); band("flag", x1, fY, fH, palette.soft, "flagged", FLAG, "left"); ribbon("non", x0, x1, M.t, M.t + nH, nY, nY + nH, palette.good); ribbon("flag", x0, x1, M.t + nH, M.t + nH + fH, fY, fY + fH, palette.soft); // Column 2: verdicts from flagged; ambiguous + wrong continue to column 3. - let ySrc = fY, yDst = fY; + let ySrc = fY, + yDst = fY; const pos = {}; for (const t of terminals) { const h = Math.max(sc(t.n), MIN_BAND); band(t.id, x2, yDst, sc(t.n), t.color, t.name, t.n, t.side); ribbon(t.id, x1 + BAR, x2, ySrc, ySrc + sc(t.n), yDst, yDst + h, t.color); pos[t.id] = { y: yDst, h }; - ySrc += sc(t.n); yDst += h + GAP; + ySrc += sc(t.n); + yDst += h + GAP; } // Column 3: harmless above, costs-points below. const hY = pos.amb.y - 2; const cY = hY + Math.max(sc(nHarm), MIN_BAND) + GAP * 1.6; - band("harmless", x3, hY, sc(nHarm), palette.cleanFill, "harmless", nHarm, "right"); - band("costs", x3, cY, sc(nCosts), palette.bad, "costs points", nCosts, "right"); - ribbon("harmless", x2 + BAR, x3, pos.amb.y, pos.amb.y + sc(ambH), hY, hY + sc(ambH), palette.warn); - ribbon("harmless", x2 + BAR, x3, pos.wrong.y, pos.wrong.y + sc(wrH), hY + sc(ambH), hY + sc(ambH) + sc(wrH), palette.bad); - ribbon("costs", x2 + BAR, x3, pos.amb.y + sc(ambH), pos.amb.y + sc(ambH) + sc(ambB), cY, cY + sc(ambB), palette.warn); - ribbon("costs", x2 + BAR, x3, pos.wrong.y + sc(wrH), pos.wrong.y + sc(wrH) + sc(wrB), cY + sc(ambB), cY + sc(ambB) + sc(wrB), palette.bad); + band( + "harmless", + x3, + hY, + sc(nHarm), + palette.cleanFill, + "harmless", + nHarm, + "right", + ); + band( + "costs", + x3, + cY, + sc(nCosts), + palette.bad, + "costs points", + nCosts, + "right", + ); + ribbon( + "harmless", + x2 + BAR, + x3, + pos.amb.y, + pos.amb.y + sc(ambH), + hY, + hY + sc(ambH), + palette.warn, + ); + ribbon( + "harmless", + x2 + BAR, + x3, + pos.wrong.y, + pos.wrong.y + sc(wrH), + hY + sc(ambH), + hY + sc(ambH) + sc(wrH), + palette.bad, + ); + ribbon( + "costs", + x2 + BAR, + x3, + pos.amb.y + sc(ambH), + pos.amb.y + sc(ambH) + sc(ambB), + cY, + cY + sc(ambB), + palette.warn, + ); + ribbon( + "costs", + x2 + BAR, + x3, + pos.wrong.y + sc(wrH), + pos.wrong.y + sc(wrH) + sc(wrB), + cY + sc(ambB), + cY + sc(ambB) + sc(wrB), + palette.bad, + ); // Hover wiring: a band and its ribbons heat together, from either side. const setHot = (id, hot) => { @@ -706,14 +1291,25 @@ export function glAuditCard(_, verdicts, spec = {}) { const { caption } = spec; const CLASSES = { CONFIRMED_BAD: { name: "wrong", color: palette.bad, bg: palette.badSoft }, - CONFIRMED_AMBIGUOUS: { name: "ambiguous", color: palette.warnInk, bg: "#faf3e0" }, - UNVERIFIABLE: { name: "unverifiable", color: palette.neutralInk, bg: palette.neutralSoft }, + CONFIRMED_AMBIGUOUS: { + name: "ambiguous", + color: palette.warnInk, + bg: "#faf3e0", + }, + UNVERIFIABLE: { + name: "unverifiable", + color: palette.neutralInk, + bg: palette.neutralSoft, + }, }; const pool = verdicts.filter((v) => CLASSES[v.final]); - const card = el("div", `border-radius:6px;padding:.85rem 1.1rem;font-family:${FONT};` + - `font-size:.93rem;line-height:1.55;transition:opacity .25s ease, background .25s ease, border-color .25s ease;` + - `border-left:3px solid;min-height:11em;`); + const card = el( + "div", + `border-radius:6px;padding:.85rem 1.1rem;font-family:${FONT};` + + `font-size:.93rem;line-height:1.55;transition:opacity .25s ease, background .25s ease, border-color .25s ease;` + + `border-left:3px solid;min-height:11em;`, + ); const render = () => { const v = pool[Math.floor(Math.random() * pool.length)]; const c = CLASSES[v.final]; @@ -723,21 +1319,34 @@ export function glAuditCard(_, verdicts, spec = {}) { `${c.name}` + `
“${esc(v.question)}” — gold: ${esc(v.gold)}
` + - (v.correct_answer ? `
verifier's answer: ${esc(v.correct_answer)}
` : "") + + (v.correct_answer + ? `
verifier's answer: ${esc(v.correct_answer)}
` + : "") + `
${esc(v.reason)}
` + - (v.source_url ? `
source
` : ""); + (v.source_url + ? `
source
` + : ""); }; render(); - const btn = el("button", `background:transparent;color:${palette.mintDeep};border:1px solid ${palette.mintEdge};` + - `border-radius:999px;padding:.45rem 1.05rem;font-family:${FONT};font-size:.85rem;cursor:pointer;`); + const btn = el( + "button", + `background:transparent;color:${palette.mintDeep};border:1px solid ${palette.mintEdge};` + + `border-radius:999px;padding:.45rem 1.05rem;font-family:${FONT};font-size:.85rem;cursor:pointer;`, + ); btn.textContent = "↻ show another flagged question"; btn.onclick = () => { card.style.opacity = 0; - setTimeout(() => { render(); card.style.opacity = 1; }, 180); + setTimeout(() => { + render(); + card.style.opacity = 1; + }, 180); }; - const btnRow = el("div", "display:flex;justify-content:center;margin-top:.8rem;"); + const btnRow = el( + "div", + "display:flex;justify-content:center;margin-top:.8rem;", + ); btnRow.appendChild(btn); const wrap = el("div"); @@ -781,8 +1390,12 @@ export function glQuestionTriplet(_, rows, spec = {}) { injectOnce("gl-question-triplet-css", TRIPLET_CSS); const byItem = new Map(rows.map((r) => [r.item, r])); - const current = seedItems.map((i) => byItem.get(i)).filter(Boolean).slice(0, count); - while (current.length < count) current.push(rows[Math.floor(Math.random() * rows.length)]); + const current = seedItems + .map((i) => byItem.get(i)) + .filter(Boolean) + .slice(0, count); + while (current.length < count) + current.push(rows[Math.floor(Math.random() * rows.length)]); const wrap = el("div"); const render = (card, q) => { @@ -805,16 +1418,324 @@ export function glQuestionTriplet(_, rows, spec = {}) { for (const card of cards) { const inner = card.querySelector(".inner"); inner.classList.add("leaving"); - inner.addEventListener("animationend", () => { - inner.classList.remove("leaving"); - render(card, rows[Math.floor(Math.random() * rows.length)]); - inner.classList.add("entering"); - inner.addEventListener("animationend", () => inner.classList.remove("entering"), { once: true }); - }, { once: true }); + inner.addEventListener( + "animationend", + () => { + inner.classList.remove("leaving"); + render(card, rows[Math.floor(Math.random() * rows.length)]); + inner.classList.add("entering"); + inner.addEventListener( + "animationend", + () => inner.classList.remove("entering"), + { once: true }, + ); + }, + { once: true }, + ); } }; - const btnRow = el("div", "display:flex;justify-content:center;margin-top:.9rem;"); + const btnRow = el( + "div", + "display:flex;justify-content:center;margin-top:.9rem;", + ); btnRow.appendChild(btn); wrap.appendChild(btnRow); return frame(wrap, { caption }); } + +/* ------------------------------------------------------------ glHfSamples */ +// Hugging-Face-dataset-viewer-style table of sample rows with a refresh +// button that draws a fresh random window from the dataset. Two live modes: +// · unpinned (default): the datasets-server /rows API — always the CURRENT +// revision, no way to pin (the API has no revision parameter). +// · pinned: spec.pin reads the parquet export at a fixed commit of the +// refs/convert/parquet branch via HTTP range requests — the exact +// audited snapshot forever, no server. Requires the hyparquet dep: +// glHfSamples({ hyparquet }, seeds, { …, pin: { revision, file } }) +// with hyparquet imported from /assets/third-party/hyparquet.mjs. +// Seeds render immediately and deterministically (offline-safe); the live +// fetch happens only on refresh, or on load when no seeds are given. The +// footer reports the served slice and revision (X-Revision header when +// unpinned, the pinned sha otherwise). +// seedRows: rows shown on load and used as fallback when the fetch fails. +// spec: { dataset, config, split, — HF ids, e.g. "mandarjoshi/trivia_qa" +// count = 3, — rows per draw +// columns, — subset/order of columns (default: all) +// format = {}, — per-column value -> display string +// pin, — { revision: , +// file: "cfg/split/0000.parquet" } +// caption } +const HFS_API = "https://datasets-server.huggingface.co"; +const HFS_SIZES = new Map(); // "ds|cfg|split" -> promise of split row count +const HFS_PINS = new Map(); // pinned parquet url -> promise of {file, metadata, total} + +const HFS_CSS = ` + .gl-hfs { border: 1px solid ${palette.hairline}; border-radius: 12px; background: #fff; + font-family: ${FONT}; overflow: hidden; } + .gl-hfs-bar { display: flex; justify-content: space-between; align-items: center; gap: .8rem; + padding: .6rem .95rem; border-bottom: 1px solid ${palette.hairline}; background: ${palette.paper}; } + .gl-hfs-id { font-family: "Geist Mono", monospace; font-size: .74rem; color: ${palette.muted}; } + .gl-hfs-id a { color: ${palette.mintDeep}; text-decoration: none; + border-bottom: 1px solid ${palette.mintEdge}; } + .gl-hfs-scroll { overflow-x: auto; } + .gl-hfs table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: .86rem; } + .gl-hfs th { text-align: left; font-weight: 600; font-size: .74rem; color: ${palette.ink}; + padding: .5rem .75rem; border-bottom: 1px solid ${palette.hairline}; } + .gl-hfs th .t { display: block; font-family: "Geist Mono", monospace; font-weight: 400; + font-size: .64rem; color: ${palette.soft}; } + .gl-hfs td { padding: .55rem .75rem; border-bottom: 1px solid #f0f0f0; vertical-align: top; + color: ${palette.ink}; line-height: 1.45; overflow-wrap: break-word; } + .gl-hfs td > div { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; + overflow: hidden; height: 4.35em; } + .gl-hfs .idx { width: 5.6em; padding-left: .4rem; padding-right: .6rem; text-align: right; + font-family: "Geist Mono", monospace; font-size: .68rem; color: ${palette.soft}; + white-space: nowrap; overflow-wrap: normal; } + .gl-hfs tbody { transition: opacity .18s ease; } + .gl-hfs tbody tr:hover { background: ${palette.bg}; } + .gl-hfs tbody tr:last-child td { border-bottom: none; } + .gl-hfs-hd { display: flex; flex-direction: column; gap: .1rem; min-width: 0; } + .gl-hfs-status { display: block; font-family: "Geist Mono", monospace; font-size: .68rem; + color: ${palette.soft}; line-height: 1.5; min-height: 1.5em; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .gl-hfs-status.err { color: #b4443c; } + .gl-hfs-btn { background: transparent; color: ${palette.mintDeep}; + border: 1px solid ${palette.mintEdge}; border-radius: 999px; padding: .4rem .95rem; + font-family: ${FONT}; font-size: .8rem; cursor: pointer; white-space: nowrap; + transition: background .15s, border-color .15s; } + .gl-hfs-btn:hover { background: ${palette.mintSoft}; border-color: ${palette.mintDeep}; } + .gl-hfs-btn:disabled { opacity: .45; cursor: default; } +`; + +export function glHfSamples(deps, seedRows, spec = {}) { + const { + dataset, + config, + split, + count = 3, + columns, + format = {}, + caption, + pin, + } = spec; + const hyparquet = deps?.hyparquet; + injectOnce("gl-hfs-css", HFS_CSS); + const seeds = (seedRows ?? []).slice(); + let cols = + columns ?? + (seeds.length ? Object.keys(seeds[0]).filter((k) => k !== "row_idx") : []); + let types = {}; // column -> dtype label, learned from the API's features + + const card = el("div"); + card.className = "gl-hfs"; + const hfUrl = `https://huggingface.co/datasets/${dataset}`; + card.innerHTML = ` +
+
+ ${ + dataset + ? `${esc(dataset)} + · ${esc(config)} · ${esc(split)}` + : "committed sample" + } + +
+ +
+
+ +
`; + const [thead, tbody, status, btn] = [ + card.querySelector("thead tr"), + card.querySelector("tbody"), + card.querySelector('[data-role="status"]'), + card.querySelector(".gl-hfs-btn"), + ]; + + const fmt = (c, v) => + format[c] + ? format[c](v) + : typeof v === "string" + ? v + : v == null + ? "" + : JSON.stringify(v); + // JS-value fallback for the header dtype sublabels — used for seed rows and + // pinned parquet rows; the unpinned API path overrides with real dtypes. + const typeLabel = (v) => + typeof v === "string" + ? "string" + : typeof v === "number" + ? "double" + : typeof v === "bigint" + ? "int64" + : Array.isArray(v) + ? "list" + : v && typeof v === "object" + ? "struct" + : ""; + const head = () => { + thead.innerHTML = + `#` + + cols + .map( + (c) => + `${esc(c)}${esc(types[c] ?? "")}`, + ) + .join(""); + }; + const body = (entries) => { + tbody.innerHTML = entries + .map( + ({ idx, row }) => ` + ${idx} + ${cols.map((c) => `
${esc(String(fmt(c, row[c])).slice(0, 600))}
`).join("")} + `, + ) + .join(""); + }; + const set = (s, err = false) => { + status.textContent = s; + status.classList.toggle("err", err); + }; + + const showSeeds = (label) => { + const rows = + seeds.length > count + ? seeds + .slice() + .sort(() => Math.random() - 0.5) + .slice(0, count) + : seeds; + for (const c of cols) + if (types[c] == null) types[c] = typeLabel(rows[0]?.[c]); + head(); + body(rows.map((row) => ({ idx: row.row_idx ?? "—", row }))); + console.log("[glHfSamples] seed rows:", rows); + set(label); + }; + + const sizeOf = () => { + const key = `${dataset}|${config}|${split}`; + if (!HFS_SIZES.has(key)) + HFS_SIZES.set( + key, + fetch( + `${HFS_API}/size?dataset=${encodeURIComponent(dataset)}&config=${config}&split=${split}`, + ) + .then((r) => r.json()) + .then((d) => d.size.splits.find((s) => s.split === split).num_rows), + ); + return HFS_SIZES.get(key); + }; + + // Pinned mode: lazily open the parquet file at the pinned revision (footer + // metadata is fetched once per url and shared across instances); each draw + // then range-reads only the row group covering the random window. + const pinnedSource = () => { + const url = `https://huggingface.co/datasets/${dataset}/resolve/${pin.revision}/${pin.file}`; + if (!HFS_PINS.has(url)) + HFS_PINS.set( + url, + (async () => { + const file = await hyparquet.asyncBufferFromUrl({ url }); + const metadata = await hyparquet.parquetMetadataAsync(file); + return { file, metadata, total: Number(metadata.num_rows) }; + })(), + ); + return HFS_PINS.get(url); + }; + + async function drawPinned() { + btn.disabled = true; + tbody.style.opacity = ".35"; + set("reading pinned parquet…"); + try { + const { file, metadata, total } = await pinnedSource(); + const offset = Math.floor(Math.random() * Math.max(1, total - count)); + const rows = await hyparquet.parquetReadObjects({ + file, + metadata, + columns: cols.length ? cols : undefined, + rowStart: offset, + rowEnd: offset + count, + }); + console.log( + `[glHfSamples] ${dataset} ${pin.file} rows ${offset}–${offset + count - 1}` + + ` of ${total} rev=${pin.revision.slice(0, 7)} (pinned) — raw rows:`, + rows, + ); + if (!cols.length) cols = Object.keys(rows[0] ?? {}); + types = Object.fromEntries(cols.map((c) => [c, typeLabel(rows[0]?.[c])])); + head(); + body(rows.map((row, i) => ({ idx: offset + i, row }))); + set( + `rows ${offset.toLocaleString()}–${(offset + count - 1).toLocaleString()}` + + ` of ${total.toLocaleString()} · rev ${pin.revision.slice(0, 7)} (pinned)`, + ); + } catch (e) { + console.warn("[glHfSamples] pinned read failed:", e); + if (seeds.length) + showSeeds("pinned read failed — committed sample shown"); + else set(`pinned read failed (${e.message}) — try again`, true); + } finally { + btn.disabled = false; + tbody.style.opacity = "1"; + } + } + + async function draw() { + if (!dataset) return showSeeds("committed sample · shuffled"); + if (pin) { + if (!hyparquet) + return set( + "spec.pin needs the hyparquet dep: glHfSamples({ hyparquet }, …)", + true, + ); + return drawPinned(); + } + btn.disabled = true; + tbody.style.opacity = ".35"; + set("fetching…"); + try { + const total = await sizeOf(); + const offset = Math.floor(Math.random() * Math.max(1, total - count)); + const res = await fetch( + `${HFS_API}/rows?dataset=${encodeURIComponent(dataset)}` + + `&config=${config}&split=${split}&offset=${offset}&length=${count}`, + ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const rev = (res.headers.get("x-revision") || "").slice(0, 7); + const data = await res.json(); + console.log( + `[glHfSamples] ${dataset} ${config}/${split} ` + + `offset=${offset} of ${total} rev=${rev || "?"} — raw response:`, + data, + ); + if (!cols.length) cols = data.features.map((f) => f.name); + types = Object.fromEntries( + data.features.map((f) => [ + f.name, + f.type?.dtype ?? f.type?._type ?? (f.type ? "struct" : ""), + ]), + ); + head(); + body(data.rows.map((r) => ({ idx: r.row_idx, row: r.row }))); + set( + `rows ${offset.toLocaleString()}–${(offset + count - 1).toLocaleString()}` + + ` of ${total.toLocaleString()}${rev ? ` · rev ${rev}` : ""}`, + ); + } catch (e) { + console.warn("[glHfSamples] live fetch failed:", e); + if (seeds.length) showSeeds("live fetch failed — committed sample shown"); + else set(`live fetch failed (${e.message}) — try again`, true); + } finally { + btn.disabled = false; + tbody.style.opacity = "1"; + } + } + + btn.onclick = draw; + if (seeds.length) showSeeds(""); + else draw(); + return frame(card, { caption }); +} diff --git a/assets/third-party/hyparquet.mjs b/assets/third-party/hyparquet.mjs new file mode 100644 index 0000000..c030dd5 --- /dev/null +++ b/assets/third-party/hyparquet.mjs @@ -0,0 +1,5 @@ +// hyparquet v1.26.2 — parquet reader used by glHfSamples pinned mode. +// Vendored, self-contained ESM (no CDN). Regenerate with: +// npm i hyparquet@1.26.2 && printf 'export * from "hyparquet";' > e.mjs \ +// && npx esbuild e.mjs --bundle --format=esm --minify --outfile=assets/third-party/hyparquet.mjs +var ye=["BOOLEAN","INT32","INT64","INT96","FLOAT","DOUBLE","BYTE_ARRAY","FIXED_LEN_BYTE_ARRAY"],S=["PLAIN","GROUP_VAR_INT","PLAIN_DICTIONARY","RLE","BIT_PACKED","DELTA_BINARY_PACKED","DELTA_LENGTH_BYTE_ARRAY","DELTA_BYTE_ARRAY","RLE_DICTIONARY","BYTE_STREAM_SPLIT"],ze=["REQUIRED","OPTIONAL","REPEATED"],We=["UTF8","MAP","MAP_KEY_VALUE","LIST","ENUM","DECIMAL","DATE","TIME_MILLIS","TIME_MICROS","TIMESTAMP_MILLIS","TIMESTAMP_MICROS","UINT_8","UINT_16","UINT_32","UINT_64","INT_8","INT_16","INT_32","INT_64","JSON","BSON","INTERVAL"],He=["UNCOMPRESSED","SNAPPY","GZIP","LZO","BROTLI","LZ4","ZSTD","LZ4_RAW"],te=["DATA_PAGE","INDEX_PAGE","DICTIONARY_PAGE","DATA_PAGE_V2"],Ke=["UNORDERED","ASCENDING","DESCENDING"],Ze=["SPHERICAL","VINCENTY","THOMAS","ANDOYER","KARNEY"];function re(e){let t=ne(e);if(t.type===1)return{type:"Point",coordinates:ge(e,t)};if(t.type===2)return{type:"LineString",coordinates:he(e,t)};if(t.type===3)return{type:"Polygon",coordinates:Qe(e,t)};if(t.type===4){let n=[];for(let r=0;r1&&i<=7&&(s=t.getUint32(e.offset,n),e.offset+=4);let o=2;return f&&o++,f===3&&o++,{littleEndian:n,type:i,dim:o,count:s}}function ge(e,t){let n=[];for(let r=0;rn.toString(16).padStart(2,"0")).join("");return t.slice(0,8)+"-"+t.slice(8,12)+"-"+t.slice(12,16)+"-"+t.slice(16,20)+"-"+t.slice(20,32)}};function we(e,t,n,r){if(t&&n.endsWith("_DICTIONARY")){let i=e;e instanceof Uint8Array&&!(t instanceof Uint8Array)&&(i=new t.constructor(e.length));for(let f=0;fl.element.logical_type?.type==="VARIANT")&&s==="BYTE_ARRAY"&&o!=="UTF8"&&a?.type!=="STRING")return e;if(o==="DECIMAL"){let u=10**-(n.scale||0),m=new Array(e.length);for(let y=0;yr.timestampFromNanoseconds(St(l)));if(o==="DATE")return Array.from(e).map(l=>r.dateFromDays(l));if(o==="TIMESTAMP_MILLIS")return Array.from(e).map(l=>r.timestampFromMilliseconds(l));if(o==="TIMESTAMP_MICROS")return Array.from(e).map(l=>r.timestampFromMicroseconds(l));if(o==="JSON")return e.map(l=>r.jsonFromBytes(l));if(o==="BSON")throw new Error("parquet bson not supported");if(o==="INTERVAL")throw new Error("parquet interval not supported");if(a?.type==="GEOMETRY")return e.map(l=>r.geometryFromBytes(l));if(a?.type==="GEOGRAPHY")return e.map(l=>r.geographyFromBytes(l));if(a?.type==="UUID")return e.map(l=>r.uuidFromBytes(l));if(o==="UTF8"||a?.type==="STRING"||i&&s==="BYTE_ARRAY")return e.map(l=>r.stringFromBytes(l));if(o==="UINT_64"||a?.type==="INTEGER"&&a.bitWidth===64&&!a.isSigned){if(e instanceof BigInt64Array)return new BigUint64Array(e.buffer,e.byteOffset,e.length);let l=_?new Array(e.length):new BigUint64Array(e.length);for(let u=0;u=2n**BigInt(n-1)&&(t-=2n**BigInt(n)),Number(t)}function St(e){let t=(e>>64n)-2440588n,n=e&0xffffffffffffffffn;return t*86400000000000n+n}function Ie(e){if(!e)return;let t=e[1]<<8|e[0],n=t>>15?-1:1,r=t>>10&31,i=t&1023;return r===0?n*2**-14*(i/1024):r===31?i?NaN:n*(1/0):n*2**(r-15)*(1+i/1024)}function Xe(e,t,n){let r=e[t],i=[],f=1;if(r.num_children)for(;i.lengths.element.name===i);if(!f)throw new Error(`parquet schema element not found: ${t}`);r.push(f),n=f}return r}function be(e){let t=[];function n(r){if(r.children.length)for(let i of r.children)n(i);else t.push(r.path.join("."))}return n(e),t}function ve(e){let t=0;for(let{element:n}of e)n.repetition_type==="REPEATED"&&t++;return t}function W(e){let t=0;for(let{element:n}of e.slice(1))n.repetition_type!=="REQUIRED"&&t++;return t}function et(e){if(!e||e.element.converted_type!=="LIST"||e.children.length>1)return!1;let t=e.children[0];return!(t.children.length>1||t.element.repetition_type!=="REPEATED")}function tt(e){if(!e||e.element.converted_type!=="MAP"||e.children.length>1)return!1;let t=e.children[0];return!(t.children.length!==2||t.element.repetition_type!=="REPEATED"||t.children.find(i=>i.element.name==="key")?.element.repetition_type==="REPEATED"||t.children.find(i=>i.element.name==="value")?.element.repetition_type==="REPEATED")}function xe(e){if(e.length!==2)return!1;let[,t]=e;return!(t.element.repetition_type==="REPEATED"||t.children.length)}function L(e){let t={},n=0;for(;e.offset>4;n=f?n+f:nt(e),t[`field_${n}`]=Te(e,i)}return t}function Te(e,t){switch(t){case 1:return!0;case 2:return!1;case 3:return e.view.getInt8(e.offset++);case 4:case 5:return nt(e);case 6:return fe(e);case 7:{let n=e.view.getFloat64(e.offset,!0);return e.offset+=8,n}case 8:{let n=O(e),r=new Uint8Array(e.view.buffer,e.view.byteOffset+e.offset,n);return e.offset+=n,r}case 9:{let n=e.view.getUint8(e.offset++),r=n&15,i=n>>4;i===15&&(i=O(e));let f=r===1||r===2,s=new Array(i);for(let o=0;o>>1^-(t&1)}function fe(e){let t=Bt(e);return t>>1n^-(t&1n)}function rt(e,t){let n=new Map,r=t?.find(({key:f})=>f==="geo")?.value,i=(r&&JSON.parse(r)?.columns)??{};for(let[f,s]of Object.entries(i)){if(s.encoding!=="WKB")continue;let o=s.edges==="spherical"?"GEOGRAPHY":"GEOMETRY",a=s.crs?.id??s.crs?.ids?.[0],_=a?`${a.authority}:${a.code.toString()}`:void 0;n.set(f,{type:o,crs:_})}for(let f=1;f=0))throw new Error("parquet expected AsyncBuffer");let i=Math.max(0,e.byteLength-n),f=await e.slice(i,e.byteLength),s=new DataView(f);if(s.getUint32(f.byteLength-4,!0)!==827474256)throw new Error("parquet file invalid (footer != PAR1)");let o=s.getUint32(f.byteLength-8,!0);if(o>e.byteLength-8)throw new Error(`parquet metadata length ${o} exceeds available buffer ${e.byteLength-8}`);if(o+8>n){let a=e.byteLength-o-8,_=await e.slice(a,i),c=new ArrayBuffer(o+8),l=new Uint8Array(c);return l.set(new Uint8Array(_)),l.set(new Uint8Array(f),i-a),Re(c,{parsers:t,geoparquet:r})}else return Re(f,{parsers:t,geoparquet:r})}function Re(e,{parsers:t,geoparquet:n=!0}={}){if(!(e instanceof ArrayBuffer))throw new Error("parquet expected ArrayBuffer");let r=new DataView(e);if(t={...B,...t},r.byteLength<8)throw new Error("parquet file is too short");if(r.getUint32(r.byteLength-4,!0)!==827474256)throw new Error("parquet file invalid (footer != PAR1)");let i=r.byteLength-8,f=r.getUint32(i,!0);if(f>r.byteLength-8)throw new Error(`parquet metadata length ${f} exceeds available buffer ${r.byteLength-8}`);let s=i-f,a=L({view:r,offset:s}),_=a.field_1,c=a.field_2.map(p=>({type:ye[p.field_1],type_length:p.field_2,repetition_type:ze[p.field_3],name:N(p.field_4),num_children:p.field_5,converted_type:We[p.field_6],scale:p.field_7,precision:p.field_8,field_id:p.field_9,logical_type:Dt(p.field_10)})),l=c.filter(p=>p.type),u=a.field_3,m=a.field_4.map(p=>({columns:p.field_1.map((d,w)=>({file_path:N(d.field_1),file_offset:d.field_2,meta_data:d.field_3&&{type:ye[d.field_3.field_1],encodings:d.field_3.field_2?.map(h=>S[h]),path_in_schema:d.field_3.field_3.map(N),codec:He[d.field_3.field_4],num_values:d.field_3.field_5,total_uncompressed_size:d.field_3.field_6,total_compressed_size:d.field_3.field_7,key_value_metadata:d.field_3.field_8?.map(h=>({key:N(h.field_1),value:N(h.field_2)})),data_page_offset:d.field_3.field_9,index_page_offset:d.field_3.field_10,dictionary_page_offset:d.field_3.field_11,statistics:Ut(d.field_3.field_12,l[w],t),encoding_stats:d.field_3.field_13?.map(h=>({page_type:te[h.field_1],encoding:S[h.field_2],count:h.field_3})),bloom_filter_offset:d.field_3.field_14,bloom_filter_length:d.field_3.field_15,size_statistics:d.field_3.field_16&&{unencoded_byte_array_data_bytes:d.field_3.field_16.field_1,repetition_level_histogram:d.field_3.field_16.field_2,definition_level_histogram:d.field_3.field_16.field_3},geospatial_statistics:d.field_3.field_17&&{bbox:d.field_3.field_17.field_1&&{xmin:d.field_3.field_17.field_1.field_1,xmax:d.field_3.field_17.field_1.field_2,ymin:d.field_3.field_17.field_1.field_3,ymax:d.field_3.field_17.field_1.field_4,zmin:d.field_3.field_17.field_1.field_5,zmax:d.field_3.field_17.field_1.field_6,mmin:d.field_3.field_17.field_1.field_7,mmax:d.field_3.field_17.field_1.field_8},geospatial_types:d.field_3.field_17.field_2}},offset_index_offset:d.field_4,offset_index_length:d.field_5,column_index_offset:d.field_6,column_index_length:d.field_7,crypto_metadata:d.field_8,encrypted_column_metadata:d.field_9})),total_byte_size:p.field_2,num_rows:p.field_3,sorting_columns:p.field_4?.map(d=>({column_idx:d.field_1,descending:d.field_2,nulls_first:d.field_3})),file_offset:p.field_5,total_compressed_size:p.field_6,ordinal:p.field_7})),y=a.field_5?.map(p=>({key:N(p.field_1),value:N(p.field_2)})),g=N(a.field_6);return n&&rt(c,y),{version:_,schema:c,num_rows:u,row_groups:m,key_value_metadata:y,created_by:g,metadata_length:f}}function T({schema:e}){return ie(e,[])[0]}function Dt(e){return e?.field_1?{type:"STRING"}:e?.field_2?{type:"MAP"}:e?.field_3?{type:"LIST"}:e?.field_4?{type:"ENUM"}:e?.field_5?{type:"DECIMAL",scale:e.field_5.field_1,precision:e.field_5.field_2}:e?.field_6?{type:"DATE"}:e?.field_7?{type:"TIME",isAdjustedToUTC:e.field_7.field_1,unit:it(e.field_7.field_2)}:e?.field_8?{type:"TIMESTAMP",isAdjustedToUTC:e.field_8.field_1,unit:it(e.field_8.field_2)}:e?.field_10?{type:"INTEGER",bitWidth:e.field_10.field_1,isSigned:e.field_10.field_2}:e?.field_11?{type:"NULL"}:e?.field_12?{type:"JSON"}:e?.field_13?{type:"BSON"}:e?.field_14?{type:"UUID"}:e?.field_15?{type:"FLOAT16"}:e?.field_16?{type:"VARIANT",specification_version:e.field_16.field_1}:e?.field_17?{type:"GEOMETRY",crs:N(e.field_17.field_1)}:e?.field_18?{type:"GEOGRAPHY",crs:N(e.field_18.field_1),algorithm:Ze[e.field_18.field_2]}:e}function it(e){if(e.field_1)return"MILLIS";if(e.field_2)return"MICROS";if(e.field_3)return"NANOS";throw new Error("parquet time unit required")}function Ut(e,t,n){return e&&{max:C(e.field_1,t,n),min:C(e.field_2,t,n),null_count:e.field_3,distinct_count:e.field_4,max_value:C(e.field_5,t,n),min_value:C(e.field_6,t,n),is_max_value_exact:e.field_7,is_min_value_exact:e.field_8}}function C(e,t,n){let{type:r,converted_type:i,logical_type:f}=t;if(e===void 0)return e;if(r==="BOOLEAN")return e[0]===1;if(r==="BYTE_ARRAY")return n.stringFromBytes(e);let s=new DataView(e.buffer,e.byteOffset,e.byteLength);return r==="FLOAT"&&s.byteLength===4?s.getFloat32(0,!0):r==="DOUBLE"&&s.byteLength===8?s.getFloat64(0,!0):r==="INT32"&&i==="DATE"?n.dateFromDays(s.getInt32(0,!0)):r==="INT64"&&i==="TIMESTAMP_MILLIS"?n.timestampFromMilliseconds(s.getBigInt64(0,!0)):r==="INT64"&&i==="TIMESTAMP_MICROS"?n.timestampFromMicroseconds(s.getBigInt64(0,!0)):r==="INT64"&&f?.type==="TIMESTAMP"&&f?.unit==="NANOS"?n.timestampFromNanoseconds(s.getBigInt64(0,!0)):r==="INT64"&&f?.type==="TIMESTAMP"&&f?.unit==="MICROS"?n.timestampFromMicroseconds(s.getBigInt64(0,!0)):r==="INT64"&&f?.type==="TIMESTAMP"?n.timestampFromMilliseconds(s.getBigInt64(0,!0)):r==="INT32"&&s.byteLength===4?s.getInt32(0,!0):r==="INT64"&&s.byteLength===8?s.getBigInt64(0,!0):i==="DECIMAL"?Ee(e)*10**-(t.scale||0):f?.type==="FLOAT16"?Ie(e):f?.type==="UUID"?n.uuidFromBytes(e):e}function Mt(e,t,n=void 0){n={...B,...n};let r=L(e);return{null_pages:r.field_1,min_values:r.field_2.map(i=>C(i,t,n)),max_values:r.field_3.map(i=>C(i,t,n)),boundary_order:Ke[r.field_4],null_counts:r.field_5,repetition_level_histograms:r.field_6,definition_level_histograms:r.field_7}}function Ne(e){let t=L(e);return{page_locations:t.field_1.map(n=>({offset:n.field_1,compressed_page_size:n.field_2,first_row_index:n.field_3})),unencoded_byte_array_data_bytes:t.field_2}}var I=0xffffffffffffffffn,k=0x9e3779b185ebca87n,H=0xc2b2ae3d27d4eb4fn,ft=0x165667b19e3779f9n,st=0x85ebca77c2b2ae63n,ot=0x27d4eb2f165667c5n;function $(e,t){return(e<>64n-t)&I}function G(e,t){return e=e+t*H&I,e=$(e,31n),e*k&I}function oe(e,t){return e^=G(0n,t),e*k+st&I}function D(e,t=0n){let n=new DataView(e.buffer,e.byteOffset,e.byteLength),r=e.byteLength,i=0,f;if(r>=32){let s=t+k+H&I,o=t+H&I,a=t,_=t-k&I;for(;i+32<=r;)s=G(s,n.getBigUint64(i,!0)),i+=8,o=G(o,n.getBigUint64(i,!0)),i+=8,a=G(a,n.getBigUint64(i,!0)),i+=8,_=G(_,n.getBigUint64(i,!0)),i+=8;f=$(s,1n)+$(o,7n)+$(a,12n)+$(_,18n)&I,f=oe(f,s),f=oe(f,o),f=oe(f,a),f=oe(f,_)}else f=t+ot&I;for(f=f+BigInt(r)&I;i+8<=r;)f^=G(0n,n.getBigUint64(i,!0)),f=$(f,27n)*k+st&I,i+=8;for(i+4<=r&&(f^=BigInt(n.getUint32(i,!0))*k&I,f=$(f,23n)*H+ft&I,i+=4);i>33n,f=f*H&I,f^=f>>29n,f=f*ft&I,f^=f>>32n,f}var Pt=new TextEncoder,$t=new Uint32Array([1203114875,1150766481,2284105051,2729912477,1884591559,770785867,2667333959,1550580529]);function Ct(e,t){return Number((e>>32n)*BigInt(t)>>32n)}function Ft(e){let t=new Uint32Array(8),n=Number(e&0xffffffffn)|0;for(let r=0;r<8;r++)t[r]=1<<(Math.imul(n,$t[r])>>>27);return t}function Be(e,t){let n=Ct(t,e.length>>3)<<3,r=Ft(t);for(let i=0;i<8;i++)if((e[n+i]&r[i])===0)return!1;return!0}function lt(e){let t=L(e),n=t.field_1;if(typeof n!="number"||n<=0||n%32!==0||!t.field_2?.field_1||!t.field_3?.field_1||!t.field_4?.field_1)return;let{view:r,offset:i}=e;if(i+n>r.byteLength)throw new Error(`parquet bloom filter truncated: need ${n} bytes, have ${r.byteLength-i}`);let f=new Uint32Array(n>>2);for(let s=0;su.slice(o,a));let _=new Headers(s.headers),c=a===void 0?"":a-1;_.set("Range",`bytes=${o}-${c}`);let l=await i(e,{...s,headers:_});if(!l.ok||!l.body)throw new Error(`fetch failed ${l.status}`);if(l.status===200)return f=l.arrayBuffer(),f.then(u=>u.slice(o,a));if(l.status===206)return l.arrayBuffer();throw new Error(`fetch received unexpected status code ${l.status}`)}}}function qt({byteLength:e,slice:t},{minSize:n=Le}={}){if(et)throw new Error(`invalid empty range [${e}, ${t}]`);return`${e},${t}`}else return n===void 0?`${e},`:`${e},${n}`}function le(e){if(!e)return[];if(e.length===1)return e[0];let t=[];for(let n of e)se(t,n);return t}function K(e){if(!e)return[];let t=[];return"$and"in e&&Array.isArray(e.$and)?t.push(...e.$and.flatMap(K)):"$or"in e&&Array.isArray(e.$or)?t.push(...e.$or.flatMap(K)):"$nor"in e&&Array.isArray(e.$nor)?t.push(...e.$nor.flatMap(K)):t.push(...Object.keys(e).map(n=>n.split(".")[0])),[...new Set(t)]}function V(e,t,n=!0){return"$and"in t&&Array.isArray(t.$and)?t.$and.every(r=>V(e,r,n)):"$or"in t&&Array.isArray(t.$or)?t.$or.some(r=>V(e,r,n)):"$nor"in t&&Array.isArray(t.$nor)?!t.$nor.some(r=>V(e,r,n)):Object.entries(t).every(([r,i])=>{let f=Gt(e,r);return typeof i!="object"||i===null||Array.isArray(i)?U(f,i,n):Object.entries(i||{}).every(([s,o])=>s==="$gt"?f>o:s==="$gte"?f>=o:s==="$lt"?fZ({rowGroup:e,physicalColumns:t,filter:s,strict:r,bloomFilters:i,schemaElements:f}));if("$or"in n&&Array.isArray(n.$or))return n.$or.every(s=>Z({rowGroup:e,physicalColumns:t,filter:s,strict:r,bloomFilters:i,schemaElements:f}));if("$nor"in n&&Array.isArray(n.$nor))return!1;for(let[s,o]of Object.entries(n)){let a=t.indexOf(s);if(a===-1)continue;let _=e.columns[a].meta_data?.statistics,{min:c,max:l,min_value:u,max_value:m}=_||{},y=u!==void 0?u:c,g=m!==void 0?m:l,p=y!==void 0&&g!==void 0,d=i?.[s],w=f?.[s];for(let[h,A]of Object.entries(o||{})){if(p&&(h==="$gt"&&g<=A||h==="$gte"&&g=A||h==="$lte"&&y>A||h==="$eq"&&(Ag)||h==="$ne"&&U(y,g,r)&&U(y,A,r)||h==="$in"&&Array.isArray(A)&&A.every(E=>Eg)||h==="$nin"&&Array.isArray(A)&&U(y,g,r)&&A.includes(y)))return!0;if(d&&w){if(h==="$eq"){let E=Oe(A,w);if(E!==void 0&&!Be(d.blocks,E))return!0}if(h==="$in"&&Array.isArray(A)&&A.length>0){let E=!0;for(let P of A){let R=Oe(P,w);if(R===void 0||Be(d.blocks,R)){E=!1;break}}if(E)return!0}}}}return!1}function Gt(e,t){let n=e;for(let r of t.split("."))n=n?.[r];return n}var Vt=1<<21;function dt({metadata:e,rowStart:t=0,rowEnd:n=1/0,columns:r,filter:i,filterStrict:f=!0,useOffsetIndex:s=!1,bloomFiltersByGroup:o,schemaElements:a}){if(!e)throw new Error("parquetPlan requires metadata");let _=[],c=[],l=[],u=be(T(e)),m=0,y=0;for(let g of e.row_groups){let p=Number(g.num_rows),d=m+p,w=o?.[y];if(p>0&&d>t&&mE&&(E=ee),s&&x.offset_index_offset&&x.offset_index_length&&(t>m||n({})),f=at(n);if(f.size===0)return i;let s=be(T(t)),o=[];return t.row_groups.forEach((a,_)=>{if(!Z({rowGroup:a,physicalColumns:s,filter:n,strict:r}))for(let c of f){let l=s.indexOf(c);if(l===-1)continue;let u=a.columns[l]?.meta_data;if(!u?.bloom_filter_offset||!u.bloom_filter_length)continue;let m=Number(u.bloom_filter_offset),y=m+u.bloom_filter_length;o.push((async()=>{let g=await e.slice(m,y),p=lt({view:new DataView(g),offset:0});p&&(i[_][c]=p)})())}}),o.length&&await Promise.all(o),i}function mt(e,{fetches:t}){let n=t.map(({startByte:r,endByte:i})=>e.slice(r,i));return{byteLength:e.byteLength,slice(r,i=e.byteLength){let f=t.findIndex(({startByte:s,endByte:o})=>s<=r&&i<=o);if(f<0)return e.slice(r,i);if(t[f].startByte!==r||t[f].endByte!==i){let s=r-t[f].startByte,o=i-t[f].startByte;return n[f]instanceof Promise?n[f].then(a=>a.slice(s,o)):n[f].slice(s,o)}else return n[f]}}}var Me=new TextDecoder,pt=new WeakMap;function Pe(e,t=B){if(Array.isArray(e))return e.map(n=>Pe(n,t));if(typeof e!="object")return e;if("metadata"in e){let n=jt(e.metadata),r=e.typed_value&&ae(e.typed_value,n,t),i=e.value&&Q(ce(e.value),n,t);return r&&i?{...i,...r}:r??i}return e}function ae(e,t,n){if(e instanceof Date)return e;if(e&&typeof e=="object"&&!Array.isArray(e)&&!(e instanceof Uint8Array)){if("typed_value"in e&&e.typed_value!==null&&e.typed_value!==void 0)return ae(e.typed_value,t,n);if("value"in e&&e.value instanceof Uint8Array)return Q(ce(e.value),t,n);if("typed_value"in e||"value"in e)return null;let r={};for(let[i,f]of Object.entries(e))t.dictionary.includes(i)&&(r[i]=ae(f,t,n));return r}return e instanceof Uint8Array?Q(ce(e),t,n):Array.isArray(e)?e.map(r=>ae(r,t,n)):e}function ce(e){return{view:new DataView(e.buffer,e.byteOffset,e.byteLength),offset:0}}function jt(e){let t=pt.get(e.buffer);t||(t=new Map,pt.set(e.buffer,t));let n=`${e.byteOffset}:${e.byteLength}`,r=t.get(n);if(r)return r;let i=ce(e),f=i.view.getUint8(i.offset++),s=f&15;if(s!==1)throw new Error(`parquet unsupported variant metadata version: ${s}`);let o=(f>>4&1)===1,a=(f>>6&3)+1,_=q(i,a),c=new Array(_+1);for(let y=0;y>2;if(i===0)return zt(e,f,n);if(i===2)return Wt(e,f,t,n);if(i===3)return Ht(e,f,t,n);let s=new Uint8Array(e.view.buffer,e.view.byteOffset+e.offset,f);return e.offset+=f,Me.decode(s)}function zt(e,t,n){switch(t){case 0:return null;case 1:return!0;case 2:return!1;case 3:{let r=e.view.getInt8(e.offset);return e.offset+=1,r}case 4:{let r=e.view.getInt16(e.offset,!0);return e.offset+=2,r}case 5:{let r=e.view.getInt32(e.offset,!0);return e.offset+=4,r}case 6:{let r=e.view.getBigInt64(e.offset,!0);return e.offset+=8,r}case 7:{let r=e.view.getFloat64(e.offset,!0);return e.offset+=8,r}case 8:return Ue(e,4);case 9:return Ue(e,8);case 10:return Ue(e,16);case 11:{let r=e.view.getInt32(e.offset,!0);return e.offset+=4,n.dateFromDays(r)}case 12:case 13:{let r=e.view.getBigInt64(e.offset,!0);return e.offset+=8,n.timestampFromMicroseconds(r)}case 14:{let r=e.view.getFloat32(e.offset,!0);return e.offset+=4,r}case 15:return yt(e);case 16:{let r=yt(e);return Me.decode(r)}case 17:{let r=e.view.getBigInt64(e.offset,!0);return e.offset+=8,r}case 18:case 19:{let r=e.view.getBigInt64(e.offset,!0);return e.offset+=8,n.timestampFromNanoseconds(r)}case 20:{let r=new Uint8Array(e.view.buffer,e.view.byteOffset+e.offset,16);e.offset+=16;let i=Array.from(r,f=>f.toString(16).padStart(2,"0")).join("");return`${i.slice(0,8)}-${i.slice(8,12)}-${i.slice(12,16)}-${i.slice(16,20)}-${i.slice(20)}`}default:throw new Error(`parquet unsupported variant primitive type: ${t}`)}}function Wt(e,t,n,r){let i=(t&3)+1,f=(t>>2&3)+1,o=t>>4&1?q(e,4):e.view.getUint8(e.offset++),a=new Array(o);for(let l=0;l>2&1,s=i+1,o=q(e,f?4:1),a=new Array(o+1);for(let l=0;ly.repetition_type),a=0,_=[e],c=e,l=0,u=0,m=0;if(n[0])for(;l>m&g;for(m+=u;m>=8;)m-=8n,e.offset++,m&&(p|=BigInt(e.view.getUint8(e.offset))<>>1;Kt(e,o,t,n,f),f+=o}}e.offset=i+r}function Kt(e,t,n,r,i){let f=n+7>>3,s=0;for(let o=0;o>1<<3,s=(1<8?(_-=8,a-=8,o>>>=8):a-_>_&s),f--,_+=n);return i}function Fe(e,t,n,r){let i=Qt(n,r),f=new Uint8Array(t*i);for(let s=0;s=n)throw new Error("invalid snappy length header");for(;i=n)throw new Error("missing eof marker");if((s&3)===0){let a=(s>>>2)+1;if(a>60){if(i+3>=n)throw new Error("snappy error literal pos + 3 >= inputLength");let _=a-60;a=e[i]+(e[i+1]<<8)+(e[i+2]<<16)+(e[i+3]<<24),a=(a&sn[_])+1,i+=_}if(i+a>n)throw new Error("snappy error literal exceeds input length");At(e,i,t,f,a),i+=a,f+=a}else{let a=0;switch(s&3){case 1:o=(s>>>2&7)+4,a=e[i]+(s>>>5<<8),i++;break;case 2:if(n<=i+1)throw new Error("snappy error end of input");o=(s>>>2)+1,a=e[i]+(e[i+1]<<8),i+=2;break;case 3:if(n<=i+3)throw new Error("snappy error end of input");o=(s>>>2)+1,a=e[i]+(e[i+1]<<8)+(e[i+2]<<16)+(e[i+3]<<24),i+=4;break;default:break}if(a===0||isNaN(a))throw new Error(`invalid offset ${a} pos ${i} inputLength ${n}`);if(a>f)throw new Error("cannot copy from before start of buffer");At(t,f-a,t,f,o),f+=o}}if(f!==r)throw new Error("premature end of input")}function Et(e,t,{type:n,element:r,schemaPath:i}){let f=new DataView(e.buffer,e.byteOffset,e.byteLength),s={view:f,offset:0},o,a=ln(s,t,i),{definitionLevels:_,numNulls:c}=an(s,t,i),l=t.num_values-c;if(t.encoding==="PLAIN")o=J(s,n,l,r.type_length);else if(t.encoding==="PLAIN_DICTIONARY"||t.encoding==="RLE_DICTIONARY"||t.encoding==="RLE"){let u=n==="BOOLEAN"?1:f.getUint8(s.offset++);u?(o=new Array(l),n==="BOOLEAN"?(M(s,u,o),o=o.map(m=>!!m)):M(s,u,o,f.byteLength-s.offset)):o=new Uint8Array(l)}else if(t.encoding==="BYTE_STREAM_SPLIT")o=Fe(s,l,n,r.type_length);else if(t.encoding==="DELTA_BINARY_PACKED")o=n==="INT32"?new Int32Array(l):new BigInt64Array(l),z(s,l,o);else if(t.encoding==="DELTA_LENGTH_BYTE_ARRAY")o=new Array(l),Ce(s,l,o);else throw new Error(`parquet unsupported encoding: ${t.encoding}`);return{definitionLevels:_,repetitionLevels:a,dataPage:o}}function ln(e,t,n){if(n.length>1){let r=ve(n);if(r){let i=new Array(t.num_values);return M(e,me(r),i),i}}return[]}function an(e,t,n){let r=W(n);if(!r)return{definitionLevels:[],numNulls:0};let i=new Array(t.num_values);M(e,me(r),i);let f=t.num_values;for(let s of i)s===r&&f--;return f===0&&(i.length=0),{definitionLevels:i,numNulls:f}}function _e(e,t,n,r){let i,f=r?.[n];if(n==="UNCOMPRESSED")i=e;else if(f)i=f(e,t);else if(n==="SNAPPY")i=new Uint8Array(t),ke(e,i);else throw new Error(`parquet unsupported compression codec: ${n}`);if(i?.length!==t)throw new Error(`parquet decompressed page length ${i?.length} does not match header ${t}`);return i}function It(e,t,n){let i={view:new DataView(e.buffer,e.byteOffset,e.byteLength),offset:0},{type:f,element:s,schemaPath:o,codec:a,compressors:_}=n,c=t.data_page_header_v2;if(!c)throw new Error("parquet data page header v2 is undefined");let l=cn(i,c,o);i.offset=c.repetition_levels_byte_length;let u=un(i,c,o),m=t.uncompressed_page_size-c.definition_levels_byte_length-c.repetition_levels_byte_length,y=e.subarray(i.offset);c.is_compressed!==!1&&(y=_e(y,m,a,_));let g=new DataView(y.buffer,y.byteOffset,y.byteLength),p={view:g,offset:0},d,w=c.num_values-c.num_nulls;if(c.encoding==="PLAIN")d=J(p,f,w,s.type_length);else if(c.encoding==="RLE")d=new Array(w),M(p,1,d),d=d.map(h=>!!h);else if(c.encoding==="PLAIN_DICTIONARY"||c.encoding==="RLE_DICTIONARY"){let h=g.getUint8(p.offset++);d=new Array(w),M(p,h,d,m-1)}else if(c.encoding==="DELTA_BINARY_PACKED")d=f==="INT32"?new Int32Array(w):new BigInt64Array(w),z(p,w,d);else if(c.encoding==="DELTA_LENGTH_BYTE_ARRAY")d=new Array(w),Ce(p,w,d);else if(c.encoding==="DELTA_BYTE_ARRAY")d=new Array(w),wt(p,w,d);else if(c.encoding==="BYTE_STREAM_SPLIT")d=Fe(p,w,f,s.type_length);else throw new Error(`parquet unsupported encoding: ${c.encoding}`);return{definitionLevels:u,repetitionLevels:l,dataPage:d}}function cn(e,t,n){let r=ve(n);if(!r)return[];let i=new Array(t.num_values);return M(e,me(r),i,t.repetition_levels_byte_length),i}function un(e,t,n){let r=W(n);if(r){let i=new Array(t.num_values);return M(e,me(r),i,t.definition_levels_byte_length),i}}function me(e){return 32-Math.clz32(e)}function qe(e,{groupStart:t,selectStart:n,selectEnd:r},i,f){let{pathInSchema:s,schemaPath:o}=i,a=xe(o),_=[],c,l,u=0,m=0,y=f&&(()=>{l&&f({pathInSchema:s,columnData:l,rowStart:t+u-l.length,rowEnd:t+u})});for(;(a?u=e.view.byteLength-1);){let g=dn(e);if(g.type==="DICTIONARY_PAGE"){let{data:p}=bt(e,g,i,c,void 0,0);p&&(c=Ae(p,i))}else{let p=l?.length||0,d=bt(e,g,i,c,l,n-u);d.skipped?(_.length||(m+=d.skipped),u+=d.skipped):d.data&&l===d.data?u+=d.data.length-p:d.data&&d.data.length&&(y?.(),_.push(d.data),u+=d.data.length,l=d.data)}}return y?.(),{data:_,skipped:m}}function bt(e,t,n,r,i,f){let{type:s,element:o,schemaPath:a,codec:_,compressors:c}=n,l=new Uint8Array(e.view.buffer,e.view.byteOffset+e.offset,t.compressed_page_size);if(e.offset+=t.compressed_page_size,t.type==="DATA_PAGE"){let u=t.data_page_header;if(!u)throw new Error("parquet data page header is undefined");if(f>u.num_values&&xe(a))return{skipped:u.num_values};let m=_e(l,Number(t.uncompressed_page_size),_,c),{definitionLevels:y,repetitionLevels:g,dataPage:p}=Et(m,u,n),d=we(p,r,u.encoding,n),w=Array.isArray(i)?i:[];return{skipped:0,data:$e(w,y,g,d,a)}}else if(t.type==="DATA_PAGE_V2"){let u=t.data_page_header_v2;if(!u)throw new Error("parquet data page header v2 is undefined");if(f>u.num_rows)return{skipped:u.num_values};let{definitionLevels:m,repetitionLevels:y,dataPage:g}=It(l,t,n),p=we(g,r,u.encoding,n),d=Array.isArray(i)?i:[];return{skipped:0,data:$e(d,m,y,p,a)}}else if(t.type==="DICTIONARY_PAGE"){let u=t.dictionary_page_header;if(!u)throw new Error("parquet dictionary page header is undefined");let m=_e(l,Number(t.uncompressed_page_size),_,c),y={view:new DataView(m.buffer,m.byteOffset,m.byteLength),offset:0};return{skipped:0,data:J(y,s,u.num_values,o.type_length)}}else throw new Error(`parquet unsupported page type: ${t.type}`)}function dn(e){let t=L(e),n=te[t.field_1],r=t.field_2,i=t.field_3,f=t.field_4,s=t.field_5&&{num_values:t.field_5.field_1,encoding:S[t.field_5.field_2],definition_level_encoding:S[t.field_5.field_3],repetition_level_encoding:S[t.field_5.field_4],statistics:t.field_5.field_5&&{max:t.field_5.field_5.field_1,min:t.field_5.field_5.field_2,null_count:t.field_5.field_5.field_3,distinct_count:t.field_5.field_5.field_4,max_value:t.field_5.field_5.field_5,min_value:t.field_5.field_5.field_6}},o=t.field_6,a=t.field_7&&{num_values:t.field_7.field_1,encoding:S[t.field_7.field_2],is_sorted:t.field_7.field_3},_=t.field_8&&{num_values:t.field_8.field_1,num_nulls:t.field_8.field_2,num_rows:t.field_8.field_3,encoding:S[t.field_8.field_4],definition_levels_byte_length:t.field_8.field_5,repetition_levels_byte_length:t.field_8.field_6,is_compressed:t.field_8.field_7===void 0?!0:t.field_8.field_7,statistics:t.field_8.field_8};return{type:n,uncompressed_page_size:r,compressed_page_size:i,crc:f,data_page_header:s,index_page_header:o,dictionary_page_header:a,data_page_header_v2:_}}function vt(e,{metadata:t},n){let r=[];for(let i of n.chunks){let{data_page_offset:f,dictionary_page_offset:s,path_in_schema:o}=i.columnMetadata,a=ie(t.schema,o),_={pathInSchema:o,element:a[a.length-1].element,schemaPath:a,parsers:{...B,...e.parsers},...e,...i.columnMetadata},{startByte:c,endByte:l}=i.range;if(!("offsetIndex"in i)){r.push({pathInSchema:o,data:Promise.resolve(e.file.slice(c,l)).then(u=>{let m={view:new DataView(u),offset:0};return qe(m,n,_,e.onPage)})});continue}r.push({pathInSchema:o,data:Promise.resolve(e.file.slice(i.offsetIndex.startByte,i.offsetIndex.endByte)).then(async u=>{let{selectStart:m,selectEnd:y}=n,g=Ne({view:new DataView(u),offset:0}).page_locations,p=-1,d=s||fm&&(c=Number(v.offset),p=x),xl.data.then(({skipped:u,data:m})=>({skipped:u,data:le(m)})))),s=n-t;if(i==="object"){let l=Array(s);for(let u=0;ul.pathInSchema[0]).filter(l=>!r||r.includes(l)),a=r??o,_=a.map(l=>e.findIndex(u=>u.pathInSchema[0]===l)),c=Array(s);for(let l=0;lo.pathInSchema[0]===f.element.name);if(!s.length)continue;i.push({pathInSchema:f.path,data:(async()=>{let o=await Promise.all(s.map(l=>l.data)),a=new Map,_=1/0;for(let l=0;l_&&a.set(l,u.slice(0,_));j(a,f,n);let c=a.get(f.element.name);if(!c)throw new Error("parquet column data not assembled");return{data:[c],skipped:0}})()})}else{let s=r.find(o=>o.pathInSchema[0]===f.element.name);s&&i.push(s)}return{...e,asyncColumns:i}}async function xt(e){e.metadata??=await F(e.file,e);let{rowStart:t=0,rowEnd:n,columns:r,onChunk:i,onComplete:f,rowFormat:s,filter:o,filterStrict:a=!0}=e;if(o&&s!=="object")throw new Error('parquet filter requires rowFormat: "object"');let _=K(o);if(_.length){let p=T(e.metadata).children.map(w=>w.element.name),d=_.filter(w=>!p.includes(w));if(d.length)throw new Error(`parquet filter columns not found: ${d.join(", ")}`)}let c=r,l=!1;if(r&&o){let p=_.filter(d=>!r.includes(d));p.length&&(c=[...r,...p],l=!0)}let u=c!==r?{...e,columns:c}:e;u=await Lt(u);let m=Tt(u);if(!f&&!i){await pe(m);return}let y=T(e.metadata),g=m.map(p=>Ge(p,y,e.parsers));if(i)for(let p of g)for(let d of p.asyncColumns)d.data.then(({data:w,skipped:h})=>{let A=p.groupStart+h;for(let E of w)i({columnName:d.pathInSchema[0],columnData:E,rowStart:A,rowEnd:A+E.length}),A+=E.length},()=>{});if(f){await pe(g);let p=[];for(let d of g){let w=Math.max(t-d.groupStart,0),h=Math.min((n??1/0)-d.groupStart,d.groupRows),A=s==="object"?await Ye(d,w,h,c,"object"):await Ye(d,w,h,r,"array");if(o){for(let E of A)if(V(E,o,a)){if(l&&r)for(let P of _)r.includes(P)||delete E[P];p.push(E)}}else se(p,A)}f(p)}else await pe(g)}async function pe(e){let t=e.flatMap(i=>i.asyncColumns.map(f=>f.data)),r=(await Promise.allSettled(t)).find(i=>i.status==="rejected");if(r)throw r.reason}function Tt(e){if(!e.metadata)throw new Error("parquet requires metadata");let t=dt(e);return e.file=mt(e.file,t),t.groups.map(n=>vt(e,t,n))}async function Rt(e){if(e.columns?.length!==1)throw new Error("parquetReadColumn expected columns: [columnName]");e.metadata??=await F(e.file,e);let t=Tt(await Lt(e)),n=T(e.metadata),r=t.map(f=>Ge(f,n,e.parsers));await pe(r);let i=[];for(let f of r){let{data:s}=await f.asyncColumns[0].data;for(let o of s)se(i,o)}return i}async function Lt(e){if(!e.useBloomFilters||!e.filter||!e.metadata)return e;let t=T(e.metadata),n={};for(let i of t.children)n[i.element.name]=i.element;let r=await _t({file:e.file,metadata:e.metadata,filter:e.filter,filterStrict:e.filterStrict});return{...e,bloomFiltersByGroup:r,schemaElements:n}}function Y(e){return new Promise((t,n)=>{xt({...e,rowFormat:"object",onComplete:t}).catch(n)})}async function _n(e){if(!e.file||!(e.file.byteLength>=0))throw new Error("parquet expected AsyncBuffer");e.metadata??=await F(e.file,e);let{metadata:t,rowStart:n=0,columns:r,orderBy:i,filter:f}=e;if(n<0)throw new Error("parquet rowStart must be positive");let s=e.rowEnd??Number(t.num_rows);if(i&&!T(e.metadata).children.map(a=>a.element.name).includes(i))throw new Error(`parquet orderBy column not found: ${i}`);if(f&&!i&&s=s)break;a=c}return o.slice(n,s)}else if(f&&i){let o=r&&!r.includes(i)?[...r,i]:r,a=await Y({...e,rowStart:void 0,rowEnd:void 0,columns:o});if(a.sort((_,c)=>Nt(_[i],c[i])),o!==r)for(let _ of a)delete _[i];return a.slice(n,s)}else{if(f)return(await Y({...e,rowStart:void 0,rowEnd:void 0})).slice(n,s);if(typeof i=="string"){let o=await Rt({...e,rowStart:void 0,rowEnd:void 0,columns:[i]}),a=Array.from(o,(l,u)=>u).sort((l,u)=>Nt(o[l],o[u])).slice(n,s),_=await mn({...e,rows:a});return a.map(l=>_[l])}else return await Y(e)}}async function mn(e){let{file:t,rows:n}=e;e.metadata??=await F(t,e);let{row_groups:r}=e.metadata,i=Array(r.length).fill(!1),f=0,s=r.map(c=>f+=Number(c.num_rows));for(let c of n){let l=s.findIndex(u=>ct?1:0}export{kt as asyncBufferFromUrl,ut as byteLengthFromUrl,qt as cachedAsyncBuffer,le as flatten,Re as parquetMetadata,F as parquetMetadataAsync,_n as parquetQuery,xt as parquetRead,Y as parquetReadObjects,T as parquetSchema,Mt as readColumnIndex,Ne as readOffsetIndex,ke as snappyUncompress,De as toJson}; diff --git a/blog/index.html b/blog/index.html index 0705518..2c786d2 100644 --- a/blog/index.html +++ b/blog/index.html @@ -294,6 +294,30 @@

Blog

+ + +

July 2026

+

TriviaQA Audit Report

+

+ TriviaQA is one of the classic QA benchmarks. We discuss its history, how it is scored, and whether it has any signal left at the frontier. +

+ + Read more + + + + +
+

July 2026 · By James Mann

diff --git a/blog/posts/triviaqa/_header.html b/blog/posts/triviaqa/_header.html new file mode 100644 index 0000000..184346f --- /dev/null +++ b/blog/posts/triviaqa/_header.html @@ -0,0 +1,12 @@ +
+

Draft · July 2026

+

TriviaQA Audit Report

+

+ TriviaQA is one of the classic QA benchmarks. We discuss its history, how it + is scored, and whether it has any signal left at the frontier. +

+
+ + +
diff --git a/blog/posts/triviaqa/index.html b/blog/posts/triviaqa/index.html new file mode 100644 index 0000000..8db9b40 --- /dev/null +++ b/blog/posts/triviaqa/index.html @@ -0,0 +1,708 @@ + + + + + + + + + +TriviaQA Audit Report - Generality Labs + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + Back to blog + +
+

Draft · July 2026

+

TriviaQA Audit Report

+

+ TriviaQA is one of the classic QA benchmarks. We discuss its history, how it + is scored, and whether it has any signal left at the frontier. +

+
+ + +
+ + + + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+

Summary of findings

+
    +
  • Saturated. The dashboard max is 87.6%. Opus 4.8 scores 89% (on a subset of 500 questions). We estimate the realistic ceiling of this dataset at ≈92%.
  • +
  • Contaminated by design. Questions are scraped from trivia websites.
  • +
  • Displays a mild inverted-U relationship with ECI and compute on the Epoch dashboard.
  • +
+

What is TriviaQA?

+

TriviaQA is introduced in the 2017 paper “TriviaQA: A Large Scale Distantly Supervised Challenge Dataset for Reading Comprehension”:

+
+

TriviaQA includes 95K question-answer pairs authored by trivia enthusiasts and independently gathered evidence documents, six per question on average.

+
+

It was originally intended as a “reading comprehension dataset”. For each question–answer pair it provides evidence documents which are likely (but not guaranteed) to contain the answer to the question. The documents were gathered automatically from Wikipedia and web search results. The dataset was intended for ML models far weaker than the current AIs. For example, the paper evaluates an RNN, which scores 40%, far below the 80% human baseline.

+

In recent years the dataset has been used in a different mode — closed book. The model isn’t provided with evidence docs, nor is it allowed to use web search. It has to answer using only internal knowledge. All of Epoch’s external runs for this dataset are done in this mode (manually verified by checking each paper).

+

Despite this change, some papers still incorrectly describe this dataset as “reading comprehension” despite clearly using it in the closed-book mode, for example:

+ +

Splits: 76.5k train / 9.9k validation / 9.5k test. The answers for the test split are not provided.

+

Example items

+
+
+
+
+ +
+
+
+

Construct validity

+

While the original paper describes the dataset as “reading comprehension”, in the closed-book mode it becomes a general QA dataset — there is nothing for the model to read, aside from the question text.

+

What is the dataset measuring then? Perhaps “breadth of factual trivia-like knowledge (prior to 2017) and the ability to retrieve it”. It’s hard to pinpoint the construct more exactly — the authors don’t balance the dataset in any way.

+

From looking at the individual items, the distribution is skewed toward US/UK pop culture, sports, and Western history, and it’s English-only — it’s unclear if the score would generalise well to multilingual or non-Western factual knowledge.

+

For humans this closed-book version would measure some forms of crystallised intelligence. Knowledge is hard-won for humans, so the ability to quickly retrieve factual knowledge can be used as a proxy for general intelligence. However, for an AI, knowledge arises as a byproduct of compressing the training data, and thus the case for it correlating with forms of crystallised intelligence is weaker.

+

Outcome validity (scoring)

+

The dataset provides a correct answer (with aliases) for the train/validation subsets. There is an official scoring script. Papers from which Epoch takes their data score via the “exact match” (“EM”) method. It compares model output to the gold truth (and its aliases) after applying normalisation. The score is accuracy: the percentage of matches, where a match for an item means a match with any of the aliases.

+

The normalisation applies four transformations to both prediction and reference: lowercase everything, remove punctuation, remove the English articles “a”, “an”, “the”, and collapse extra whitespace. Then it requires the resulting strings to be character-for-character identical.

+

There is no way to abstain from giving an answer — wrong answers are scored as 0, the same as no answer. The drawback of this dataset is that it doesn’t measure anything related to epistemic calibration.

+

Saturation

+

Saturated for several years:

+
    +
  • The Epoch dashboard page (with “Frontier trend only” enabled)
  • +
  • LLM stats lists some newer models
  • +
+

Scores for the current frontier models are not provided on either leaderboard.

+

The original paper lists 79.7% as the human performance level (with the evidence docs). There are multiple top scores in the 80+% range, with the max being 87.6%.

+

However, these do not come from the current frontier models. To evaluate whether there is any room for realistic improvement, we ran Opus 4.8 on 500 samples and then used an LLM judge (also Opus 4.8, but with web search) to classify the reasons for the model giving a wrong answer.

+

Opus 4.8 correctly answered 445 out of 500 questions (89%). Most of the wrong answers are inexact matches — the model giving a correct answer that doesn’t match any of the provided aliases for the “gold truth”, for example:

+
+

Which World War 2 American general was known as ‘Vinegar Joe’?

+

Model answer: Joseph Stilwell. Gold truth: “Stilwell” (aliases: “Stillwell”, “Stilwell (disambiguation)”)

+
+

There are occasional outdated questions, for example:

+
+

Who is currently the Supreme Governor of the Church of England?

+

Model answer: King Charles III. Gold truth: “Queen Elizabeth II” (correct at the time of collection, but she’s dead now)

+
+
+

In geology, Greywacke is classed as what form of sedimentary rock?

+

Model answer: Sandstone. Gold truth: “Deep Ocean” (incorrect — Wikipedia lists “Sandstone”)

+
+

Some questions had missing information, for example: “In which town in Greater Manchester is the TV series set?” (no TV series name — maybe the question was supposed to have a picture?) or “Measuring from the closest point of each of these countries to the equator, which is the furthest from the equator?” (no list of countries).

+

There are only 17 / 500 questions where Opus 4.8 was cleanly and clearly wrong — 3.4%. Verdict: the benchmark is basically saturated; the actual realistic ceiling is ≈92% (0.89 + 0.034).

+

Contamination

+

Contaminated by design. It originally contains scraped trivia questions: “First we gathered question-answer pairs from 14 trivia and quiz-league websites”. Even if the canonical version of the dataset is filtered out of the training data, these trivia question sets are unlikely to be removed.

+

From “A Comprehensive Survey of Contamination Detection Methods in Large Language Models”:

+
+

[…] the Llama pre-training corpus and The Pile (Gao et al., 2020) are found to be contaminated for many popular evaluation benchmarks, notably BigBench, HellaSwag, PiQA, MMLU and TriviaQA

+
+

From “Generalization v.s. Memorization: Tracing Language Models’ Capabilities Back to Pretraining Data”:

+
+

Among the four tasks [TriviaQA, MMLU, WMT, and GSM8K], our analysis reveals that TriviaQA exhibits the strongest memorization effect, with task performance highly correlated to the n-gram distributions in the pretraining data.

+
+

Additionally, “Question and Answer Test-Train Overlap in Open-Domain Question Answering Datasets” highlights significant test–train overlap.

+

ECI vs TriviaQA and compute vs TriviaQA

+

TriviaQA seems to display a mild inverted-U relationship between compute (or ECI) and score on this benchmark. See the charts below — the shape of the data starts to bend downwards. The best models on this task aren’t the ones with the highest compute budget or the most capable.

+ +

Compare the TriviaQA dashboard to three randomly picked benchmarks: MATH Level 5, HellaSwag, BBH.

+ +

Hypotheses for the inverted U-shape

+
    +
  1. Saturation. Measurements near the ceiling are noisy — they don’t allow us to separate models by skill from each other.
  2. +
  3. Factual knowledge is not a good proxy for general capabilities in models. Models have a parameter budget, and you can “spend” this budget either to “buy” yourself better factual knowledge or general capabilities. Labs prefer the latter.
  4. +
  5. There aren’t a lot of measurements in general: only 29 if you don’t count duplicates (n-shot for different n).
  6. +
+ +
+
+
+
+ +
+
+ © 2026 Generality Labs +
+
+ + + + + + + + \ No newline at end of file diff --git a/blog/posts/triviaqa/index.qmd b/blog/posts/triviaqa/index.qmd new file mode 100644 index 0000000..f427065 --- /dev/null +++ b/blog/posts/triviaqa/index.qmd @@ -0,0 +1,155 @@ +--- +pagetitle: "TriviaQA Audit Report - Generality Labs" +format: + html: + page-layout: custom + theme: none + include-in-header: ../../../_includes/head.html + include-before-body: + - ../../../_includes/post-before.html + - _header.html + include-after-body: ../../../_includes/post-after.html + link-external-newwindow: true + embed-resources: false +--- + +```{ojs} +//| echo: false +glVersion = "dev" +gl = import(`/assets/gl-components/gl.js?v=${glVersion}`) +mc = import(`/assets/gl-components/model-colors.js?v=${glVersion}`) +hyparquet = import(`/assets/third-party/hyparquet.mjs?v=${glVersion}`) +``` + +## Summary of findings + +- **Saturated.** The dashboard max is 87.6%. Opus 4.8 scores 89% (on a subset of 500 questions). We estimate the realistic ceiling of this dataset at ≈92%. +- **Contaminated by design.** Questions are scraped from trivia websites. +- Displays a **mild inverted-U relationship** with ECI and compute on the Epoch dashboard. + +## What is TriviaQA? + +TriviaQA is introduced in the 2017 paper ["TriviaQA: A Large Scale Distantly Supervised Challenge Dataset for Reading Comprehension"](https://arxiv.org/abs/1705.03551): + +> TriviaQA includes 95K question-answer pairs authored by trivia enthusiasts and independently gathered evidence documents, six per question on average. + +It was originally intended as a "reading comprehension dataset". For each question–answer pair it provides evidence documents which are likely (but not guaranteed) to contain the answer to the question. The documents were gathered automatically from Wikipedia and web search results. The dataset was intended for ML models far weaker than the current AIs. For example, the paper evaluates an RNN, which scores 40%, far below the 80% human baseline. + +In recent years the dataset has been used in a different mode — _closed book_. The model isn't provided with evidence docs, nor is it allowed to use web search. It has to answer using only internal knowledge. All of Epoch's external runs for this dataset are done in this mode (manually verified by checking each paper). + +Despite this change, some papers still incorrectly describe this dataset as "reading comprehension" despite clearly using it in the closed-book mode, for example: + +- [Gemma: Open Models Based on Gemini Research and Technology](https://arxiv.org/pdf/2403.08295) +- [Claude 2 Model Card](https://www-cdn.anthropic.com/5c49cc247484cecf107c699baf29250302e5da70/claude-2-model-card.pdf) +- [Releasing Claude Instant 1.2](https://www.anthropic.com/news/releasing-claude-instant-1-2) + +Splits: 76.5k train / 9.9k validation / 9.5k test. The answers for the test split are not provided. + +### Example items + +```{ojs} +//| echo: false +gl.glHfSamples({ hyparquet }, [ + { question: "In what year was the best foreign film category introduced to the Academy Awards?", + answer: { value: "1948", aliases: ["one thousand, nine hundred and forty-eight"] } }, + { question: "What is the name of the strip of land between a golf tee and the green?", + answer: { value: "Fairway", aliases: ["Fair-way", "Fair Way", "Fairway (disambiguation)"] } }, + { question: "Which locks control entry to the Panama canal from the Atlantic end?", + answer: { value: "Gatun Lock", aliases: ["Gatun Locks", "Panama Canal Locks", "Gatún lock"] } }, +], { + dataset: "mandarjoshi/trivia_qa", config: "rc.nocontext", split: "validation", + // Pinned to the refs/convert/parquet commit current at audit time (July + // 2026): samples always come from this exact snapshot, regardless of any + // future dataset changes. Rows are range-read straight from the parquet + // shard in the reader's browser — no server, no copy in the repo. + pin: { revision: "bd67746ffd85804fdc6b2ec5b00222ea26f8117e", + file: "rc.nocontext/validation/0000.parquet" }, + columns: ["question", "answer"], count: 3, + format: { answer: (a) => a?.value + ? a.value + (a.aliases?.length ? ` · aliases: ${a.aliases.slice(0, 4).join(", ")}` : "") + : JSON.stringify(a) }, +}) +``` + +## Construct validity + +While the original paper describes the dataset as "reading comprehension", in the closed-book mode it becomes a general QA dataset — there is nothing for the model to read, aside from the question text. + +What is the dataset measuring then? Perhaps "breadth of factual trivia-like knowledge (prior to 2017) and the ability to retrieve it". It's hard to pinpoint the construct more exactly — the authors don't balance the dataset in any way. + +From looking at the individual items, the distribution is skewed toward US/UK pop culture, sports, and Western history, and it's English-only — it's unclear if the score would generalise well to multilingual or non-Western factual knowledge. + +For humans this closed-book version would measure some forms of crystallised intelligence. Knowledge is hard-won for humans, so the ability to quickly retrieve factual knowledge can be used as a proxy for general intelligence. However, for an AI, knowledge arises as a byproduct of compressing the training data, and thus the case for it correlating with forms of crystallised intelligence is weaker. + +## Outcome validity (scoring) + +The dataset provides a correct answer (with aliases) for the train/validation subsets. There is an official scoring script. Papers from which Epoch takes their data score via the "exact match" ("EM") method. It compares model output to the gold truth (and its aliases) after applying normalisation. The score is accuracy: the percentage of matches, where a match for an item means a match with any of the aliases. + +The normalisation applies four transformations to both prediction and reference: lowercase everything, remove punctuation, remove the English articles "a", "an", "the", and collapse extra whitespace. Then it requires the resulting strings to be character-for-character identical. + +There is no way to abstain from giving an answer — wrong answers are scored as 0, the same as no answer. The drawback of this dataset is that it doesn't measure anything related to epistemic calibration. + +## Saturation + +Saturated for several years: + +- The Epoch dashboard page (with "Frontier trend only" enabled) +- LLM stats lists some newer models + +Scores for the current frontier models are not provided on either leaderboard. + +The original paper lists 79.7% as the human performance level (with the evidence docs). There are multiple top scores in the 80+% range, with the max being 87.6%. + +However, these do not come from the current frontier models. To evaluate whether there is any room for realistic improvement, we ran Opus 4.8 on 500 samples and then used an LLM judge (also Opus 4.8, but with web search) to classify the reasons for the model giving a wrong answer. + +Opus 4.8 correctly answered 445 out of 500 questions (89%). Most of the wrong answers are inexact matches — the model giving a correct answer that doesn't match any of the provided aliases for the "gold truth", for example: + +> Which World War 2 American general was known as 'Vinegar Joe'? +> +> Model answer: **Joseph Stilwell**. Gold truth: "Stilwell" (aliases: "Stillwell", "Stilwell (disambiguation)") + +There are occasional outdated questions, for example: + +> Who is currently the Supreme Governor of the Church of England? +> +> Model answer: **King Charles III**. Gold truth: "Queen Elizabeth II" (correct at the time of collection, but she's dead now) + +> In geology, Greywacke is classed as what form of sedimentary rock? +> +> Model answer: **Sandstone**. Gold truth: "Deep Ocean" (incorrect — Wikipedia lists "Sandstone") + +Some questions had missing information, for example: "In which town in Greater Manchester is the TV series set?" (no TV series name — maybe the question was supposed to have a picture?) or "Measuring from the closest point of each of these countries to the equator, which is the furthest from the equator?" (no list of countries). + +There are only 17 / 500 questions where Opus 4.8 was cleanly and clearly wrong — 3.4%. Verdict: the benchmark is basically saturated; the actual realistic ceiling is ≈92% (0.89 + 0.034). + +## Contamination + +Contaminated by design. It originally contains scraped trivia questions: "First we gathered question-answer pairs from 14 trivia and quiz-league websites". Even if the canonical version of the dataset is filtered out of the training data, these trivia question sets are unlikely to be removed. + +From "A Comprehensive Survey of Contamination Detection Methods in Large Language Models": + +> [...] the Llama pre-training corpus and The Pile (Gao et al., 2020) are found to be contaminated for many popular evaluation benchmarks, notably BigBench, HellaSwag, PiQA, MMLU and TriviaQA + +From "Generalization v.s. Memorization: Tracing Language Models' Capabilities Back to Pretraining Data": + +> Among the four tasks [TriviaQA, MMLU, WMT, and GSM8K], our analysis reveals that TriviaQA exhibits the strongest memorization effect, with task performance highly correlated to the n-gram distributions in the pretraining data. + +Additionally, ["Question and Answer Test-Train Overlap in Open-Domain Question Answering Datasets"](https://arxiv.org/abs/2008.02637) highlights significant test–train overlap. + +## ECI vs TriviaQA and compute vs TriviaQA + +TriviaQA seems to display a mild inverted-U relationship between compute (or ECI) and score on this benchmark. See the charts below — the shape of the data starts to bend downwards. The best models on this task aren't the ones with the highest compute budget or the most capable. + + + +Compare the TriviaQA dashboard to three randomly picked benchmarks: MATH Level 5, HellaSwag, BBH. + + + +### Hypotheses for the inverted U-shape + +1. **Saturation.** Measurements near the ceiling are noisy — they don't allow us to separate models by skill from each other. +2. **Factual knowledge is not a good proxy for general capabilities in models.** Models have a parameter budget, and you can "spend" this budget either to "buy" yourself better factual knowledge or general capabilities. Labs prefer the latter. +3. **There aren't a lot of measurements in general:** only 29 if you don't count duplicates (n-shot for different n).