Add upgrade status data layer for roadmap pages - #18991
Conversation
✅ Deploy Preview for ethereumorg ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🔎 First-pass review — ✅ Looks mergeableData-only PR (lane: data) adding a typed upgrade-status layer under One cheap cleanup worth doing now, before five PRs depend on these types:
AnalysisLane: data (all four files under Checked: placement/shape vs neighbouring Non-blocking nit: CI: Lint/type-check ✅, Chromatic visual ✅ (no diff, as intended), unit tests running/passing. No failing checks. The five maintainer open-questions (directory home, second date stamp, i18n label set,
|
|
Both fixed. Dropped
|
|
Hi @nloureiro — apologies for the slow reply on this, and thanks for the unusually thorough writeup. It made the following possible. I've opened #19059, which supersedes this PR. Before the details: your diagnosis was right and your What we foundDigging into your open questions turned up one thing that reframes the problem. Forkcast publishes the Glamsterdam target in two places:
So on your question 6 ("is there an ACD source for Q4 2026?"): yes, effectively — Forkcast has published it for nine months. The page wasn't overclaiming, it was nine months behind, and for the first ~2.5 months of that it actively contradicted upstream (H1 2026 vs Q4 2026). Your instinct that the copy should follow the data was correct; the data just says more than That also means the "deliberately no quarter granularity" decision needs reversing — The wider find: Forkcast has 23 in-scope EIPs for Glamsterdam, and the page covers 11. Ten more are named only in the hand-typed note at line 34, and two (8070, 8136) appear nowhere on the site. Also — EIP-8080 was declined at ACDC #181 on 2026-06-25 and the page kept a full section explaining it until #18990 on 08-06. Six weeks, in 25 languages. Your PR couldn't have caught that; nothing could, which is rather the point. What #19059 changesSame schema, different author. The records are generated from Forkcast rather than transcribed, which also scales past one file per fork — Forkcast tracks 7 upgrades, so Answering your other open questions from that work:
Your other three PRs stay open#18993, #18998 and #19000 are not superseded and I'd like to keep them as yours. #18993's approach is better than what I first wrote: rendering They will need a rebase and a small adaptation once #19059 lands. Concretely, in #18993:
And in #18998: In #19000: Since these are your branches, I haven't touched them. Happy to do the rebase and the renames if that's useful, or leave it to you — whichever you prefer. Your call on closing this one too; I've left it open so you can see the reasoning first. Genuinely: the schema in here is the load-bearing part of #19059, and several of your open questions turned out to be the right questions. |

Description
The problem
The Glamsterdam roadmap page went stale within about two weeks of its 20 July 2026 edit. Worth being precise about what went stale, because it points at the fix:
Facts have a roughly weekly shelf life, set by the All Core Devs call cadence. Prose has a shelf life of months. Right now they share one markdown file, so refreshing a date means a human editing a file full of explainer text. That friction is why it drifts.
This happened again today, which is a decent illustration: #18990 landed a scope refresh, and getting the timeline from "H2 2026" to "Q4 2026" meant touching the page prose,
src/data/roadmap/releases.tsx, and an intl string in three separate places.The proposal
Separate the two. Volatile facts move into a typed data layer; prose stays in
public/content/. This PR adds only the data layer.src/data/upgrades/types.ts—UpgradePhase,MilestoneStatus,EipStatus,PartialDate, and theUpgradeData/Milestone/UpgradeEip/MainnetTargetshapes.src/data/upgrades/glamsterdam.ts— the first record: phase, mainnet target, 3 milestones, and the 11 EIPs the page has a section for.src/data/upgrades/index.ts— barrel, matching thesrc/data/{apps,quizzes,topics}/index.tspattern.src/data/upgrades/README.md— the editing contract.There is no visual change
Nothing imports this yet. The Netlify preview should be identical to
dev— that's the intended review check for this PR.Why
src/datadocs/stack.mddescribes/src/dataas "general data files importable by components", which is exactly what this is. It sits alongsidecommunity-meetups.json,developer-docs-links.yaml, and — most directly comparable —networkUpgradeSummaryData.ts, which already stores activation dates for shipped forks.Why
.tsand not.jsonInitially specced as JSON. That doesn't work: TypeScript widens JSON string values to
string, sosatisfies UpgradeDatafails on correct data:declare modulewould type the import asUpgradeData, but that asserts rather than checks —"phase": "devnetX"would sail through. That's the appearance of safety with none of it..tswithsatisfiesgives a real build-time guarantee, and matches how every other typed data file here is written (developerTools.ts,wallet-data.ts,quizzes/index.tsall usesatisfieson an inline literal; none of the 12 JSON imports insrc/carry a type).Verified by breaking each enum in turn and running
pnpm type-check:A typo'd status now fails CI instead of quietly rendering.
Dates carry only the precision that has a source
whenis a single field whose precision is whatever keys are present —{ year },{ year, month }, or{ year, month, day }. It replaces an earlierwindowstring plus a separate ISOdate. Two fields carrying the same information are two fields a future Forkcast sync could update independently and inconsistently; one field can't disagree with itself. It also removes an English month name ("August 2026") from the data, so windows now locale-format throughdateTimeFormat()instead of rendering as English inside a translated page.The union shape does real work — both of these are compile errors:
There is deliberately no quarter or half-year granularity. Every value with a real source fits year / month / day, and the only quarter ever claimed for Glamsterdam is the unsourced one discussed below. I checked whether half-year framing exists in the wild before ruling it out: there is no
H1/H2string anywhere in English content orsrc/intl/en/. Quarters appear only in prose strings (page-10-year-anniversary.json, a Pectra video transcript) and in the three places #18990 added "Q4 2026" — never in structured data. Documented as a deliberate omission in the README so it doesn't get re-litigated.Why every date carries a confidence level
The July page flattened a projection into a plain statement. Once "we think Q4" is written as "planned for Q4", nothing downstream can tell the difference.
So confidence lives in the data:
MilestoneStatusis orderedlive→confirmed→anticipated→projected, andmainnet-targetcarries an explicitconfirmedboolean. The UI cannot overclaim, because the data won't let it — aprojecteddate has to render with a qualifier.Why two date stamps
template: upgradepages. Implies a human reviewed the prose.facts-verified— says only that someone checked these values against Forkcast that day. Makes no claim about the surrounding prose.Collapsing them loses real information in both directions: a facts-only refresh shouldn't imply the explainers were re-read, and a typo fix in the FAQ shouldn't imply the dates were re-checked.
Sources
Every value traces to Forkcast (via its published structured data, since the site body is client-rendered) or meta EIP-7773. Three notes where that changed what was originally specced:
mainnet-target.windowis"2026", not"Q4 2026". See the section below.anticipated, notlive.Which EIPs are in
eips[]The 11 the Glamsterdam page has a section for, in page order — not the meta EIP's full roster. Forkcast and EIP-7773 already publish the complete list; a hand-maintained copy of 25 entries would be a second source of truth that silently rots, and it edges toward being an EIP directory.
This gives a checkable invariant: every EIP section on the page has exactly one entry in
eips[]. PR 3 will add a test that parses the page for EIP references and asserts coverage, so a future twelfth section fails CI rather than silently rendering no chip.All 11 are
sfiper EIP-7773. Two of them — 7975 (eth/70) and 8159 (eth/71) — are filed under the meta EIP's "Other EIPs → Networking" heading rather than under SFI, but they're scheduled all the same; the page correctly describes both as required for all execution layer clients. They're recorded assfiwith nonetworkingstatus and nocategoryfield, on the rule that status expresses confidence and category expresses kind: collapsing two orthogonal dimensions into one enum is how enums rot. That rule is now written into the README.Scope can still change — EIP-7773 is still
Draft, as meta EIPs are until activation. The README says so, and PR 3 keeps a sentence to that effect on the page.The mainnet target is
"2026", not"Q4 2026"This is the one place where the data deliberately says something less specific than the page currently does, so it's worth setting out the reasoning.
2026because that is the most specific defensible claim.This is a net gain for readers, not a regression. The page today shows a vague quarter and no milestones at all. The component in PR 2 shows the year plus a concrete next milestone, a
facts-verifieddate, and a link to Forkcast for live detail. Readers get more useful information and a clearer sense of how firm it is.One honest caveat on that: because Forkcast has no Glamsterdam testnet forks scheduled, the concrete next milestone is currently Devnet-8 ("Plataberget"), expected August 2026 — a devnet rather than a public testnet fork. Once Sepolia and Hoodi forks get dates, they land in
milestonesand become the next milestone automatically, with no prose edit.Follow-up stack: four PRs, not six
roadmap-upgrade-data-layerdevroadmap-upgrade-statusTopicLayoutroadmap-eip-chipseips[].statusroadmap-hegota-pageTwo further PRs were planned and have been deliberately dropped, because building them requires answering question 2 below rather than guessing at it:
/roadmap/upgrade-sequence refactor, pointing the existing release carousel at this data layer.They collapse into a single piece of work about one seam, and it is the seam maintainers have not ruled on. The carousel spans both categories — Pectra and Fusaka have shipped, Glamsterdam and Hegotá have not — so "read from the data layer" would mean reading from two sources whose relationship is undecided. Worse, that refactor would have created Pectra and Fusaka records here, while both already exist in
networkUpgradeSummaryData.ts(Fusaka at2025-12-03with block, epoch, slot, ETH price and Wayback link). That is precisely the duplication the refactor was meant to remove.Explicitly not in scope for any of this: a workflow, cron, or script that writes these files, and any EIP directory, call tracker, or devnet dashboard. Forkcast does those well; duplicating them would create the second source of truth this exists to prevent. (For the avoidance of doubt, the scheduled job this schema anticipates would be a deterministic diff of structured fields against Forkcast's published data — no model involved in deciding what a value should be.)
Open questions for maintainers
Is
src/data/upgrades/the right home? Two reasons to doubt it, and I'd rather ask than be told:src/data/roadmap/already exists (releases.tsx, which powers the/roadmap/release carousel). These files are roadmap data by any reasonable reading, sosrc/data/roadmap/upgrades/may be the more consistent home. I defaulted to the top level becausesrc/data/roadmap/currently holds one presentational file rather than a general namespace — but that's a weak reason and easy to change now, much harder after five more PRs import from it.networkUpgradeSummaryData.tsalready holds activation facts for shipped forks, so there's a surface case for one file covering an upgrade's whole lifecycle. I think they should stay separate, and the reason is a safety property rather than tidiness:src/data/upgradesis bot-writable by design — the point of a typed, Forkcast-checkable file is that a future scheduled job could diff it and open a PR.networkUpgradeSummaryData.tsis the opposite: an immutable historical record of 26 entries with ETH prices and Wayback links going back to 2013, appended once per activation. Merging them would hand a future automated writer access to settled history. Keeping them apart bounds the blast radius of a bad automated write to facts that are still provisional.isPendingis the intended mechanism, the answer is clean: the release carousel and the history page read fromnetworkUpgradeSummaryData.ts,src/data/upgradesholds only forward-looking volatile facts, and the two dropped PRs become one PR about the handoff between them. If it isn't, the shape is different enough that guessing would waste the work.Any objection to a second date stamp? Two dates on one page needs clear labelling or it's just confusing. If one stamp is preferred, I'd rather know before PR 2 renders it.
i18n input wanted on the label set. Every user-facing label is a Crowdin task across all locales and expensive to revise later, so the stack keeps the set deliberately small: 11 new strings total — 7 in PR 2 (status block), 2 in PR 3 (EIP chips), 2 in PR 4 (planning-phase label, Hegotá nav entry). If anyone with i18n context wants to weigh in on trimming that further — or on whether hedges like "Date not yet confirmed" survive translation without losing their force — that feedback is most useful before PR 2 merges.
Does
NetworkUpgradeDetails.isPendingpoint at a better design for the handoff? That type has anisPending: truevariant which forbidsethPriceInUSDandwaybackLink— and no entry currently uses it. Read one way, someone already designed the pending-upgrade flow: a pending row exists first, and the price/Wayback fields get filled in on activation. If that's right, it is a better design than having the history page read activation dates out ofsrc/data/upgrades: the upgrade would already have its row, and activation would simply populate it.I'm flagging rather than concluding: an unused type variant is as often abandoned intent as it is a plan, and someone here may know the history. Worth settling before the deferred handoff work is built, since the two designs are different PRs.
Should
UpgradePhasehave anactivatedvalue at all? It's in the union, but if shipped upgrades live innetworkUpgradeSummaryData.tsthen it may never be used — and the rest of anUpgradeDatapayload stops meaning anything once an upgrade ships. A settled activation date needs noconfirmedflag, milestones become history rather than expectations, andfacts-verifiedhas nothing left to verify.So
activatedis either the handoff state at which a file stops being maintained — reached once, then frozen or deleted — or it should not exist. This is worth settling before anything writes to these files: it determines what a future scheduled sync does the day an upgrade ships, and "keep updating a file whose every field has become meaningless" is the wrong default to arrive at by accident.Is there an ACD source for "Q4 2026"? Update Glamsterdam page to match devnet-8 scope #18990 introduced that wording to the page copy earlier today. If there's a confirmation I couldn't find, this is a one-line change to the data file. Otherwise I'd suggest the page copy follow the data rather than the other way round — which is the whole point of having the data layer.
Does
facts-verifiedsurvive contact with a scheduled sync? Genuine uncertainty rather than a proposal — I don't have a good answer.Today the field means someone confirmed these values against Forkcast on that date, which works while a human maintains the file. Under a scheduled job it gets awkward. If the job checks weekly and nothing has changed, the facts genuinely were verified, so the stamp should move. But bumping it produces a PR every week containing nothing but a date change, which defeats the "only open a PR when something actually changed" rule that makes such a job tolerable in the first place.
So either the stamp goes stale while verification is demonstrably happening, or it generates weekly noise. Possibly the field doesn't belong in a bot-maintained file at all and belongs in the job's own output — a run log or a status endpoint — with the page reading it from there. Raising it now because it is cheap to change before merge and awkward to change after.
This has already happened once, during review.
facts-verifiedwas bumped from2026-08-06to2026-08-10in a commit that changed nothing else, because the values were re-checked against Forkcast and none of them had moved. That is the whole problem in miniature: the verification was real and worth recording, and the commit still contains only a date. A weekly job would produce that same commit indefinitely.Related Issue
N/A — follow-up to the content drift addressed in #18990.