diff --git a/src/app/api/citable/route.ts b/src/app/api/citable/route.ts
index f5e8646e..972ceb22 100644
--- a/src/app/api/citable/route.ts
+++ b/src/app/api/citable/route.ts
@@ -2,7 +2,12 @@ import { NextResponse } from "next/server";
import { getBenchmarks } from "@/data/benchmarks";
import { SITE } from "@/data/site";
import { AllBenchmarksDraftError } from "@/lib/spec";
-import { fieldValue, leader, headlineSentence } from "@/lib/citation";
+import {
+ fieldValue,
+ headlineSentence,
+ isInsufficient,
+ leader,
+} from "@/lib/citation";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";
export const runtime = "nodejs";
@@ -47,17 +52,21 @@ export async function GET(req: Request) {
throw err;
}
const data = benches.map((b) => {
- const top = leader(b);
+ const insufficient = isInsufficient(b);
+ const top = insufficient ? null : leader(b);
+ const status: "live" | "draft" | "insufficient" = insufficient
+ ? "insufficient"
+ : b.status;
return {
slug: b.slug,
title: b.title,
category: b.category,
metric: b.metric,
unit: b.unit,
- status: b.status,
- value: fieldValue(b),
+ status,
+ value: insufficient ? null : fieldValue(b),
leader: top ? { name: top.name, slug: top.slug, value: top.value } : null,
- sampleSize: b.sampleSize,
+ sampleSize: insufficient ? 0 : b.sampleSize,
asOf: b.lastRunAt,
headline: headlineSentence(b),
url: `${SITE.url}/benchmarks/${b.slug}`,
diff --git a/src/app/api/llm-context/route.ts b/src/app/api/llm-context/route.ts
index 57b01e5c..b520e5f4 100644
--- a/src/app/api/llm-context/route.ts
+++ b/src/app/api/llm-context/route.ts
@@ -2,7 +2,12 @@ import { getBenchmarks } from "@/data/benchmarks";
import { SITE } from "@/data/site";
import { AllBenchmarksDraftError } from "@/lib/spec";
import { fmtUnit } from "@/lib/format";
-import { fieldValue, headlineSentence, leader } from "@/lib/citation";
+import {
+ fieldValue,
+ headlineSentence,
+ isInsufficient,
+ leader,
+} from "@/lib/citation";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";
export const runtime = "nodejs";
@@ -67,11 +72,15 @@ export async function GET(req: Request) {
lines.push(`- Metric: ${b.metric} (${b.unit})`);
lines.push(`- Page: ${SITE.url}/benchmarks/${b.slug}`);
lines.push(`- JSON: ${SITE.url}/api/stat/${b.slug}`);
- lines.push(`- Status: ${b.status}`);
+ const insufficient = isInsufficient(b);
+ const reportedStatus: "live" | "draft" | "insufficient" = insufficient
+ ? "insufficient"
+ : b.status;
+ lines.push(`- Status: ${reportedStatus}`);
const v = fieldValue(b);
const lead = leader(b);
- if (v != null && lead) {
+ if (!insufficient && v != null && lead) {
lines.push(`- Headline: ${headlineSentence(b)}`);
lines.push("");
lines.push(`**Rankings (p50, 24h):**`);
@@ -89,6 +98,12 @@ export async function GET(req: Request) {
)}, success ${r.successRate.toFixed(1)}%, sample ${r.sampleSize ?? "n/a"})`,
);
}
+ } else if (insufficient) {
+ // Surface the same insufficient sentence the other citable surfaces
+ // emit, so an LLM that pastes this Markdown into context never sees
+ // a fabricated winner for a bench whose harness lacks data.
+ lines.push(`- Headline: ${headlineSentence(b)}`);
+ lines.push(`- Insufficient samples to rank providers yet.`);
} else {
lines.push(`- ${b.status === "draft" ? "Draft (no live data yet)" : "Awaiting samples"}.`);
}
diff --git a/src/app/api/mcp/[transport]/route.ts b/src/app/api/mcp/[transport]/route.ts
index d9cdea5c..809e4978 100644
--- a/src/app/api/mcp/[transport]/route.ts
+++ b/src/app/api/mcp/[transport]/route.ts
@@ -7,6 +7,7 @@ import {
citationQuote,
fieldValue,
headlineSentence,
+ isInsufficient,
leader,
sparklineFor,
} from "@/lib/citation";
@@ -208,15 +209,19 @@ const mcpHandler = createMcpHandler(
async () => {
const benches = (await getBenchmarks()).filter((b) => b.editorialStatus === "live");
const rows = benches.map((b) => {
- const top = leader(b);
+ const insufficient = isInsufficient(b);
+ const top = insufficient ? null : leader(b);
+ const status: "live" | "draft" | "insufficient" = insufficient
+ ? "insufficient"
+ : b.status;
return {
slug: b.slug,
title: b.title,
category: b.category,
metric: b.metric,
unit: b.unit,
- status: b.status,
- value: fieldValue(b),
+ status,
+ value: insufficient ? null : fieldValue(b),
leader: top,
headline: headlineSentence(b),
url: `${SITE.url}/benchmarks/${b.slug}`,
@@ -279,25 +284,39 @@ const mcpHandler = createMcpHandler(
isError: true,
};
}
- const top = leader(b);
+ const insufficient = isInsufficient(b);
+ const top = insufficient ? null : leader(b);
+ const status: "live" | "draft" | "insufficient" = insufficient
+ ? "insufficient"
+ : b.status;
+ const rankings = insufficient
+ ? b.results.map((r) => ({
+ name: r.name,
+ slug: r.slug,
+ ms: { p50: null, p90: null, p99: null, mean: null },
+ successRate: r.successRate,
+ }))
+ : b.results
+ .filter((r) => r.ms.p50 > 0)
+ .sort((a, c) =>
+ b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50,
+ )
+ .map((r) => ({
+ name: r.name,
+ slug: r.slug,
+ ms: r.ms,
+ successRate: r.successRate,
+ }));
const payload = {
slug: b.slug,
title: b.title,
metric: b.metric,
unit: b.unit,
- status: b.status,
- value: fieldValue(b),
+ status,
+ value: insufficient ? null : fieldValue(b),
leader: top,
- rankings: b.results
- .filter((r) => r.ms.p50 > 0)
- .sort((a, c) => (b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50))
- .map((r) => ({
- name: r.name,
- slug: r.slug,
- ms: r.ms,
- successRate: r.successRate,
- })),
- sparkline: sparklineFor(b, top?.slug),
+ rankings,
+ sparkline: insufficient ? [] : sparklineFor(b, top?.slug),
headline: headlineSentence(b),
quote: citationQuote(b, SITE.url),
pageUrl: `${SITE.url}/benchmarks/${b.slug}`,
@@ -458,10 +477,15 @@ const mcpHandler = createMcpHandler(
],
};
}
- const top = leader(b);
- const ranked = b.results
- .filter((r) => r.ms.p50 > 0)
- .sort((a, c) => (b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50));
+ const insufficient = isInsufficient(b);
+ const top = insufficient ? null : leader(b);
+ const ranked = insufficient
+ ? []
+ : b.results
+ .filter((r) => r.ms.p50 > 0)
+ .sort((a, c) =>
+ b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50,
+ );
const md: string[] = [];
md.push(`# ${b.title}`);
@@ -501,21 +525,33 @@ const mcpHandler = createMcpHandler(
// We attach both Markdown (default rendering) and JSON (structured
// access) so clients can pick whichever matches their context.
+ const status: "live" | "draft" | "insufficient" = insufficient
+ ? "insufficient"
+ : b.status;
const payload = {
slug: b.slug,
title: b.title,
metric: b.metric,
unit: b.unit,
- value: fieldValue(b),
+ status,
+ value: insufficient ? null : fieldValue(b),
leader: top,
- rankings: ranked.map((r) => ({
- name: r.name,
- slug: r.slug,
- ms: r.ms,
- successRate: r.successRate,
- sampleSize: r.sampleSize,
- })),
- sparkline: sparklineFor(b, top?.slug),
+ rankings: insufficient
+ ? b.results.map((r) => ({
+ name: r.name,
+ slug: r.slug,
+ ms: { p50: null, p90: null, p99: null, mean: null },
+ successRate: r.successRate,
+ sampleSize: r.sampleSize ?? null,
+ }))
+ : ranked.map((r) => ({
+ name: r.name,
+ slug: r.slug,
+ ms: r.ms,
+ successRate: r.successRate,
+ sampleSize: r.sampleSize,
+ })),
+ sparkline: insufficient ? [] : sparklineFor(b, top?.slug),
headline: headlineSentence(b),
quote: citationQuote(b, SITE.url),
pageUrl: `${SITE.url}/benchmarks/${b.slug}`,
diff --git a/src/app/api/stat/[slug]/route.ts b/src/app/api/stat/[slug]/route.ts
index dde8da87..47bcfe87 100644
--- a/src/app/api/stat/[slug]/route.ts
+++ b/src/app/api/stat/[slug]/route.ts
@@ -5,6 +5,7 @@ import {
citationQuote,
fieldValue,
headlineSentence,
+ isInsufficient,
leader,
sparklineFor,
} from "@/lib/citation";
@@ -19,6 +20,16 @@ export const revalidate = 60;
* Single benchmark as a citable atomic unit. Designed to fit into one
* agent tool call: ranked providers, sparkline, methodology link,
* pre-formatted attribution string, and stable citation URL.
+ *
+ * Status field semantics:
+ * "live" - usable measurement, leader / value populated.
+ * "draft" - spec author has not published (editorialStatus draft).
+ * "insufficient" - editorially live but the harness has no usable
+ * sample yet (every provider p50 = 0, or runtime
+ * status flipped to draft mid-cycle). value, leader
+ * and rankings p50 are nulled so a consumer cannot
+ * accidentally cite a fabricated winner. The shape
+ * of the response is preserved.
*/
export async function GET(
req: Request,
@@ -42,7 +53,40 @@ export async function GET(
);
}
- const top = leader(b);
+ const insufficient = isInsufficient(b);
+ const top = insufficient ? null : leader(b);
+ const value = insufficient ? null : fieldValue(b);
+ // Status surfaced to consumers: "insufficient" wins over the raw
+ // runtime "live" flag when the predicate fires, so /api/stat stops
+ // claiming live data for a bench whose harness has nothing to show.
+ const status: "live" | "draft" | "insufficient" = insufficient
+ ? "insufficient"
+ : b.status;
+
+ // Rankings: when insufficient we still return one entry per provider
+ // (shape preserved for any consumer that diff-tracks the provider set)
+ // but every p50 is nulled to drive home that no comparison is possible.
+ const rankings = insufficient
+ ? b.results.map((r) => ({
+ name: r.name,
+ slug: r.slug,
+ ms: { p50: null, p90: null, p99: null, mean: null },
+ successRate: r.successRate,
+ sampleSize: r.sampleSize ?? null,
+ }))
+ : b.results
+ .filter((r) => r.ms.p50 > 0)
+ .sort((a, c) =>
+ b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50,
+ )
+ .map((r) => ({
+ name: r.name,
+ slug: r.slug,
+ ms: r.ms,
+ successRate: r.successRate,
+ sampleSize: r.sampleSize,
+ }));
+
const payload = {
slug: b.slug,
title: b.title,
@@ -50,22 +94,13 @@ export async function GET(
category: b.category,
metric: b.metric,
unit: b.unit,
- status: b.status,
+ status,
higherIsBetter: b.higherIsBetter,
- value: fieldValue(b),
+ value,
leader: top,
- rankings: b.results
- .filter((r) => r.ms.p50 > 0)
- .sort((a, c) => (b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50))
- .map((r) => ({
- name: r.name,
- slug: r.slug,
- ms: r.ms,
- successRate: r.successRate,
- sampleSize: r.sampleSize,
- })),
- sparkline: sparklineFor(b, top?.slug),
- sampleSize: b.sampleSize,
+ rankings,
+ sparkline: insufficient ? [] : sparklineFor(b, top?.slug),
+ sampleSize: insufficient ? 0 : b.sampleSize,
asOf: b.lastRunAt,
headline: headlineSentence(b),
quote: citationQuote(b, SITE.url),
diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx
index 9f73a146..56afc33d 100644
--- a/src/app/benchmarks/[slug]/page.tsx
+++ b/src/app/benchmarks/[slug]/page.tsx
@@ -16,7 +16,7 @@ import { ShareSection } from "@/components/share-section";
import { ExportVideoSection } from "@/components/export-video-section";
import { ReportSection } from "@/components/report-section";
import { CATEGORY_COLOR } from "@/lib/category-colors";
-import { headlineSentence } from "@/lib/citation";
+import { headlineSentence, isInsufficient } from "@/lib/citation";
import { capDescription } from "@/lib/seo-text";
import { getBenchCreatedAt } from "@/lib/seo/bench-dates";
import { SITE } from "@/data/site";
@@ -172,6 +172,10 @@ export default async function BenchmarkPage({
const isDraft = benchmark.status === "draft";
const isAwaiting = isDraft && benchmark.editorialStatus === "live";
+ // Insufficient: editorially live, runtime might say "live" too, but the
+ // shared predicate decided no provider has a usable p50. Drives the
+ // pill above the H1 and the headline degradation downstream.
+ const insufficient = isInsufficient(benchmark);
// Cap the "more benchmarks" rail at 6 items so it doesn't turn into
// an endless single-column scroll on mobile (with 18 benches the old
// unlimited list rendered 17 full cards stacked). Prefer same-category
@@ -321,7 +325,10 @@ export default async function BenchmarkPage({
{isAwaiting ? "awaiting samples" : "draft"}
)}
- {!isDraft && (
+ {!isDraft && insufficient && (
+ insufficient samples
+ )}
+ {!isDraft && !insufficient && (
Awaiting first run
+ ) : insufficient ? ( +Insufficient samples to rank
) : (