Skip to content

Add upgrade status data layer for roadmap pages - #18991

Closed
nloureiro wants to merge 7 commits into
devfrom
roadmap-upgrade-data-layer
Closed

Add upgrade status data layer for roadmap pages#18991
nloureiro wants to merge 7 commits into
devfrom
roadmap-upgrade-data-layer

Conversation

@nloureiro

@nloureiro nloureiro commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Base branch: dev. This is PR 1 of a stack of four. Later PRs target each other, not dev — see the stack below.

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:

  • Everything stale was a volatile fact — the mainnet target, testnet milestones, EIP inclusion status.
  • Everything correct was prose — the ePBS and BAL explainers, the FAQ, the page structure.

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.tsUpgradePhase, MilestoneStatus, EipStatus, PartialDate, and the UpgradeData / Milestone / UpgradeEip / MainnetTarget shapes.
  • 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 the src/data/{apps,quizzes,topics}/index.ts pattern.
  • 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/data

docs/stack.md describes /src/data as "general data files importable by components", which is exactly what this is. It sits alongside community-meetups.json, developer-docs-links.yaml, and — most directly comparable — networkUpgradeSummaryData.ts, which already stores activation dates for shipped forks.

Why .ts and not .json

Initially specced as JSON. That doesn't work: TypeScript widens JSON string values to string, so satisfies UpgradeData fails on correct data:

error TS1360: Type '{ phase: string; ... }' does not satisfy the expected type 'UpgradeData'.
  Types of property 'phase' are incompatible.
    Type 'string' is not assignable to type 'UpgradePhase'.

declare module would type the import as UpgradeData, but that asserts rather than checks — "phase": "devnetX" would sail through. That's the appearance of safety with none of it.

.ts with satisfies gives a real build-time guarantee, and matches how every other typed data file here is written (developerTools.ts, wallet-data.ts, quizzes/index.ts all use satisfies on an inline literal; none of the 12 JSON imports in src/ carry a type).

Verified by breaking each enum in turn and running pnpm type-check:

src/data/upgrades/glamsterdam.ts(9,3):  error TS2820: Type '"devnets"' is not assignable to type 'UpgradePhase'. Did you mean '"devnet"'?
src/data/upgrades/glamsterdam.ts(24,7): error TS2322: Type '"probably"' is not assignable to type 'MilestoneStatus'.
src/data/upgrades/glamsterdam.ts(35,17): error TS2322: Type '"scheduled"' is not assignable to type 'EipStatus'.

A typo'd status now fails CI instead of quietly rendering.

Dates carry only the precision that has a source

when is a single field whose precision is whatever keys are present — { year }, { year, month }, or { year, month, day }. It replaces an earlier window string plus a separate ISO date. 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 through dateTimeFormat() instead of rendering as English inside a translated page.

The union shape does real work — both of these are compile errors:

error TS2322: Type '{ year: number; day: number; }' is not assignable to type 'PartialDate'.
error TS2353: Object literal may only specify known properties, and 'quarter' does not exist in type 'PartialDate'.

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/H2 string anywhere in English content or src/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: MilestoneStatus is ordered liveconfirmedanticipatedprojected, and mainnet-target carries an explicit confirmed boolean. The UI cannot overclaim, because the data won't let it — a projected date has to render with a qualifier.

Why two date stamps

  • "Page last updated" — git-derived, already rendered in the hero for template: upgrade pages. 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.window is "2026", not "Q4 2026". See the section below.
  • "Plataberget" is devnet-8, not a testnet. It's a short-lived fork-transition devnet, and as of today it hadn't launched (Forkcast targets early August, exact date deferred to ACDT call 90). Recorded as anticipated, not live.
  • No testnet milestones. Forkcast has no scheduled Glamsterdam testnet forks, and the meta EIP's own activation table is still empty. Rather than invent a window, they're omitted until there's a date. Note the meta EIP's table lists Holešky, which is stale — Holešky was shut down after Fusaka, and this repo's own docs already name Sepolia and Hoodi as the maintained testnets. When those forks get dates they go in as Hoodi.

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 sfi per 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 as sfi with no networking status and no category field, 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.

  • The Q4 figure traces to an X post by SSV Network, a staking provider. Ethereum has not confirmed a mainnet date.
  • ethereum.org's own official roadmap framing elsewhere is the broader H2 2026 window.
  • The projections disagree with each other. eipsinsight projects 2026-09-16 — that's Q3, not Q4 — and is explicit that only ACD-confirmed dates are marked as confirmed. Everstake still cites end of August 2026 as the internal working target. Datawallet puts the base case at September to December.
  • Forkcast's year-only value is editorial restraint, not a schema limitation. Forkcast records exact dates when they exist (it has them for Pectra and Fusaka down to the slot). For Glamsterdam it publishes 2026 because that is the most specific defensible claim.
  • Rendering an unconfirmed quarter as settled is precisely the failure this data layer exists to prevent. If we can't cite an ACD source, the schema should not let the UI imply one.

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-verified date, 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 milestones and become the next milestone automatically, with no prose edit.

