From 89e58e399ce19e6ecb849f95a35dc8ada46a1a5e Mon Sep 17 00:00:00 2001 From: Flotapponnier Date: Sat, 19 Sep 2026 15:57:17 +0200 Subject: [PATCH] products: the provider index keeps only appearances on benches that exist in this deployment (the blob is built from dev; on prod every product page linked 3 to 7 dev-only bench and compare URLs that 404) The worker builds providers.json from the `dev` checkout, so on production the index credited appearances on benches whose spec is not on `main` (terminal-fill-quality, trading-app-daily-volume, bridge-execution-latency, bridge-realized-cost, bridge-quote-latency- solana, ...). /products/gmgn linked 4 dead URLs, /products/mobula 3, and the pairs that exist only through those benches (banana-gun-vs-gmgn, bonkbot-vs-gmgn) 404 as well. Search Console lists 271 404s; the audit of 2026-09-19 (products scope) flagged 14 of 16 sampled product pages. getProviders() now confines the index to this deployment's spec set (loadSpecsUncached, minus DEV_ONLY_BENCH_SLUGS and REMOVED_BENCH_SLUGS): appearances on absent benches are dropped, wins and chainWins are reduced by what those appearances contributed (same rule as buildProvidersFromBenches: per chain led when the bench has chain dimensions, else the aggregate #1), categories recomputed, and a profile with nothing left disappears (so its compare pairs are not linked). On dev every bench exists: no-op. Falls open if the spec directory cannot be read. Verified on a local render of this branch against the live blob: /products/gmgn lists 7 benches (was 9, the 2 dev-only ones gone) and 12 compare links, none to banana-gun or bonkbot; /products/mobula 8 benches, the 3 bridge dev-only ones gone. tsc and eslint clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HJgbZCqjR4nvCfcJSzofbw --- src/lib/providers.ts | 64 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/src/lib/providers.ts b/src/lib/providers.ts index adde99606..460fd8231 100644 --- a/src/lib/providers.ts +++ b/src/lib/providers.ts @@ -12,6 +12,8 @@ import { cache } from "react"; import { unstable_cache } from "next/cache"; import { getBenchmarksSafe } from "@/data/benchmarks"; import { loadProvidersFromBlob } from "@/lib/bench-blob"; +import { loadSpecsUncached } from "@/lib/materialize/load"; +import { DEV_ONLY_BENCH_SLUGS, REMOVED_BENCH_SLUGS } from "@/lib/removed-benches"; import { liveResults } from "@/lib/provider-filters"; import { citationCandidates } from "@/lib/citation"; import { readBestPerChain } from "@/lib/per-chain-contract"; @@ -635,8 +637,9 @@ async function loadProvidersMemoized(): Promise { } const value = (async () => { const fromBlob = await loadProvidersFromBlob().catch(() => null); - if (fromBlob && fromBlob.length > 0) return fromBlob; - return buildProvidersCached(); + const profiles = + fromBlob && fromBlob.length > 0 ? fromBlob : await buildProvidersCached(); + return confineToThisDeploy(profiles); })(); providersMemo = { at: now, value }; value.catch(() => { @@ -645,6 +648,63 @@ async function loadProvidersMemoized(): Promise { return value; } +/** The providers blob is built by the worker from the `dev` checkout, so + * on production it credits appearances on benches whose spec is not on + * `main` (dev-only benches: the page 404s there). Every product page + * then linked 3 to 7 dead bench and compare URLs (Search Console 2026-09: + * 271 404s; audit 2026-09-19). Keep only appearances whose bench exists + * in this deployment's spec set (and is not held out or removed), recount + * wins from what is left, and drop a profile that has nothing left. On + * dev every bench exists and this is a no-op. Falls open (no filtering) + * when the spec directory cannot be read. */ +async function confineToThisDeploy( + profiles: ProviderProfile[], +): Promise { + const specs = await loadSpecsUncached().catch(() => []); + if (specs.length === 0) return profiles; + const here = new Set(specs.map((s) => s.slug)); + const keep = (slug: string) => + here.has(slug) && !DEV_ONLY_BENCH_SLUGS.has(slug) && !REMOVED_BENCH_SLUGS.has(slug); + const out: ProviderProfile[] = []; + for (const p of profiles) { + if (p.appearances.every((a) => keep(a.benchmark.slug))) { + out.push(p); + continue; + } + const appearances = p.appearances.filter((a) => keep(a.benchmark.slug)); + if (appearances.length === 0) continue; + const dropped = p.appearances.filter((a) => !keep(a.benchmark.slug)); + // Wins: 1 per aggregate #1 without chain dimensions, 1 per chain led + // with them (the rule in buildProvidersFromBenches). Subtract what the + // dropped appearances contributed rather than recomputing from scratch. + let lostWins = 0; + const chainWins = { ...(p.chainWins ?? {}) }; + for (const a of dropped) { + const hasChainDims = + (a.benchmark.chainDimensions ?? []).some((c) => c.value !== "all"); + if (hasChainDims) { + const led = Object.entries(a.rankPerChain ?? {}).filter(([, v]) => v.rank === 1); + lostWins += led.length; + for (const [chain] of led) { + if (chainWins[chain]) chainWins[chain] -= 1; + if (!chainWins[chain]) delete chainWins[chain]; + } + } else if (a.rank === 1) { + lostWins += 1; + } + } + const categories = [...new Set(appearances.map((a) => a.benchmark.category))]; + out.push({ + ...p, + appearances, + wins: Math.max(0, p.wins - lostWins), + categories, + chainWins: Object.keys(chainWins).length > 0 ? chainWins : undefined, + }); + } + return out; +} + export const getProviders = cache(loadProvidersMemoized); export async function getProviderSlugs(): Promise {