Keeping a spoke current
The hub renames things. Your spoke does not break when it happens — which is exactly the problem, because nothing tells you either. This is how you find out what has drifted and move off it, in your own repo, on your own schedule.
Why a rename reaches you with no warning
A spoke does not install a copy of the design system. It points at the hub checkout
through a file: symlink, so a rename in the hub lands in your spoke on
the next dev tick — there is no version bump and no publish step to absorb it.
The hub keeps old names working as compatibility aliases, so nothing looks broken. That is the trap: a spoke can sit on retired names for months and only find out when the aliases are finally deleted. Worse, an alias only covers one of the two ways you can use a token — see below.
The one thing to understand: reads vs declarations
This is the whole reason the tooling is more than a find-and-replace, and the single most common way a spoke is silently wrong.
| What you wrote | What it is | After a hub rename |
|---|---|---|
color: var(--old-name) | a read | ✅ Rescued by the alias. Renders exactly as before. |
--old-name: #1e5386; | a declaration | ❌ Inert. Nothing reads that name any more, so your override does nothing. You get the hub default, and no error says so. |
An alias points old → new. It rescues you when you read the old name. It can never rescue you when you declare it, because the components are reading the new name and your theme file is setting the old one. Those two never meet.
Your theme file is nothing but declarations. That means the file where you set your brand is the file most exposed to this — and the failure looks like "the design system just doesn't apply our colour", not like an error.
Step 1 — find out where you stand
npm run doctor Two separate warnings matter here, and they mean different things:
| Warning | Severity |
|---|---|
no deprecated @esa/tokens names in src/ | Renders correctly today via aliases. Fix on your own schedule, before the hub drops them. |
no deprecated names DECLARED in src/ | Already wrong. These overrides are inert right now. Fix first. |
Step 2 — preview the change
node ../ecology/scripts/migrate-tokens.mjs Nothing is written. It groups every rewrite by the rename that caused it, and calls out four things you should not skim past:
-
⚠ NOT an exact alias— that rule changes rendering, not just the name. Read the reason it prints and look at those call sites afterwards. -
✖ NOT REWRITTEN — replacement is itself gone— the hub renamed something and then deleted the destination. The tool refuses to swap one dead name for another and shows you the chain. These need a decision, not a rename: ask what replaced the capability, because the answer is usually a different CSS property rather than a new token. -
✖ REMOVED— deleted with no equivalent. Reads of these resolve to nothing today; whatever literal fallback you wrote is what you are actually seeing. -
⚠ declared here, not just read— the inert overrides from the section above.
If it refuses to run
A collapse collision means the hub merged two names into one, and your spoke gave those two names different values. The merge was safe in the hub because the values were identical there; here the distinction is real. Rewriting both would emit the same property twice and silently drop one value, so nothing is written until you decide which one survives. Delete the loser from your theme file and re-run.
Step 3 — apply, then look at it
git status --porcelain # must be clean — this edits in place
node ../ecology/scripts/migrate-tokens.mjs --write
npm run build For every rename except the ones flagged inexact, the correct result is no visible difference at all — the old name and the new one resolve to the same value, which is what made the rename safe. Load the pages that changed most and confirm that is what you see. A visible change on an "exact" rule means something else is going on; say so rather than accepting it.
In Claude Code, /update-tokens wraps all three steps and explains the output in
plain language — useful if the person doing this does not read CSS.
What the hub guarantees in return
None of this works if the hub can drop a name without saying so, and until
2026-08-14 nothing stopped it. Every mechanism above — the aliases, the codemod,
the doctor warnings — starts from a row in migrations.json,
and nothing checked the row had been written. Three guards now enforce it:
- A name cannot disappear silently. The hub commits a baseline of every token name it ships; its own test suite fails if one vanishes without a migration row. Forgetting is a build failure, not a thing to remember.
- A rename cannot point at a dead name. The destination has to resolve to a real value, not merely exist — an alias aimed at a deleted token is itself declared and still resolves to nothing. That case shipped once and reached two spokes before this guard existed.
- Additions are always free. New tokens never trip anything. A spoke cannot be broken by a name it has never used.
So the deal is: the hub owes you a declared, resolvable path off every name it retires. You owe it a run of the migration before the aliases are dropped.
When to do this
There is no schedule and no forced upgrade. Reasonable triggers: before you ship anything
real, when doctor reports a declared deprecated name (that one is not
cosmetic), and whenever the hub announces it is about to drop aliases. Aliases are a bridge,
not a fixture — they exist so you can cross on your own schedule, not so you never have to.
If the migration tells you a capability is gone and you genuinely need it, that is a
/request-lego against the hub. A missing hook is a conversation, not something
to work around locally.