Follow-up stack: four PRs, not six

PR Branch Base What
1 roadmap-upgrade-data-layer dev This PR — data only, no visual change
2 roadmap-upgrade-status PR 1 Status block, rendered by TopicLayout
3 roadmap-eip-chips PR 2 EIP inclusion chips driven by eips[].status
4 roadmap-hegota-page PR 2 Hegotá record + a thin planning-phase page

Two further PRs were planned and have been deliberately dropped, because building them requires answering question 2 below rather than guessing at it:

  • A /roadmap/ upgrade-sequence refactor, pointing the existing release carousel at this data layer.
  • Making the technical-history page read activation dates from here.

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 at 2025-12-03 with 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

  1. 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, so src/data/roadmap/upgrades/ may be the more consistent home. I defaulted to the top level because src/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.ts already 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/upgrades is 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.ts is 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.
    • The actual overlap is one field — the activation date. Everything else is disjoint: no confidence model, milestones, EIP statuses, or verification stamp exists on the historical side, and no ETH price or Wayback link belongs on the forward-looking side.
    • This question gates the two dropped PRs. If isPending is the intended mechanism, the answer is clean: the release carousel and the history page read from networkUpgradeSummaryData.ts, src/data/upgrades holds 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.
  2. 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.

  3. 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.

  4. Does NetworkUpgradeDetails.isPending point at a better design for the handoff? That type has an isPending: true variant which forbids ethPriceInUSD and waybackLink — 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 of src/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.

  5. Should UpgradePhase have an activated value at all? It's in the union, but if shipped upgrades live in networkUpgradeSummaryData.ts then it may never be used — and the rest of an UpgradeData payload stops meaning anything once an upgrade ships. A settled activation date needs no confirmed flag, milestones become history rather than expectations, and facts-verified has nothing left to verify.

    So activated is 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.

  6. 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.

  7. Does facts-verified survive 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-verified was bumped from 2026-08-06 to 2026-08-10 in 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.

@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for ethereumorg ready!

Name Link
🔨 Latest commit 8cf70cf
🔍 Latest deploy log https://app.netlify.com/projects/ethereumorg/deploys/6a79cb7e430ecb0008995be2
😎 Deploy Preview https://deploy-preview-18991.ethereum.it
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
7 paths audited
Performance: 58 (🟢 up 1 from production)
Accessibility: 95 (no change from production)
Best Practices: 100 (🟢 up 1 from production)
SEO: 98 (no change from production)
PWA: 60 (no change from production)
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@nloureiro
nloureiro marked this pull request as ready for review August 6, 2026 15:41
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🔎 First-pass review — ✅ Looks mergeable

Data-only PR (lane: data) adding a typed upgrade-status layer under src/data/upgrades/ — nothing imports it yet, and the green Chromatic run confirms the preview is identical to dev. Type-check/lint pass, the satisfies UpgradeData approach matches the repo convention (quizzes/index.ts, wallet-data.ts), and the enum-narrowing rationale is sound. The Glamsterdam record is internally consistent (11 EIPs in page order, all sfi; year-only mainnet target; no invented testnet milestones). No blocking items.

One cheap cleanup worth doing now, before five PRs depend on these types:

  • EipStatus has a redundant member. types.ts:178 declares "pfi" | "cfi" | "sfi" | "dfi" | "declined", but the doc comment right above it defines dfi as declined — so "declined" is a second, undocumented way to say the same thing. Two synonyms in a confidence enum is exactly the "enums rot" failure the PR argues against elsewhere. Suggest dropping "declined" and keeping dfi.
Analysis

Lane: data (all four files under src/data/upgrades/). Held to full convention depth as a team PR.

