Skip to content

fix(build): break circular @/lib/chains → @/lib/spec causing prod TDZ - #712

Closed
Flotapponnier wants to merge 85 commits into
devfrom
fix/circular-chains-spec
Closed

fix(build): break circular @/lib/chains → @/lib/spec causing prod TDZ#712
Flotapponnier wants to merge 85 commits into
devfrom
fix/circular-chains-spec

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

PR #708 introduced spec.ts → chains.ts → data/benchmarks → spec.ts cycle, hitting Cannot access 'ec' before initialization on /alternatives OG image and blocking every Vercel prod deploy since. Extract the slug-alias helpers to a standalone module with no deps.

Flotapponnier and others added 30 commits June 18, 2026 19:11
rss: ship /rss.xml feed for benchmark releases
* feat(backlinks): badge embed snippet endpoint + /partners page (#541)

* feat(footer): surface /partners in Developers column (#542)

* feat(backlinks): badge embed snippet endpoint + /partners page

* feat(footer): surface /partners in Developers column
…ee (#593) (#594)

Adds the Near Intents 1Click API provider to both bridge benchmarks.
Metrics will start populating once the bridge-monitor harness is
redeployed with the nearintents_bridge.go integration shipped on
mobula-api side and the NEARINTENTS_API_KEY env var set on Railway.

Includes Near Protocol logo (public/logos/near-intents.svg), provider
registry entry with longDescription + docs link, and YAML provider
entries on both bridge-quote-latency and bridge-fee with the post-#584
avg(...) wrapper on success queries.

SEO cohort copy updated to include Near Intents in both benches.
Adds a 'dimensions.chain' block to bridge-quote-latency and bridge-fee
with values matching the harness chain label (Solana / Base / Arbitrum
/ HyperCore). The site UI surfaces this as clickable tabs.

Why: the aggregate cross-corridor view is misleading on bridges with
asymmetric per-corridor performance. Near Intents in particular is
bimodal (Arb to HyperCore ~30ms via solver-cached bid, Sol to Base
~3000ms full auction wait), and the aggregate histogram quantile
produced 175ms p50 which represents neither corridor.

Requires the matching mobula-api PR (chain label emission in the
bridge-monitor harness).
) (#598)

Each destination chain in the current route set has exactly one source
chain, so we can surface the full corridor (Sol to Base, Arb to
HyperCore, etc.) in the tab label without changing the underlying
dimension (still chain = destination, Prom label intact). User scans
the tabs and knows immediately which route each tab represents
instead of having to remember the route set.
…tab (#599) (#600)

Two small UX fixes on the chain dimension:

1. Add a hypercore -> hyperliquid alias in logo-manifest.ts so the
   'Arb to HyperCore' tab renders with the Hyperliquid logo. HyperCore
   is the trading engine of Hyperliquid, same brand. No new asset needed.

2. Drop the 'All routes' default tab. The aggregated cross-corridor
   view is mathematically valid but conceptually meaningless on these
   benches (Sol to Base ~3000ms and Arb to HyperCore ~30ms produce a
   histogram_quantile that lands at neither corridor). Per-corridor
   tabs are the honest default.
…#603)

The route was redirecting to /api/llm-context, which is fine for
permissive clients but breaks:
- llms.txt validators (llmstxt-checker fails on non-200)
- stricter AI crawlers (Perplexity, some Bing AI variants) that mark
  non-standard paths returning a redirect as no-content and skip
- canonical attribution (AI engines citing the body now point at the
  internal /api/llm-context path instead of the public /llms-full.txt)

Re-exports the GET handler from /api/llm-context so there is a single
source of truth for the markdown body. No content change.
…#605)

* fix(sitemap): pin <lastmod> to deploy time, not request time (#604)

Sitemap runs on force-dynamic (to bypass Next's 2 MB Data Cache limit
on the rendered output), which means `new Date()` at module init
re-evaluates on every Google crawl. Effect: every URL had a freshly
updated <lastmod> each visit, Google flagged the signal as unreliable
sitewide and stopped using lastmod for recrawl prioritisation
(confirmed behaviour, documented by Gary Illyes publicly).

Fix: bake a build-time timestamp via next.config env injection, read
it from process.env.NEXT_PUBLIC_BUILD_TIME in sitemap.ts. Stable per
deploy, fresh per deploy. Per-bench <lastmod> still uses live
bench.lastRunAt as before — that path was already correct.

* release: hold solana-tx-landing-latency off main

Bench and its dependent /alternatives + /answers pages held off main
while the harness data quality / methodology is iterated on dev.
Matches the existing release-hold pattern (see 12bb8a7). Files stay
on dev untouched; dev to main merges will need to re-delete or skip.

Removed:
- benchmarks/solana-tx-landing-latency.yml
- alternatives/jito.yml (benchmark: solana-tx-landing-latency)
- alternatives/helius.yml (benchmark: solana-tx-landing-latency)
- answers/which-solana-rpc-lands-the-most-transactions.yml
Fixes /api/citable returning all benchmarks as draft when
/api/stat/<slug> for the same bench returns live data. Root cause:
loadAllBenchmarksCached returned an all-draft set whenever per-bench
fetches threw at cold start, and the cache then served that poisoned
set for the rest of the 60s revalidate window.

Restores the throw-on-all-draft behaviour at the cache boundary so
unstable_cache keeps the previous good value during a Prom blackout.
Adds an AllBenchmarksDraftError sentinel and a getBenchmarksSafe
wrapper. Call sites split:

- API endpoints and feeds (citable, llm-context, llms.txt, rss.xml)
  catch the sentinel and return 503 with retry-after, so downstream
  consumers do not treat the placeholder set as ground truth.
- Pages (home, benchmarks index, bench slug, category, mcp page) and
  shared loaders (chains, providers) use getBenchmarksSafe which
  catches the sentinel and renders draft placeholders so the build
  and the page render still succeed.
- Sitemap already uses safeLoad which catches and falls back.

Cache key bumped from v12 to v13 to flush any poisoned snapshot
already stored in Upstash KV.

Adds a regression test that asserts the aggregator never returns a
stable all-draft list. Extracted aggregateBenchmarks as a pure
function so the test does not need Prometheus or unstable_cache.
…) (#617)

* feat(rate-limit): bypass per-IP throttle for known AI crawler user agents

* rate-limit: expand AI bot allowlist to match robots.txt

Adds Meta-ExternalAgent, Meta-ExternalFetcher, cohere-ai, Perplexity-User
(all already in robots.txt), plus AI2Bot, Ai2Bot-Dolma, Kagibot,
FacebookBot, MistralAI-User, TimpiBot, Webzio-Extended for completeness.
Comment now states the regex must stay in sync with src/app/robots.ts.

Western LLM coverage is at ~95 percent after this. Chinese models other
than ByteDance (Bytespider) do not publish a documented bot UA, so they
fetch via generic browser UAs and stay throttled by design.
…#621) (#622)

PR #613 left valid ad-hoc /compare/<a>-vs-<b> URLs serving HTTP 200 +
a loading skeleton + noindex meta. With loading.tsx in place, Next 16
streams the skeleton with 200 before the page body's notFound() can
demote the status. Plus generateMetadata only checked that both
providers exist, missing the no-shared-bench case.

This hotfix lands the same fix as PR #621 (dev) but inline on main's
older file structure (no extracted compare-compute helper module
yet). hasSharedBenches() is a pure set arithmetic helper on
appearances, no Prom fan out. loading.tsx removal lets notFound()
ship a real 404.

Verified locally:

  /compare/alchemy-vs-quicknode  -> 404 (quicknode missing)
  /compare/mobula-vs-coingecko   -> 308 then 404 (coingecko missing)
  /compare/alchemy-vs-helius     -> 404 (no shared bench)
  /compare/quicknode-vs-alchemy  -> 308 to canonical
  /compare/binance-vs-bybit      -> 200 real h1 (valid ad-hoc)
  /compare/ethereum-vs-solana    -> 200 real h1 (curated)
)

Live audit on prod showed /api/citable marking 25 of 26 benches as
status=insufficient while /api/stat for the same slugs returned
status=live with real leader values. Root cause: the per-bench loader
and the aggregator loader compute sampleSize differently; when the
aggregator falls back to a draft placeholder due to a cold Prom hit,
the bench appears with sampleSize=0 even though per-bench cache holds
fresh data. The check b.sampleSize===0 was then mass-flagging these
benches as insufficient in /api/citable, /api/llm-context, /api/mcp.

The other checks already catch the genuine empty case (liveResults
length and p50 finiteness). Dropping the sampleSize check restores
the 12 to 14 healthy benches that surfaced pre-regression.
- Aggregator throws AllBenchmarksDraftError when <50% of live specs
  produce a live bench (was: only when 0/N live). Prevents a mixed
  set like 2/26 live being cached for 60s while /api/stat returns
  live for the same slugs.
- Bumped all-benchmarks cache key v13 to v14 to flush the current
  poisoned snapshot on prod.
- Warm /api/citable, /api/llm-context, /api/mcp explicitly on
  prod deploy so the aggregator cache starts hot.
- Added tests for the new quorum threshold.
Same files as #632 on dev, brought to main so the GitHub Actions
cron schedule can fire (workflow_dispatch and on.schedule both
require the workflow file to live on the default branch).

No runtime code change. Only adds:
- scripts/hf_publisher/* (Python daily snapshot script + tests)
- .github/workflows/hf-publish.yml (cron + manual dispatch)
- .gitignore (.venv exclusion)
…iders

feat(pm): aggregator providers (Mobula, Predexon, Codex) on pm-api-latency
…n-bundle

fix(build): trace YAML spec dirs so Vercel ships latest dev specs
…or-providers-prod

chore: cherry-pick PM aggregator providers + YAML file tracing to prod
)

* feat(hf): card metadata + schema completeness v2 (audit follow-up)

* fix(hf): type-narrow higher_is_better_by_slug dict + silence chain_leaders snap
…gregators

feat(pm): collapse aggregator providers + add venue filter
…m-prod

chore: cherry-pick venue dimension + collapsed aggregators to prod
Flotapponnier and others added 27 commits June 24, 2026 15:14
Brings the full /perps hub from dev to main:

- /perps hub page with cohort summary, leaderboard, By asset tab, age badge
- 3 new bench specs (perp-volume-share #41, perp-open-interest #42,
  perp-funding-stability #43)
- Per-product PerpVenueSection wired into /products/<slug> for 16
  perp DEX venues
- 9 new perp DEX registry entries (drift, vertex, edgex, extended, aevo,
  pacifica, variational, ostium, grvt)
- 13 logo assets, SVG-first
- Footer + sitemap entries for /perps
- Hub pill on per-product pages

Backed by perp-cohort-stats harness on Railway (volume, OI, fees,
active markets, funding via Mobula aggregator across 12 venues).
…r-prod

feat(perps): cherry-pick perp cluster sprint 1+2 to prod
- perp-volume-share + perp-open-interest: query venue=gmx-v2 (harness
  label) instead of venue=gmx, so the GMX row stops returning /bin/zsh.
- perp-funding-stability: replace 8 venues that have no funding source
  (dydx, paradex, aster, drift, vertex, edgex, extended, aevo) with the
  8 Mobula funding cohort venues that DO publish
  perp_venue_funding_24h_bps (gate, lighter, kucoin, mexc, bitget,
  kraken, coinbase, deribit). The bench now has 12 real venues with
  data, was 4 of 12 with the rest blank.
…fixes-prod

chore(prod): cherry-pick perp bench fixes (gmx slug + funding-stability cohort)
…cohort copy

- src/lib/providers.ts: seed stub profiles for 9 perp cohort venues
  (drift, vertex, edgex, extended, aevo, pacifica, variational, ostium,
  grvt) that have no bench appearance. Without this, getProvider()
  returned undefined and /products/<slug> rendered 404 for every venue
  the cohort harness tracks but no bench specifically ranks.
- benchmarks/perp-funding-stability.yml: fix cohort copy to honestly
  describe the Mobula funding aggregator scope (HL onchain + 11 CEX),
  not the onchain-perp cohort the original draft promised.
- src/data/provider-registry.ts: vertex URL updated to www.nado.xyz
  after the rebrand (vertexprotocol.com returns 404).
- src/app/perps/page.tsx: description says 16 perpetual DEXes, not 15
  (off-by-one).
…3-fixes-prod

chore(prod): cherry-pick perp Sprint 3 fixes (product pages + funding cohort copy)
- Bump providers cache key v1 to v2 so the PERP_VENUE_SEED added in
  PR #683 actually surfaces (Vercel ISR was serving the pre-seed
  cached profile list, which is why /products/drift etc. kept 404ing).
- Loosen Vol/OI bands from 50/200 to 80/300 so zero-fee venues like
  Lighter (~115x today) read as elevated rather than wash-traded.
  Tooltip copy updated to explain the new thresholds.
- Document the PromQL or-fallback mask risk on the perp funding query
  (P1 audit item, no behavior change today).
…bump-prod

chore(prod): cherry-pick providers cache bump + vol/OI bands
Adds an Upstash-backed cohort snapshot layer so the two hub pages serve
last-known-good data even when Prom blips. A Vercel cron writes a fresh
JSON snapshot every minute; the readers (fetchPerpCohort, fetchHlCohort,
fetchHlHip3Cohort) prefer the snapshot, fall through to live Prom on
stale or missing blobs, and write back on success.
…rialize-prod

chore(prod): cherry-pick cohort materialize for /perps + /hyperliquid
…ative

chore(gas-estimation): drop Blocknative provider (cherry-pick from dev #693)
…ts (#695)

PR #690 cherry-picked the cron route + the helper file but the helper
shipped to main at src/lib/hl-builder-stats.ts while the route still
imported from the dev-only path @/lib/benches/hyperliquid/builder-stats,
which broke every prod build since (Turbopack module-not-found). Aligning
the import path with the actual file location on main.
…ce (12 venues) + correct shuffled tags

success query: switched from perp_venue_health{venue} (only published
for cohort onchain venues, returned null for 11 CEX) to clamp_max(
count_over_time(perp_venue_funding_24h_bps{venue,asset=ETH}[1h]), 1).
Now returns 1 if any funding sample landed in the last hour for that
venue, works for all 12 venues. Bench was returning unknown_slug from
/api/stat because the missing health metric was failing the data
confidence check.

tags + formulas: 8 venues (gate, lighter, kucoin, mexc, bitget, kraken,
coinbase, deribit) had tags + formula venue references copied from the
onchain cohort by mistake (Lighter tagged 'Starknet L2', KuCoin 'BNB
Chain', MEXC 'Solana perp DEX', etc.). Restored to honest descriptions
matching each CEX's actual cadence.
…bility-fix-prod

chore(prod): cherry-pick perp-funding-stability success query + tags fix
Bundles the search-experience work merged on dev into a single
cherry-pick to main. Includes:

- Cmd+K / Ctrl+K / '/' to open; ESC to close
- Wide input-pill trigger in the header (Linear-style, no shortcut badge)
- cmdk + Fuse.js fuzzy search over ~400-doc index (benchmarks, products,
  alternatives, answers, chains, compare pairs, pages)
- Modal redesign: hairline border, single shadow, no accent focus ring
  (opt-out from the global :focus-visible accent rule)
- Idle state: Recent (localStorage chips, X-to-remove, Clear) + Live
  leaders (horizontal snap cards with leader logo + p50 from /api/citable)
  + Trending list (vertical, same data shape)
- Search state: results grouped by kind, each row gets the leader's
  logo (benches) / entity logo (products/chains) / kind icon fallback
- Mobile: full-bleed sheet, footer hidden, X close button
- Open animation: handcrafted CSS keyframes (fade + 4px slide-down,
  150ms ease-out), reduced-motion fallback

Template fix bundled (search-adjacent — surfaces description tokens):
- renderBenchmarkText now resolves seoTitle / seoDescription / subtitle /
  methodology / disclaimer (Featured / Trending cards in the search
  dialog were showing raw '{{best_name}}' tokens in description previews)
- Chain stash lookup case-insensitive (fixes perp-fees uppercase chain
  values ETH/BTC/SOL)
- Bumped bench-unfiltered v11→v12 + all-benchmarks v15→v16 so the
  cached snapshots regenerate through the wider resolver instead of
  serving raw tokens for up to 300s after deploy

Deps:
- cmdk@^1.1.1, fuse.js@^7.4.2 (lockfile synced from dev)

Header redesign:
- Removed | separator between Contribute and GitHub
- 15px nav, h-16 desktop, backdrop-blur on the sticky
- GitHub icon-only, gap-spaced utilities
)

The Toncoin → Gram (ticker TON → GRAM) rename took effect on 2026-06-15
via TON Vote (81% community support). The network itself stays branded
TON / The Open Network; only the native token rebranded. We render
'Gram' everywhere visible (chain label, ticker, native symbol, copy)
while keeping internal identifiers stable.

Display layer (TON → Gram everywhere visible):
- chains.ts: label 'TON' → 'Gram', nativeSymbol 'TON' → 'GRAM',
  description updated with 'formerly TON / Toncoin' continuity line.
- provider-registry.ts: ton + tonapi descriptions updated.
- benchmarks/l1-finality.yml: SEO + FAQ + per-chain explainer + provider
  entry display + abstract + methodology.
- benchmarks/network-fees.yml: editorial + FAQ + per-chain explainer +
  fee tier copy + provider entry display (0.005 TON → 0.005 GRAM).
- benchmarks/wallet-labels-coverage.yml: editorial + FAQ + per-chain
  explainer + chain dimension label + TonAPI provider tag.
- answers/{cheapest-fees,fastest-finality}.yml: intro / FAQ / limitations.
- l1-finality.md + harness READMEs: chain table + confidence audit copy.

Logo + brand:
- public/logos/ton.svg replaces ton.png (new Gram circular badge from
  ton.org/media; logo-manifest now points at the .svg).
- brand.ts: ton color #0098EA → #30A1F5 to match the new mark.

Kept stable on purpose (technical identifiers, ops contract):
- slug 'ton' everywhere (URL, Prom queries, harness contract).
- Prometheus label chain="ton" in all queries.
- DefiLlama chain mapping ton → 'TON' (their API still uses 'TON';
  tokenSymbol is already 'GRAM').
- API endpoints (tonapi.io, ton.org), twitter handles (@ton_blockchain,
  @tonapi_io) - all unchanged upstream.
- Go function/type names (StartTONWallClock, tonState) and TON_API_KEY
  env var - renaming would require ops coordination + redeploy.

SEO continuity: 'Gram (formerly TON)' phrasing in 1-2 anchor spots per
page so search engines bridge the rename for 30+ days.
Privacy-friendly analytics (no cookies, no PII). Loaded async so it
never blocks paint. Key is exposed as NEXT_PUBLIC_AHREFS_KEY (public
by design, identifies the property not an account secret). Skipped
entirely when the env var is unset so staging and preview deploys do
not pollute the prod analytics graph.
chore(prod): cherry-pick Ahrefs Web Analytics
Follow-up to the TON → Gram display rebrand (#697). The 'Ton' label
was still leaking into search results and the URL /chains/ton was
visible to users. This PR renames the chain slug ton → gram with 301
redirects so inbound links + SEO rank signal consolidate onto the new
canonical, and adds a Prom regex straddle so the existing harness
keeps feeding live data through the transition.

## Slug rename (TS layer)
- src/lib/chains.ts: slug 'ton' → 'gram' (label/description/native unchanged)
- src/data/provider-registry.ts: rekey ton → gram
- src/lib/logo-manifest.ts: gram → /logos/ton.svg (file kept, slug renamed)
- src/lib/brand.ts: rekey color entry
- src/lib/chain-kpis.ts: rekey to gram, DefiLlama API string stays 'TON' (verified live)
- src/lib/providers.ts:
  - CANONICAL_NAMES: gram → 'Gram', tonapi → 'TonAPI' (was title-casing to 'Tonapi')
  - PRODUCT_ALIASES: ton → gram (stale inbound references resolve to canonical)

## Redirects (next.config.ts)
- /chains/ton → /chains/gram (308 permanent)
- /products/ton → /chains/gram (308 permanent)
- /benchmarks/:slug/ton → /benchmarks/:slug/gram (308 permanent)
- CHAIN_REDIRECT_SLUGS: ton → gram so /products/gram keeps redirecting to /chains/gram

## Bench YAMLs
- per_chain_explainer.slug: ton → gram (matches the new /benchmarks/<bench>/gram URL)
- provider entry slug: ton → gram (matches CHAINS new slug for surfaces that ref by slug)
- Template refs {{p50:ton}} → {{p50:gram}}
- Prom queries: chain="ton" → chain=~"ton|gram" (straddle during harness relabel
  window; the regex matches the current chain="ton" labels and the future
  chain="gram" labels, so the bench keeps rendering live data with no gap)
- wallet-labels-coverage dimension value stays 'ton' (Prom label the harness
  emits at runtime; flip to 'gram' once the harness redeploys)

## Out of scope (separate harness PR)
The harness Go code in harnesses/l1-finality/ and harnesses/network-fees/
emits chain="ton" labels. Updating those is a separate Railway redeploy.
The straddle regex covers the transition with zero data gap.

## Test plan
- [ ] Cmd+K, type 'gram' → top result reads 'Gram' chain (no more 'Ton').
- [ ] /chains/ton → 308 → /chains/gram (page renders with Gram badge, leader, p50).
- [ ] /products/ton → 308 → /chains/gram.
- [ ] /benchmarks/l1-finality/gram → renders per-chain explainer.
- [ ] /benchmarks/l1-finality leaderboard row Gram still has live p50 (Prom straddle works).
- [ ] No regression on other chains in same benches.
rename chain slug ton → gram + 301 redirects + Prom straddle
…benchmarks payload) (#706)

* chore(benchmarks): drop solana-tx-landing-latency (harness offline, 404 bleeding SEO) (#701)

* perf(benchmarks): slim card projection cuts /benchmarks payload 3.2MB to 280KB (#704)

* fix(types): widen isInsufficient to accept BenchmarkCardData slim shape

Cherry-pick fallout: #704 introduced BenchmarkCardData (slim projection
for the hub) but main's isInsufficient still required full Benchmark.
The hub card calls isInsufficient(b) where b is the slim type, which
typecheck rejected. Widening the predicate input to a structural
subset lets both Benchmark and BenchmarkCardData pass without losing
the runtime behavior.
Fixes the slow Live Leaders skeleton: dialog used to fetch /api/citable
on every open (~50 KB, full benchmark assembly server-side, frequently
visible 1-2 s skeleton). Now a Vercel cron pre-builds a 12-card subset
every minute, persists it as a single Upstash KV blob, and the dialog
opens with data already prefetched at page load.

Architecture:
- src/lib/search-featured.ts: builds the slim blob (featured + trending
  cards: slug, title, category, unit, value, leader). Single source of
  truth for the dialog's hardcoded slug lists.
- src/app/api/cron/warm-search-featured/route.ts: every minute, calls
  buildFeaturedLeaders + writeCohortSnapshot('search-featured'). Same
  CRON_SECRET gate + soft-no-op-on-missing-creds pattern as the existing
  cohort cron jobs.
- src/app/api/search/featured/route.ts: KV-first reader with live-build
  fallback (cold-start safety). Aggressive edge cache (s-maxage=60,
  swr=300) — most requests served by Vercel edge with zero origin hit.
- SearchProvider: prefetches /api/search/featured at mount (idempotent)
  and exposes featured + prefetchFeatured via context. Re-fetch hook on
  trigger hover/focus.
- SearchTrigger: onMouseEnter + onFocus call prefetchFeatured. Dialog
  almost always opens with the blob in memory; the brief 'skeleton'
  state is now invisible to the user.
- SearchDialog: consumes featured/trending arrays from context (zipped
  against the search index), drops the per-mount /api/citable fetch.
  Search-results rows still get leader logos via the same warmed blob
  when the bench appears in featured/trending; otherwise fall back to
  the kind-icon (no extra API calls).
- vercel.json: cron schedule * * * * * for warm-search-featured.

Graceful degradation:
- No CRON_SECRET / no Upstash creds → cron 200s with configured:false,
  endpoint falls through to live build path, dialog still works.
- Upstash unreachable → endpoint falls through to live build, same UX
  as the old /api/citable path (slow but functional).
- Worker dies → endpoint serves last-good KV until 24h safety TTL
  expires, then falls through to live build.

Payload size: ~50 KB (/api/citable, full index) → ~2 KB
(/api/search/featured, 12 cards).
TTFB: ~1-2 s server-side assembly → ~5-10 ms KV GET (edge-hit: ~0 ms).
Diagnostic: after PR #705 (chain slug ton → gram), /chains/gram 404s
and /benchmarks/network-fees still renders 'TON' in chain headings.
Two root causes:

1. getBenchmarksForChain('gram') filters results[].slug === 'gram',
   but the materialize worker's KV snapshots still hold the legacy
   results[].slug === 'ton' (worker on Railway runs from the private
   mobula-api repo; it hasn't redeployed with the renamed YAML yet).
   → 0 matches → notFound() at chains/[slug]/page.tsx:90.

2. ChainHeadingsSummary renders r.name verbatim. Stored snapshots
   still carry name='TON', so the heading reads 'TON native transfer
   fee usd' even though chains.ts now says label='Gram'.

Fix: introduce a chain-slug alias system so the canonical layer
absorbs the rebrand without waiting on the harness.

- src/lib/chains.ts:
  - CHAIN_SLUG_ALIASES { ton: 'gram' } — single source of truth.
  - canonicalChainSlug(slug) and chainLabelForSlug(slug) helpers.
  - getBenchmarksForChain now matches on the canonical slug + any
    legacy alias, so /chains/gram surfaces benches whose stored
    results still carry slug='ton'.

- src/lib/spec.ts:
  - overlayEditorial reconciles stale stored results against the
    current spec. For each result whose slug is a known legacy alias
    (e.g. 'ton'), look up the spec provider by canonical slug
    ('gram'), rewrite result.slug + result.name to match. Downstream
    surfaces (leaderboard table, search dialog, ChainHeadings) all
    see the canonical identity without any per-component patch.
  - Cache keys bumped bench-unfiltered v12 → v13 and
    all-benchmarks v16 → v17 so the unstable_cache regenerates
    through the new reconciliation path on deploy.

- src/components/chain-headings-summary.tsx:
  - Anchor id, link URL, and explainer lookup go through
    canonicalChainSlug so a stale 'ton' result still hits the renamed
    'gram' explainer + lands at /benchmarks/<bench>/gram.
  - Display name resolves through chainLabelForSlug, with the bench
    result name as a fallback. Renders 'Gram' the moment chains.ts is
    updated — no wait on the materialize worker.

Test plan:
- /chains/gram → 200 with l1-finality + network-fees in the list.
- /benchmarks/network-fees → chain heading reads 'Gram transaction
  fee usd', anchor id='gram', link to /benchmarks/network-fees/gram.
- /api/citable still returns rows for both old + new slug surfaces
  (no impact on machine-readable consumers).
- Harness on Railway can stay on chain='ton' label; the existing Prom
  regex straddle (PR #705) keeps queries hitting both labels.
…h 036 metric)

perp_venue_funding_24h_bps from the new perp-cohort-stats harness was
not reaching OCB Prom reliably after the recent harness redeploys, so
the bench returned all-null. The bench now uses
perp_funding_hold_24h_bps which the established perp-funding bench
036 already publishes for 7 venues (Hyperliquid, Binance, Bybit, OKX,
dYdX v4, Paradex, Aster). Cohort trimmed to those 7 to match the data
that actually exists in Prom.

Drops the 5 venues that only had Mobula-sourced funding (gate, lighter,
kucoin, mexc, bitget, kraken, coinbase, deribit) since the new metric
was not reliably scraped. Future revision can re-add them once the
perp_venue_funding_24h_bps scrape is verified end-to-end.
spec.ts imported canonicalChainSlug from @/lib/chains. chains.ts in
turn imports getBenchmarksSafe from @/data/benchmarks which imports
loadBenchmark from @/lib/spec. Webpack collapsed the cycle into a
chunk whose init order tripped a TDZ on the /alternatives/[slug]/
opengraph-image route ('Cannot access ec before initialization'),
breaking every prod deploy since PR #708.

Fix: extract CHAIN_SLUG_ALIASES + canonicalChainSlug into a tiny
@/lib/chain-aliases module with zero external imports. spec.ts now
imports from there. chains.ts re-exports for backward compatibility
so no consumer needs a path change.
@Flotapponnier

Copy link
Copy Markdown
Collaborator Author

Superseded by #711 (same fix landed independently). Closing.

@Flotapponnier
Flotapponnier deleted the fix/circular-chains-spec branch June 25, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants