From 437083f98e72eb1dd7f9e14064849f179191dd49 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:33:04 +0200 Subject: [PATCH] eleven comments that described something the code does not do, and four dead exports Found by an audit of today's diff. Each of these was checked against the code beside it; nothing here changes behaviour. Cache windows that moved and left their prose behind (#2724 widened the routes, these were not resurveyed): - api/stat/[slug] said the worst case went "from about 360 s to about 600 s" and called that parity with the bench page's 600 s ISR. 360 counts stale-while-revalidate on the old header (60 + 300); the matching figure for 300 + 900 is 1200, not 600, so it is about twice the page's ISR, not parity. The comment now says so and records where it shows: /rwa is revalidate = 300 and its FAQ, which becomes FAQPage JSON-LD, tells readers this endpoint carries "the same values and timestamp" while advertising it as a DataDownload. - spec.ts said loadAllBenchmarksCached shares its result "with 60s revalidate" three lines above `revalidate: 300`. Several API routes size their own windows against this number. - benchmark-body.tsx said the variant API "dedupes across users via its 60s cache"; that cache is 300 s. - api/freshness said its edge cache is "5 s s-maxage, 20 s swr" and that the LiveIndicator polls "every 8 s". The response sets s-maxage=30, swr=60 and the widget polls every 30 s. - live-indicator.tsx said it re-fetches /api/citable, edge-cached 60 s. It fetches /api/freshness, and that is 30 s. Both now note the asymmetry this creates: at 30 s the widget is the freshest clock on the page and can read newer than the table under it. - api/aggregate said "The ISR revalidate=60 above handles freshness instead". There is no revalidate on that route; it is force-dynamic. Wrong reason, right conclusion: - next.config.ts said the sitemap sets its own Cache-Control "because metadata routes ignore next.config headers()". sitemap.xml/route.ts is a Route Handler, not a metadata route, and the same comment already gives the real reason two lines earlier. Docs separated from their fields by today's zero_is_a_value insertion, so each described its neighbour. Reordered, text verbatim: - spec-schema.ts: three docs stacked in the reverse order of the three fields, so zero_is_a_value showed ledger_reliability's doc and ledger_columns' doc was stranded two fields up. - types/benchmark.ts: dimensionLabels' doc sat on zeroIsAValue. Counts that no longer hold: - site-footer.tsx named /llms.txt and /api/citable as the links absent from the sitemap, which reads as a complete list. Four of the ten are absent: those two plus /api/openapi.json and /api/llm-context. The next person pruning that row needs all four. - capital-hub.ts and capital/page.tsx both list five benches; eight feed the hub. Both now point at CAPITAL_BENCHES instead of carrying a copy, because the copy went stale the first time the list grew. - capital-hub.ts also credited bench 265 for perp open interest while correctly naming 265 as perp-pf-ratio two lines later. 265 stopped feeding that column today; the block above perpOi explains why. - capital-hub-rules.ts said "n/a" is the missing value on "the other 37 places the site uses it". Not derivable: the literal appears 63 times in src/ across 21 files. The claim keeps its substance and drops the number rather than standing as a fact-shaped guess. - the COLUMN_COVERAGE note quotes the owner's "two rows of seventy-nine" from the first hub draft. Kept as the quotation it is, with the current cohort size (63) named beside it so the two figures stop reading as a contradiction. One reader-facing string, not a comment: the categoryTooSmall reason said a category under five ranked protocols "compares the token with one or two others". Below a floor of five it is up to three others. Dead exports removed from capital-hub-rules.ts, zero callers across src/, scripts/ and worker/: - change7dFromDays and HISTORY_FRESH_MS (its only reader). Today's perp open-interest change removed the single call site, and the doc still named the data path the hub stopped reading. - change7dFromSeries, a one-line wrapper over change7dOfSeries. Its test covers change7dOfSeries' real behaviour, so the test was retargeted at that function rather than deleted: every assertion is kept. - the NaReason type, one reference in the tree and it was its own declaration. Using it instead of deleting it was the alternative, and it is a refactor rather than a cleanup: CCTP_SCOPE_LABEL values are also assigned to the notApplicable fields and are not in NA_REASON, so tightening those to NaReason needs a decision about the union first. typecheck, lint, validate (241 specs) and the suite (367 tests) clean. Deliberately not in this sweep: the warm-up and smoke comments in prod-deploy.yml, staging-deploy.yml and scripts/sitemap-smoke.mjs, which belong to whoever is holding that change; page-metadata.ts's "fallback" comment, which is only wrong because of a behaviour bug fixed separately; and benchmarks/rpc-cost.yml's "one plan in seven", which is page copy whose denominator two passes disagreed on. --- next.config.ts | 11 ++++--- src/app/api/aggregate/route.ts | 6 ++-- src/app/api/freshness/route.ts | 13 ++++---- src/app/api/stat/[slug]/route.ts | 19 +++++++++--- src/app/capital/page.tsx | 7 +++-- src/components/benchmark-body.tsx | 2 +- src/components/live-indicator.tsx | 10 +++++-- src/components/site-footer.tsx | 12 ++++++-- src/lib/capital-hub-rules.test.ts | 31 +++++-------------- src/lib/capital-hub-rules.ts | 49 ++++++------------------------- src/lib/capital-hub.ts | 18 ++++++++---- src/lib/spec-schema.ts | 26 ++++++++-------- src/lib/spec.ts | 4 ++- src/types/benchmark.ts | 4 +-- 14 files changed, 103 insertions(+), 109 deletions(-) diff --git a/next.config.ts b/next.config.ts index 79e482667..6922f3750 100644 --- a/next.config.ts +++ b/next.config.ts @@ -192,10 +192,13 @@ const nextConfig: NextConfig = { // 2026-09-30, the edge entry for /api/citable went STALE at age // 306 and reset to 42, then went STALE again at 313: two clean // cycles, so the served window is that route's 300 s and not a - // one-off eviction. Change the window in the route, not here. Sitemap's - // Cache-Control is likewise set inside its route.ts - // (`src/app/sitemap.xml/route.ts`) because metadata routes - // ignore next.config headers(). + // one-off eviction. Change the window in the route, not here. + // The sitemap sets its Cache-Control the same way and for the same + // reason: `src/app/sitemap.xml/route.ts` is a Route Handler that + // returns its own Response, so whatever it sets wins over this + // layer. (It is a Route Handler rather than a `sitemap.ts` + // metadata route precisely because a metadata route left Next in + // charge of the header; the file's own docblock has that history.) source: "/api/citable", headers: [ { diff --git a/src/app/api/aggregate/route.ts b/src/app/api/aggregate/route.ts index 6500c8b28..c8deae48e 100644 --- a/src/app/api/aggregate/route.ts +++ b/src/app/api/aggregate/route.ts @@ -42,8 +42,10 @@ export async function GET() { headers: { "Accept-Encoding": "gzip, br" }, // No cache:"no-store" here: that flag opts the route into // dynamic mode and causes Next.js to strip s-maxage from our - // response headers, breaking Vercel CDN caching. The ISR - // revalidate=60 above handles freshness instead. + // response headers, breaking Vercel CDN caching. There is no ISR + // revalidate on this route to fall back on (it is force-dynamic, + // line 18); freshness comes from the s-maxage=300 the response + // below sets, which is what the edge honours. }); } catch (err) { lastErr = String(err); diff --git a/src/app/api/freshness/route.ts b/src/app/api/freshness/route.ts index c72e7dbb4..f2584ced9 100644 --- a/src/app/api/freshness/route.ts +++ b/src/app/api/freshness/route.ts @@ -11,11 +11,14 @@ export const runtime = "nodejs"; * (`scalar(time() - max(timestamp()))`), returning just the * resolved data timestamp per slug. * - * Separate from /api/citable so the LiveIndicator can poll fast (every - * 8 s) without re-running the heavy spec → rankings → sparkline → series - * pipeline. Edge-cache is short (5 s s-maxage, 20 s swr) so the visible - * staleness lands around 15-20 s p99 - close to the Prom scrape interval - * floor (15 s) that bounds how fresh any client-side query can ever be. + * Separate from /api/citable so the LiveIndicator can poll cheaply + * without re-running the heavy spec → rankings → sparkline → series + * pipeline. It polls every 30 s (live-indicator.tsx) against a 30 s + * s-maxage and 60 s swr, so the visible staleness lands within about a + * minute, above the Prom scrape interval floor (15 s) that bounds how + * fresh any client-side query can ever be. This is the shortest window on + * the site, which is why the widget can read newer than the table beneath + * it. */ // Cache window has to stay STRICTLY shorter than the LiveIndicator poll diff --git a/src/app/api/stat/[slug]/route.ts b/src/app/api/stat/[slug]/route.ts index 005565cc1..bef456762 100644 --- a/src/app/api/stat/[slug]/route.ts +++ b/src/app/api/stat/[slug]/route.ts @@ -183,10 +183,21 @@ export async function GET( // 300 s, not 60: getBenchmark reads through the bench data cache // (src/lib/spec.ts, revalidate 300), so four of every five fills at // a 60 s window recomputed this payload and returned the same bytes. - // The two windows do compose, so this is not free: worst-case data - // age goes from about 360 s to about 600 s. That is parity with the - // 600 s ISR on the bench page this mirrors, which is the number a - // reader comparing the two would see anyway. + // + // The two windows compose, and stale-while-revalidate is part of the + // window the edge actually serves: 60 + 300 was a worst case of + // about 360 s, 300 + 900 is about 1200 s, plus the 300 s data cache + // under it. So this is not parity with the 600 s ISR on the bench + // page; it is roughly twice it, and an earlier version of this + // comment claimed parity by counting swr on the old header and not + // on the new one. + // + // Where that shows: /rwa is revalidate = 300 and its FAQ (which + // becomes FAQPage JSON-LD) tells readers this endpoint carries "the + // same values and timestamp" as the page, while advertising it as a + // DataDownload. An agent that follows that link can get a body older + // than the stamp the page showed it. Narrow the window here, or + // soften that sentence there, before treating the two as identical. "cache-control": "public, s-maxage=300, stale-while-revalidate=900", "access-control-allow-origin": "*", }, diff --git a/src/app/capital/page.tsx b/src/app/capital/page.tsx index 636e1b1f3..66574c0f4 100644 --- a/src/app/capital/page.tsx +++ b/src/app/capital/page.tsx @@ -13,8 +13,11 @@ import { buildCitationMeta, CREATOR_PUBLISHER, DATASET_LICENSE } from "@/lib/dat * (TVL, bridged value, stablecoin flows, open interest) and how tokens are * priced against the fees their protocols earn (P/F, P/S, FDV, float, fee * trend against token move). Everything is read from the materialized - * benches 273, 275, 274, 265 and 277 plus the two daily history blobs the - * worker publishes; see src/lib/capital-hub.ts. + * benches plus the two daily history blobs the worker publishes. The bench + * list is CAPITAL_BENCHES in src/lib/capital-hub-types.ts, eight of them + * today (273, 274, 275, 265, 277, 271, 280, 281); read it there rather + * than from a copy here, because this copy went stale the first time the + * list grew. See src/lib/capital-hub.ts for how each is used. * * The page names numbers and the peers they are compared against. It does * not tell the reader what to do with them: the neutral wording is the diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index ff1d55ee8..0f83f41cb 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -492,7 +492,7 @@ export function BenchmarkBody({ // + first render), which the user otherwise eats as a long dimmed // state after clicking a tab. Warming them right after mount turns // tab flips into in-memory swaps. Staggered 400ms apart to stay - // gentle; the variant API dedupes across users via its 60s cache, + // gentle; the variant API dedupes across users via its 300s cache, // and re-runs when the user settles on a new axis value so the // cross-axis re-warms. useEffect(() => { diff --git a/src/components/live-indicator.tsx b/src/components/live-indicator.tsx index f856f97d2..31986483c 100644 --- a/src/components/live-indicator.tsx +++ b/src/components/live-indicator.tsx @@ -12,9 +12,13 @@ import { useEffect, useState } from "react"; * Every page that renders this widget has its own ISR cycle, so the SSR * `lastRunAt` can drift across pages by a few seconds (home was rendered * at one cache miss, detail at another). To present a single canonical - * value site-wide, when a `slug` is passed we re-fetch /api/citable on - * mount and replace the SSR value with the API value. /api/citable is - * edge-cached for 60 s so the network cost is amortised across viewers. + * value site-wide, when a `slug` is passed we re-fetch /api/freshness on + * mount and replace the SSR value with the API value. That route is + * edge-cached for 30 s (s-maxage=30, stale-while-revalidate=60) so the + * network cost is amortised across viewers. Note the asymmetry: at 30 s + * this widget is the freshest clock on the page, so it can read newer + * than the table under it, whose figures come from the page's own ISR + * window and from per-panel routes on 300 s. */ export function LiveIndicator({ lastRunAt, diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx index 611aaf95a..7aa571304 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -15,9 +15,15 @@ import { SITE } from "@/data/site"; * What did NOT change is the link set. None of these ten appear in the * left rail: the rail carries the editorial sections (/benchmarks, * /perps, /rpc and so on) and this carries the developer and project - * surfaces. /llms.txt and /api/citable are not in the sitemap either, so - * this footer is their only inbound link and dropping it would orphan the - * two files the whole citability effort exists to serve. + * surfaces. + * + * Before pruning this row, check the sitemap. FOUR of these ten are + * absent from it, so for each of them this footer is the only inbound + * link on the site: /llms.txt, /api/citable, /api/openapi.json and + * /api/llm-context. Dropping any one orphans a file the citability work + * exists to serve. (/partners, /badges, /team and /press are in the + * sitemap and would survive a cut here.) An earlier version of this note + * named only the first two, which read as a complete list and was not. */ const DEVELOPER_LINKS = [ { label: "Partners + embeds", href: "/partners" }, diff --git a/src/lib/capital-hub-rules.test.ts b/src/lib/capital-hub-rules.test.ts index b65115fc0..c077c5e5c 100644 --- a/src/lib/capital-hub-rules.test.ts +++ b/src/lib/capital-hub-rules.test.ts @@ -9,8 +9,6 @@ import { SIGNAL_READING, bridgedShareSubline, cctpScope, - change7dFromDays, - change7dFromSeries, categoryMedianUsable, change7dOfSeries, cohortCell, @@ -217,39 +215,24 @@ describe("divergences", () => { }); describe("7d change", () => { - test("from daily history: only once the day seven days back exists and the newest day is current", () => { - const days = [ - { day: "2026-09-18", oi: 100 }, - { day: "2026-09-19", oi: 105 }, - { day: "2026-09-25", oi: 120 }, - ]; - const now = Date.parse("2026-09-25T18:00:00Z"); - expect(change7dFromDays(days, "oi", now)).toBeCloseTo(20, 6); - // Yesterday's point is still current; a point three days old is a stalled worker. - expect(change7dFromDays(days, "oi", Date.parse("2026-09-26T23:00:00Z"))).toBeCloseTo(20, 6); - expect(change7dFromDays(days, "oi", Date.parse("2026-09-28T01:00:00Z"))).toBeNull(); - expect(change7dFromDays(days.slice(1), "oi", now)).toBeNull(); - expect(change7dFromDays([], "oi", now)).toBeNull(); - expect(change7dFromDays([{ day: "2026-09-25", oi: 120 }], "oi", now)).toBeNull(); - }); test("from a bench series: first and last finite buckets, and only when the series covers the whole window", () => { const covered = [100, null, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]; - expect(change7dFromSeries(covered)).toBeCloseTo(100, 6); + expect(change7dOfSeries(covered).value).toBeCloseTo(100, 6); // A bench that started this week: the first finite bucket sits past the first tenth of the window. const late = [null, null, null, null, null, null, 150, 160, 170, 180, 190, 200]; - expect(change7dFromSeries(late)).toBeNull(); + expect(change7dOfSeries(late).value).toBeNull(); // About 75% coverage is not seven days either. const threeQuarters = Array.from({ length: 84 }, (_, i) => (i < 21 ? null : 100 + i)); - expect(change7dFromSeries(threeQuarters)).toBeNull(); + expect(change7dOfSeries(threeQuarters).value).toBeNull(); // A step of more than 2x between two adjacent buckets is a change in what the bench // measures (Kalshi's open interest went from $71M to $1.02B in one bucket this week): no 7d figure. const step = Array.from({ length: 84 }, (_, i) => (i < 40 ? 71_000_000 + i * 10_000 : 1_010_000_000 + i * 10_000)); - expect(change7dFromSeries(step)).toBeNull(); + expect(change7dOfSeries(step).value).toBeNull(); // A large but continuous move stays: +80% over the week in small increments. const steady = Array.from({ length: 84 }, (_, i) => 100 * (1 + (0.8 * i) / 83)); - expect(change7dFromSeries(steady)).toBeCloseTo(80, 6); - expect(change7dFromSeries(undefined)).toBeNull(); - expect(change7dFromSeries([null, null])).toBeNull(); + expect(change7dOfSeries(steady).value).toBeCloseTo(80, 6); + expect(change7dOfSeries(undefined).value).toBeNull(); + expect(change7dOfSeries([null, null]).value).toBeNull(); }); test("an empty 7d cell says which condition failed, so the table never prints a bare dash for a known reason", () => { expect(change7dOfSeries(undefined).naReason).toBe(NA_REASON.oiSeriesMissing); diff --git a/src/lib/capital-hub-rules.ts b/src/lib/capital-hub-rules.ts index 902dc84ef..61399c902 100644 --- a/src/lib/capital-hub-rules.ts +++ b/src/lib/capital-hub-rules.ts @@ -82,11 +82,11 @@ export function bridgedShareSubline(sharePct: number | null): string | null { * withheld row, a bench that failed to load this render). The cell * prints "n/a" and nothing else. * - * "n/a" is the missing value here because it is the missing value on the - * other 37 places the site uses it (perp head to head, the trading-app - * charts, the RPC tables, the fee comparison), and a token cannot mean two - * things on two pages. The inapplicable case gets its own rendering instead, - * as it already does on the fee comparison page. + * "n/a" is the missing value here because it is the missing value + * everywhere else on the site (perp head to head, the trading-app charts, + * the RPC tables, the fee comparison), and a token cannot mean two things + * on two pages. The inapplicable case gets its own rendering instead, as it + * already does on the fee comparison page. * * This is the bench 208 rule applied to a table: an absent measurement must * not read as a measured absence. @@ -127,11 +127,9 @@ export const NA_REASON = { oiSeriesShort: "the 7-day window is not covered: the series starts inside it", oiSeriesFlat: "fewer than two readings in the 7-day window", oiSeriesStep: "the series steps more than 2x inside the window: a change in what the venue reports, not a 7-day move", - categoryTooSmall: "category too small for a peer median: under five ranked protocols, the ratio compares the token with one or two others", + categoryTooSmall: "category too small for a peer median: under five ranked protocols, the ratio compares the token with at most three others", } as const; -export type NaReason = (typeof NA_REASON)[keyof typeof NA_REASON]; - export function cohortCell( inCohort: boolean, ranked: number | null, @@ -315,31 +313,6 @@ export function selectDivergences(rows: T[], limi /* ------------------------------------------------------- 7d change */ -/** A daily history counts as current while its newest day is today or yesterday by the clock (UTC), not by the blob's own stamp. */ -export const HISTORY_FRESH_MS = 2 * 86_400_000; - -/** - * Percent change between the newest point and the point seven days before - * it in a daily history (the valuation blob's `oi` per perp). Null until - * the blob holds that older day, and null when the newest day is older - * than `nowMs` minus two days: no shorter window is passed off as 7d and - * a stalled worker publishes no change. - */ -export function change7dFromDays(days: { day: string; [k: string]: unknown }[], field: string, nowMs: number = Date.now()): number | null { - if (days.length === 0) return null; - const last = days[days.length - 1]; - const lastV = last[field]; - if (typeof lastV !== "number" || !Number.isFinite(lastV)) return null; - const t = Date.parse(`${last.day}T00:00:00Z`); - if (!Number.isFinite(t)) return null; - if (nowMs - t > HISTORY_FRESH_MS) return null; - const target = new Date(t - 7 * 86_400_000).toISOString().slice(0, 10); - const before = days.find((d) => d.day === target); - const beforeV = before?.[field]; - if (typeof beforeV !== "number" || !Number.isFinite(beforeV) || beforeV <= 0) return null; - return ((lastV - beforeV) / beforeV) * 100; -} - /** Largest bucket-to-bucket ratio a 7d series may carry and still read as one continuous window. */ export const SERIES_STEP_MAX = 2; @@ -381,11 +354,6 @@ export function change7dOfSeries(series: (number | null)[] | undefined): { value return { value: ((b - a) / a) * 100, naReason: null }; } -/** The number only, for callers that already know why an empty cell is empty. */ -export function change7dFromSeries(series: (number | null)[] | undefined): number | null { - return change7dOfSeries(series).value; -} - /* --------------------------------------------------- reading notes */ /** @@ -536,8 +504,9 @@ export function signalRowNote(r: { * Whether an optional column is worth a place in the table: at least half the * rows carry a value. A column filled for two rows of seventy-nine paints the * table with empty cells and says nothing (the owner's note on the first hub - * draft), so a source still filling in, or one a rate limit keeps mostly - * empty, stays hidden until it covers the cohort. + * draft, whose chain cohort was seventy-nine rows; it is 63 today), so a + * source still filling in, or one a rate limit keeps mostly empty, stays + * hidden until it covers the cohort. * * Every optional column of the chains table goes through this, on the value * that would actually print (`printedValue`), so a column like net USDC over diff --git a/src/lib/capital-hub.ts b/src/lib/capital-hub.ts index 8c4d05a6b..10311bf6b 100644 --- a/src/lib/capital-hub.ts +++ b/src/lib/capital-hub.ts @@ -5,17 +5,25 @@ * bench pages render) plus the two daily history blobs the worker writes * (src/lib/capital-history.ts). No Prometheus call at render time. * - * Two cohorts: + * Two cohorts (the full bench list is CAPITAL_BENCHES in + * src/lib/capital-hub-types.ts, eight entries): * - follow the capital: chains, from bench 273 chain-bridged-tvl (L2Beat * value secured), bench 275 chain-stablecoin-flow (DeFiLlama stablecoin - * float per chain) and the chains history blob (TVL, native mcap, DEX - * volume, chain fees and revenue when the chain-kpis harness publishes - * them); open interest from bench 265 (perp DEX OI) and bench 277 - * (prediction markets). + * float per chain), bench 280 chain-fees-revenue, bench 281 + * usdc-corridor-flows (net USDC over CCTP) and the chains history blob + * (TVL, native mcap, DEX volume, chain fees and revenue when the + * chain-kpis harness publishes them); open interest from the perp venue + * cohort gauges with turnover from bench 271 perp-volume-oi-ratio, and + * from bench 277 pm-open-interest for prediction markets. * - valuation: bench 274 protocol-pf-ratio (cross-DeFi P/F, fees MoM, * token 30d, category median) and bench 265 perp-pf-ratio (P/F, P/S, * FDV, float, OI, fees and revenue). * + * Perp open interest is NOT bench 265's `oi` panel. It was until + * 2026-09-30; the block above perpOi below says why it moved and what + * disagreed. Bench 265 is named once here, as perp-pf-ratio, and that is + * the only thing it feeds. + * * Every field is independently nullable, and an empty cell says which kind * of empty it is: a column that does not apply to the row carries the * reason why (`notApplicable`, from the cohort flags the benches publish), diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index a2bb57c97..8e90e6dfe 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -722,19 +722,6 @@ export const SpecSchema = z .max(17) .optional(), - /** - * Optional relabeling of the ledger's aggregate columns. For benches - * whose unit has no percentile semantics (USD revenue leaderboards), - * the p50/p90/p99/mean slots are repurposed; declaring ledger_columns - * renders each column with an honest label and unit instead of the - * default latency headers. `slot` reads the provider's headline slot, - * `panel` reads the values of a metric_panels entry by id. The first - * column is the headline (sort key, data bar, mobile column). - */ - /** false when the providers' `success` query is a publication gate - * (0 or 1) rather than a probe success rate: the ledger then hides - * the Success and Reliability columns and describes an unpublished - * row as under its threshold, not as a dead endpoint. */ /** * Whether 0 is a real value on this bench rather than a missing one. * @@ -747,7 +734,20 @@ export const SpecSchema = z * Defaults false, so every existing bench keeps the behaviour it has. */ zero_is_a_value: z.boolean().default(false), + /** false when the providers' `success` query is a publication gate + * (0 or 1) rather than a probe success rate: the ledger then hides + * the Success and Reliability columns and describes an unpublished + * row as under its threshold, not as a dead endpoint. */ ledger_reliability: z.boolean().default(true), + /** + * Optional relabeling of the ledger's aggregate columns. For benches + * whose unit has no percentile semantics (USD revenue leaderboards), + * the p50/p90/p99/mean slots are repurposed; declaring ledger_columns + * renders each column with an honest label and unit instead of the + * default latency headers. `slot` reads the provider's headline slot, + * `panel` reads the values of a metric_panels entry by id. The first + * column is the headline (sort key, data bar, mobile column). + */ ledger_columns: z .array( z diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 33899d077..291209776 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -619,7 +619,9 @@ export async function aggregateBenchmarks( // Promise.allSettled so a single bench's transient throw doesn't bring // down the whole list. See aggregateBenchmarks for the actual logic. // This wrapper layers unstable_cache on top so the result is shared -// across requests with 60s revalidate. +// across requests with 300s revalidate (the option is on the call below; +// several API routes size their own edge windows against this number, so +// the two have to be read together). const loadAllBenchmarksCached = unstable_cache( async (): Promise => { const specs = await loadSpecs(); diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index 5e3b75149..064d655f9 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -298,11 +298,11 @@ export type Benchmark = { * injecting a PromQL label; the first value is the headline cohort. */ tier?: { value: string; label: string }[]; }; - /** Per-bench axis labels for the dimension selectors, from the spec's - * `dimension_labels`. Absent keys keep the UI's stock label. */ /** True when 0 is a real value on this bench (a free tier costing * nothing) rather than a missing one (a dead latency probe). */ zeroIsAValue?: boolean; + /** Per-bench axis labels for the dimension selectors, from the spec's + * `dimension_labels`. Absent keys keep the UI's stock label. */ dimensionLabels?: { chain?: string; region?: string;