Checked: placement/shape vs neighbouring src/data/*/index.ts barrels (matches); satisfies vs declare module reasoning (correct — satisfies is the real build-time check); PartialDate union correctly makes day-without-month a type error; data values trace to Forkcast/EIP-7773 per the description; base branch dev is correct.

Non-blocking nit: MilestoneStatus (types.ts:158) doc says "ordered strongest→weakest evidence", but complete sits last, after the weakest (projected). complete isn't weaker than projected, so the "ordered" claim is slightly off — either reword or move complete first.

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, isPending for Phase 6, Q4 source) are design calls for a human reviewer, not first-pass blockers — routing to dev accordingly.

Generated by PR Reviewer (team) for #18991 · 78.7 AIC · ⌖ 27.8 AIC · ⊞ 5.2K ·

@nloureiro

Copy link
Copy Markdown
Contributor Author

Both fixed. Dropped declined from EipStatus in bedbd58 after confirming nothing referenced it as a value — STATUS_LABELS in #18998 is a Partial<Record<EipStatus, …>> covering only sfi/cfi/pfi. Good catch, it was exactly the failure mode this PR argues against elsewhere.

MilestoneStatus reordered in 8b35880 rather than reworded: complete moved to the front, since the only distinction any consumer makes is complete vs not. Same bug was in the README and is fixed there too.

@pettinarip

Copy link
Copy Markdown
Member

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 types.ts survives largely intact — it's the origin of the contract in #19059. What changed is the author of the records, not the schema.

What we found

Digging into your open questions turned up one thing that reframes the problem. Forkcast publishes the Glamsterdam target in two places:

file field value
src/data/upgrades.ts activationDate 2026
src/constants/timeline-phases.ts mainnet-deployment.projectedDate Q4 2026 — since 2025-11-15

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 upgrades.ts alone suggests.

That also means the "deliberately no quarter granularity" decision needs reversing — PartialDate now has a quarter, because dropping it made our store less precise than its own source. Quarter ranges (Q3-Q4 2026) still degrade to the year, which is the part of your argument that holds.

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 changes

Same 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 glamsterdam.ts + hegota.ts implies a new hand-authored file per fork forever.

Answering your other open questions from that work:

  • Q1 (right home?) Still open, carried over to Derive network upgrade facts from Forkcast #19059. Worth settling before anything imports it.
  • Q4/Q5 (isPending, should activated exist?) Forkcast already models the post-activation state as Included, and its upgrade status is Live | Upcoming | Planning | Research. Derive network upgrade facts from Forkcast #19059 mirrors that rather than inventing a parallel vocabulary. isPending is separately dead — zero entries use it — and I've written up why in a follow-up refactor note.
  • Q7 (does facts-verified survive a scheduled sync?) Your instinct was right that it doesn't. It's removed: under a job it either goes stale while verification demonstrably happens, or it generates a weekly commit containing only a date. Provenance moves to the job's run log and PR body.
  • Q2 (second date stamp?) Moot, given the above.
  • Q3 (i18n on the label set) Still the right question, and it now applies to Consolidate roadmap status, EIP chips, and Hegota page #18993's 7 strings rather than anything here — Derive network upgrade facts from Forkcast #19059 adds no user-facing strings at all.

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 UpgradeSummary from TopicLayout keyed off the slug, so every locale gets it without propagating a tag into 24 translated files, is exactly right and I dropped my MDX-based version in favour of it. The · not confirmed qualifier as its own clause rather than an adjective is a good i18n call I hadn't thought of.

They will need a rebase and a small adaptation once #19059 lands. Concretely, in #18993:

  • upgrade.phaseupgrade.status (values are now live | upcoming | planning | research)
  • upgrade["mainnet-target"]upgrade.mainnetTarget
  • upgrade["source-url"]upgrade.sourceUrl
  • drop the facts-verified row — the field is gone
  • .find(m => m.status !== "complete") should also exclude "live", or it returns the currently-running devnet rather than what's next
  • formatPartialDate needs a quarter branch. This is the one non-mechanical item: Intl.DateTimeFormat has no quarter skeleton, so Q4 2026 needs a translated pattern ("Q{quarter} {year}") and the formatter can't stay a pure util. Without it your block renders 2026 instead of Q4 2026. Happy to hand you a working version — I built and then removed one.

And in #18998: STATUS_LABELS rekeys from sfi/cfi/pfi to scheduled/included. Note the store now holds only in-scope EIPs, so your cfi/pfi branches would be unreachable — declined EIPs have no entry at all rather than a status, which matches your "declined renders nothing" decision.

In #19000: src/data/upgrades/hegota.ts becomes unnecessary — Hegotá is generated, at Q2 2027. The page, nav and carousel entries are all still wanted.

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.

@pettinarip pettinarip closed this Aug 17, 2026
@github-actions github-actions Bot added the abandoned This has been abandoned or will not be implemented label Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

abandoned This has been abandoned or will not be implemented

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants