From f6f84b8ebaeb3a315d11d24ca072272af3b30d82 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Mon, 13 Jul 2026 22:01:48 +0200 Subject: [PATCH 1/2] fix: 3 machine-readable surface bugs surfaced by GEO audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. /api/citable/{date} unit scaling. Snapshot route emitted `value: 627` for `aggregator-head-lag` while the live /api/citable emitted `value: 0.63` on the same measurement. The live route already wraps with valueInDeclaredUnit(raw, b.unit) but the snapshot route was left with the raw internal-ms value. Wrap the same way; sub-1 s benches now render in the declared `s` unit on both routes. 2. /api/stat/{slug} silently ignored ?chain= / ?region= / ?kind= / ?venue=. getBenchmark accepted a filters object but the route never parsed the URL. A citer asking /api/stat/rpc-capabilities?chain=ethereum got the cross-chain aggregate. Parse the four dimension params, pass through to the loader; unknown values fall back to unfiltered inside the loader. Also widen getBenchmark options type to include kind and venue — the underlying loadBenchmark already supports both. 3. Draft benches leaked a fake freshness signal. Draft placeholder set lastRunAt to new Date for type safety (Benchmark.lastRunAt is non-null), and both citable routes passed it straight through as asOf. LLM crawlers treating asOf as ground truth would think a draft bench was measured every minute. Null asOf in the JSON when status is draft; the internal type stays intact. --- src/app/api/citable/[date]/route.ts | 21 ++++++++++++++++++--- src/app/api/citable/route.ts | 6 +++++- src/app/api/stat/[slug]/route.ts | 23 ++++++++++++++++++++++- src/data/benchmarks.ts | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/app/api/citable/[date]/route.ts b/src/app/api/citable/[date]/route.ts index debd0be0..3c4f5874 100644 --- a/src/app/api/citable/[date]/route.ts +++ b/src/app/api/citable/[date]/route.ts @@ -3,6 +3,7 @@ import { getBenchmarks } from "@/data/benchmarks"; import { SITE } from "@/data/site"; import { AllBenchmarksDraftError } from "@/lib/spec"; import { citeBundle, fieldValue, leader, headlineSentence } from "@/lib/citation"; +import { valueInDeclaredUnit } from "@/lib/format"; import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit"; export const runtime = "nodejs"; @@ -111,6 +112,13 @@ export async function GET( const data = benches.map((b) => { const top = leader(b); const insufficient = b.dataConfidence === "insufficient"; + // `value` and `leader.value` are published in the declared `unit`. + // Latency benches with unit "s" store ms internally (fmtUnit + // convention); valueInDeclaredUnit converts so a snapshot never + // claims 627 seconds for a 627 ms head lag. Same conversion as + // the live /api/citable route; missing here caused sub-1 unit + // values to leak through as their internal ms representation. + const raw = insufficient ? null : fieldValue(b); return { slug: b.slug, title: b.title, @@ -118,17 +126,24 @@ export async function GET( metric: b.metric, unit: b.unit, status: b.status, - value: insufficient ? null : fieldValue(b), + value: raw == null ? null : valueInDeclaredUnit(raw, b.unit), leader: insufficient ? null : top - ? { name: top.name, slug: top.slug, value: top.value } + ? { + name: top.name, + slug: top.slug, + value: valueInDeclaredUnit(top.value, b.unit), + } : null, sampleSize: b.sampleSize, expectedN: b.expectedN, dataConfidence: b.dataConfidence, - asOf: b.lastRunAt, + // Same rule as the live /api/citable route: draft benches carry + // a wall-clock `lastRunAt` for type safety, not a real + // measurement, so null it here rather than spoofing freshness. + asOf: b.status === "draft" ? null : b.lastRunAt, headline: headlineSentence(b), url: `${SITE.url}/benchmarks/${b.slug}`, api: `${SITE.url}/api/stat/${b.slug}`, diff --git a/src/app/api/citable/route.ts b/src/app/api/citable/route.ts index 1440ab17..431b7c58 100644 --- a/src/app/api/citable/route.ts +++ b/src/app/api/citable/route.ts @@ -80,7 +80,11 @@ export async function GET(req: Request) { sampleSize: b.sampleSize, expectedN: b.expectedN, dataConfidence: b.dataConfidence, - asOf: b.lastRunAt, + // Draft benches have no measurement history; the loader stamps + // `lastRunAt` with the current wall clock so downstream code always + // has a string. Do NOT leak that as a real measurement timestamp + // here — LLM crawlers key on `asOf` as ground truth for freshness. + asOf: b.status === "draft" ? null : b.lastRunAt, headline: headlineSentence(b), url: `${SITE.url}/benchmarks/${b.slug}`, api: `${SITE.url}/api/stat/${b.slug}`, diff --git a/src/app/api/stat/[slug]/route.ts b/src/app/api/stat/[slug]/route.ts index 973f1e16..a1eb59eb 100644 --- a/src/app/api/stat/[slug]/route.ts +++ b/src/app/api/stat/[slug]/route.ts @@ -33,7 +33,28 @@ export async function GET( if (!SLUG_RE.test(slug)) { return NextResponse.json({ error: "bad_slug" }, { status: 400 }); } - const b = await getBenchmark(slug); + // Dimension query params (?chain=, ?region=, ?kind=, ?venue=) mirror + // the same client-side selector on the bench page, so a citer asking + // "fastest ethereum us-east RPC" gets the per cell leader instead of + // the cross chain aggregate. Values are pass-through; the loader + // validates each against the bench's declared dimensions and falls + // back to unfiltered when a value is unknown. + const url = new URL(req.url); + const filters: { + chain?: string; + region?: string; + kind?: string; + venue?: string; + } = {}; + const chainParam = url.searchParams.get("chain"); + const regionParam = url.searchParams.get("region"); + const kindParam = url.searchParams.get("kind"); + const venueParam = url.searchParams.get("venue"); + if (chainParam && chainParam !== "all") filters.chain = chainParam; + if (regionParam && regionParam !== "all") filters.region = regionParam; + if (kindParam && kindParam !== "all") filters.kind = kindParam; + if (venueParam && venueParam !== "all") filters.venue = venueParam; + const b = await getBenchmark(slug, filters); if (!b || b.editorialStatus !== "live") { return NextResponse.json( { error: "unknown_slug", slug }, diff --git a/src/data/benchmarks.ts b/src/data/benchmarks.ts index 0808aaf5..96cbb6e5 100644 --- a/src/data/benchmarks.ts +++ b/src/data/benchmarks.ts @@ -115,7 +115,7 @@ export const getBenchmarksSafe = cache(loadAllBenchmarksSafe); export async function getBenchmark( slug: string, - options: { chain?: string; region?: string } = {} + options: { chain?: string; region?: string; kind?: string; venue?: string } = {} ): Promise { return loadBenchmark(slug, options); } From 8fceb75dcb18b8c8db06c0be4c3507664d28c50f Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Mon, 13 Jul 2026 22:11:58 +0200 Subject: [PATCH 2/2] fix: propagate draft-asOf null-out to /api/stat, MCP, JSON-LD dateModified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GEO audit review found the initial fix in commit f6f84b8 only covered the two /api/citable JSON surfaces, but the same "draft benches spoof freshness" class of bug lives on three more machine-readable channels that LLM crawlers also key on: - /api/stat/{slug} asOf field. - MCP tool responses (list_benchmarks, get_benchmark, resource template, and the plain-text "Last sample" line). - JSON-LD dateModified on the bench page, per-chain sub-page, answer page and alternatives page — Google, Bing, Perplexity all consume dateModified as a freshness ranking signal. Centralize the guard as citableAsOf(b) in @/lib/citation, rewire every call site. Draft benches now uniformly omit or null the freshness timestamp; live benches unchanged. On the visible /answers page, the "Data as of..." line and its