AI DESIGN SYSTEMS
Round-Trip Parity on a Code-First Design System
Key Tools & Techniques
- Astryx Meta
- Pencil.dev
- Claude Code Skills
- Chrome DevTools Protocol
- Held-out Fixtures

01
Challenge
Astryx is Meta’s largest design system, open-sourced in June 2026: 149 components, and no official Figma kit. That is a deliberate position, not an oversight — the system is code-first, the repository is the source of truth, and there is no designer-facing token sync. It also means a designer has nowhere to draw. A screen either starts in code, or it starts in a design file nobody can prove matches the system.
The reflex is to rebuild the library by hand: 149 components, redrawn, stale by the next release. The bridge belongs at the token layer, not the component layer. Tokens are a small, enumerable set, and they are what actually decides whether a screen looks like the system.
The second choice was the design tool. Pencil.dev, because a .pen file lives in the repository next to the code — one agent, one loop, no handoff between two applications that cannot see each other.
02
What a Missing Design Kit Actually Costs
A code-first system without a design surface does not fail loudly. It fails by routing every visual decision through whoever can read the repository, and by making fidelity a matter of opinion. Left alone, the split produces four things:
None of these are visible in a design review. They are measurement gaps — the design and the code look close enough, and nobody can say by how much.
03
The Idea: Bridge at the Token Layer
A component-level bridge asks: can I redraw the library? A token-level bridge asks a smaller and far more durable question: does this design paint values the system actually defines?
That turns a design file into something testable. The token set is finite, the running app resolves it, and the .pen file is text in the same repository — so one agent can read both sides of the loop, and a gate can score it on every change.
HOW THE LOOP RUNS
01
Round trip
both directions · 66/68 token parity
DESIGN → CODE
measured — 96.5% node-weighted
.pen design
›
Resolved token scale
›
Astryx screen
CODE → DESIGN
same gate — 92.7% → 100%
Astryx screen
›
getComputedStyle
›
.pen mirror
02
Measurement
three lenses
One number is gameable: a screen painted with one safe token everywhere scores fully on-system and looks nothing like the design. So every lens reports its numbers side by side — conformance, what the screen actually paints, and, on the design side, whether a value is bound to a token or typed in by hand.
03
Falsification
broken fixture
A page built entirely on hardcoded values ships with the harness, permanently broken on purpose. Every run proves the gate can still fail — 39 of 39 self-tests green, plus 15 of 15 on the write path, including break-restore on a real design file.
↑ every confirmed drift feeds back into the skill
HOW THE LOOP RUNS
01
Round trip
both directions · 66/68 token parity
DESIGN → CODE
measured — 96.5% node-weighted
.pen design
›
Resolved token scale
›
Astryx screen
CODE → DESIGN
same gate — 92.7% → 100%
Astryx screen
›
getComputedStyle
›
.pen mirror
02
Measurement
three lenses
One number is gameable: a screen painted with one safe token everywhere scores fully on-system and looks nothing like the design. So every lens reports its numbers side by side — conformance, what the screen actually paints, and, on the design side, whether a value is bound to a token or typed in by hand.
03
Falsification
broken fixture
A page built entirely on hardcoded values ships with the harness, permanently broken on purpose. Every run proves the gate can still fail — 39 of 39 self-tests green, plus 15 of 15 on the write path, including break-restore on a real design file.
↑ every confirmed drift feeds back into the skill
HOW THE LOOP RUNS
01
Round trip
both directions · 66/68 token parity
DESIGN → CODE
measured — 96.5% node-weighted
.pen design
›
Resolved token scale
›
Astryx screen
CODE → DESIGN
same gate — 92.7% → 100%
Astryx screen
›
getComputedStyle
›
.pen mirror
02
Measurement
three lenses
One number is gameable: a screen painted with one safe token everywhere scores fully on-system and looks nothing like the design. So every lens reports its numbers side by side — conformance, what the screen actually paints, and, on the design side, whether a value is bound to a token or typed in by hand.
03
Falsification
broken fixture
A page built entirely on hardcoded values ships with the harness, permanently broken on purpose. Every run proves the gate can still fail — 39 of 39 self-tests green, plus 15 of 15 on the write path, including break-restore on a real design file.
↑ every confirmed drift feeds back into the skill
04
When the Measurement Lies
This is the part that reshaped the work, and the part worth stealing. Four separate times, a measurement that looked correct was measuring the wrong thing.
01
The wrong source
packages/core/dist → #0064E0
running app → #262626
The first drift was a false alarm. The comparison read base tokens out of the built package, not the running app with the theme applied.
02
A value that is not a value
light-dark(a, b)
Tokens resolve only at the point of use. Read as-is, a custom property is a function call, not a color — all 14 would have reported as drift.
03
A green gate, wrong object
hardcoded page 97.1%
system-driven 97.1%
62 of 68 checks read the app’s global stylesheets, not the page under test. A page of pure hardcoded values scored the same as the real one.
04
A measure that rewards a copy
copied frame → 87.5%
design work → none
In the reverse direction both runs started by copying the frame that already mirrored the screen, so the lens paid 87.5% for no design work at all. A flaw in my prompt, not in the agents — and it narrows the claim to what the pair really measures.
01
The wrong source
packages/core/dist → #0064E0
running app → #262626
The first drift was a false alarm. The comparison read base tokens out of the built package, not the running app with the theme applied.
02
A value that is not a value
light-dark(a, b)
Tokens resolve only at the point of use. Read as-is, a custom property is a function call, not a color — all 14 would have reported as drift.
03
A green gate, wrong object
hardcoded page 97.1%
system-driven 97.1%
62 of 68 checks read the app’s global stylesheets, not the page under test. A page of pure hardcoded values scored the same as the real one.
04
A measure that rewards a copy
copied frame → 87.5%
design work → none
In the reverse direction both runs started by copying the frame that already mirrored the screen, so the lens paid 87.5% for no design work at all. A flaw in my prompt, not in the agents — and it narrows the claim to what the pair really measures.
01
The wrong source
packages/core/dist → #0064E0
running app → #262626
The first drift was a false alarm. The comparison read base tokens out of the built package, not the running app with the theme applied.
02
A value that is not a value
light-dark(a, b)
Tokens resolve only at the point of use. Read as-is, a custom property is a function call, not a color — all 14 would have reported as drift.
03
A green gate, wrong object
hardcoded page 97.1%
system-driven 97.1%
62 of 68 checks read the app’s global stylesheets, not the page under test. A page of pure hardcoded values scored the same as the real one.
04
A measure that rewards a copy
copied frame → 87.5%
design work → none
In the reverse direction both runs started by copying the frame that already mirrored the screen, so the lens paid 87.5% for no design work at all. A flaw in my prompt, not in the agents — and it narrows the claim to what the pair really measures.
The third one is the dangerous one. A failing check gets investigated; a passing check that measures the wrong object gets trusted — the gate was green, and it was answering a different question than the one being asked of it.
So the harness grew a second lens — one that scores the values a single page actually paints — and later a third for the design file itself. A deliberately broken page now ships with them as a permanent fixture. Both pages below score the same on the first lens. Only one of them is the real screen.
05
Measured, Not Assumed
With a trustworthy instrument, the question becomes worth asking: what does a skill actually buy? The same design, the same prompt, the same model and effort, a fresh context in both runs. The only difference was that the second agent was told to read and apply the skill.
The numbers were read back from the gate, not from the agents’ own reports — and with an assertion that the instrument itself was untouched, because the skilled run edited the token map mid-run and reverted it. A subagent’s report is not evidence.
The whole delta is carried by one nameable thing: geometry outside the scale. The design file itself carries cornerRadius 999 on 40 nodes, and the unskilled run copied it faithfully into 160 nodes of code — the system’s --radius-full is 9999px, one digit apart — plus 2px on 52 more and 8px on 8. The skilled run emitted none of them: it read the real radius scale off the running app and snapped the off-scale values to it. The drift was inherited, not invented, which is the more uncomfortable finding — a design file is what the next screen gets built from.
The same experiment then ran the other way — mirroring a live screen back into the design file, with the skill again as the only variable. Both tables below are read off the same harness. The 100% in the second one is not a saturated gate: the same run binds only 92% of its values to tokens, which is exactly why each lens reports more than one number.
WHAT THE DESIGN FILE CARRIED
999px
on 40 nodes in .pen, copied into 160 nodes of code
WHAT THE SYSTEM DEFINES
9999px
one digit apart — 220 off-scale values, gone with the skill
WHAT THE DESIGN FILE CARRIED
999px
on 40 nodes in .pen, copied into 160 nodes of code
WHAT THE SYSTEM DEFINES
9999px
one digit apart — 220 off-scale values, gone with the skill
WHAT THE DESIGN FILE CARRIED
999px
on 40 nodes in .pen, copied into 160 nodes of code
WHAT THE SYSTEM DEFINES
9999px
one digit apart — 220 off-scale values, gone with the skill
| Design → code · same screen, same prompt | Without the skill | With the skill | Delta |
|---|---|---|---|
| Token conformance | 82.0% | 88.8% | +6.8 pp |
| Node-weighted conformance | 78.8% | 96.5% | +17.7 pp |
| Design tokens painted (of 34) | 29 | 30 | +1 |
| Off-scale geometry values emitted | 220 | 0 | −220 |
| Code → design · same starting frame, same prompt | Without the skill | With the skill | Delta |
|---|---|---|---|
| Scale conformance | 87.5% | 100% | +12.5 pp |
| Node-weighted conformance | 92.7% | 100% | +7.3 pp |
| Values bound to a token | 89.6% | 92.0% | +2.4 pp |
| Screen values covered (of 29) | 23 | 27 | +4 |
| Off-scale values left in the design | 5 | 0 | −5 |
06
What the Skill Did Not Fix
A case study that only reports wins is not a measurement. Four things are worth naming, and each of them makes the rest more credible, not less.
The baseline was hard, not straw.
The repository auto-injects Astryx’s own agent documentation, which already says “tokens for every value, no raw hex or px.” Taking the docs away from the unskilled agent was not physically possible, and would have been dishonest. That is why the baseline starts at 82%, not 20% — the delta is smaller and it survives “any model would do that.”
Two colors survived both runs.
#0B991F and #8494A3 are literals inside the .pen file, not token references, and the running app resolves nothing matching them — no --color-data-* family exists. Both agents in the first pair reproduced them faithfully, so they do not separate those two runs. The reverse-direction run under the skill later replaced them with declared tokens, which is where that rule stopped being a hypothesis.
The ceiling is not 100%.
The charting library paints its own defaults — 11px spacing, #0074e2 — and they score as off-system because that is exactly what they are.
The write path is evidence, not proof.
The .pen format turned out to be canonical JSON — parse and re-serialize reproduces a 190KB file byte for byte — so writing it needs no vendor tool, and a validator rejects duplicate ids, undeclared tokens and dangling references. What was never confirmed is that the design application opens a file written that way.
Both directions are measured here, on the same harness. What is not measured is a from-scratch draw with no existing frame to copy from — that pair is still to run.
07
What This Means for a Design Team
The transferable part is not Astryx, and it is not Pencil. It is that “does this design match the system?” can be a number instead of an opinion — and that the number is worth exactly what its instrument is worth.
Written as a practice
Bridge at the smallest shared layer
tokens are enumerable and cheap to keep in sync; component libraries are neither
Measure against the running product
built packages, design files and documentation each answer the token question wrong, in a different way
Make the gate fail on purpose
a check that has never failed is a check nobody has tested
Report what did not move
the findings a measurement refuses to confirm are what make the confirmed ones worth reading
Carry the one fact an agent cannot look up
the delta came from a single sentence about which layer wins, not from tone, length or encouragement
State the rule as a decision, not a list
“a value that exists nowhere on the scale is a finding, not a requirement” catches 999px; an enumerated list of allowed radii never would
A design system that lives in code is not a system designers are locked out of. It is a system where the question of fidelity finally has an answer you can run, on every change, instead of arguing about it in review.
08
Key Takeaways
A code-first design system does not need its component library redrawn. It needs a bridge at the token layer — small, enumerable, and testable.
Same prompt, same model, both directions: 78.8% → 96.5% one way and 92.7% → 100% the other. The whole delta is geometry outside the system's scale.
Four times the measurement lied, and the worst one was green — a page built entirely on hardcoded values scored an identical 97.1%.