Founder Lessons

What a design-system engine finds in a Claude Design export

Timurtek

• 6 min read

I had three Claude Design exports on disk: FakeOrders, PainSense and AstroSense. I wanted to know what a design-system engine would say about them, so I built an adapter and ran Zengin over all three.

The command is zengin init --from claude-design. It reads the export and writes a design-system config. Tokens come from the stylesheets. Components come from the type files. Then zengin check runs the rules against the code.

The method was four steps: run, classify every kind of finding, fix each defect with a regression test, run again. After the fixes the second run found 137 findings in FakeOrders, 162 in PainSense and 26 in AstroSense, and a script in the repo reproduces those counts.

Every finding fell into one of three buckets: engine defects, adapter defects and policy gaps. In the reproducible run, the engine defects sat in the suggested fixes. Three detection errors from an earlier run are covered separately below. The run also never exercised the component rules on the real kits.

What the export hands you

Each of these three exports ships a manifest, _ds_manifest.json, with a list of components, a list of tokens, and the stylesheets to read. Each component has a type file next to it. FakeOrders also shipped a lint config, _adherence.oxlintrc.json, that I had hand-written before Zengin existed.

The first question was whether this shape holds from one export to the next, or only for the first one I looked at. If it was per-project, I would be writing a converter for one design system. So before writing the adapter I diffed the manifests of all three. The shape held: a namespace, components with a source path, the stylesheet list. The contents did not. FakeOrders has no named theme, PainSense has one called night, AstroSense has a light theme over a dark default.

The method

Four steps, and I'd run them the same way on any system.

  1. Run the engine on the real export and write down the count.
  2. Classify every kind of finding, not just the total.
  3. Fix each defect with a regression test.
  4. Run again and compare.

"Every kind" took some negotiating. I had asked for the findings to be classified one by one, by hand. The builder agent changed that to one classification per distinct kind, with a note in its delivery, but it did not ask me first. The auditor failed it for that. I ended up relaxing the criterion myself. The report now classifies 158 kinds and opens a sample from each: the first, the middle and the last finding. That was 235 findings opened by hand, and none of the sampled findings contradicted the classification of its kind. The rest were not opened one by one.

How the findings split

| Bucket | What it is | Example from this run | |---|---|---| | Engine defect | The rule engine gave a wrong answer | Spacing fixes named a token that is named for a use, like --safe-top, instead of the scale step. 61 of them | | Adapter defect | The adapter read the export wrong | --fs-*, --r-*, --ls-* and --bw* variables read as spacing | | Policy gap | The engine is right and the policy is open | The system's own components are checked for literal values, and they have some |

In the reproducible run the engine defects were in the suggested fixes, so fixing them moved no count on FakeOrders. Ten color fixes told a translucent rgba() it matched the opaque token as an exact match. A token's alpha was ignored when matching #fff, so #fff was told to use --line, which is 8% white. One earlier defect was in detection: oklch(var(--l) var(--c) 145) was read as a color literal. It is fixed, and its three findings are among those withdrawn below.

The adapter had a similar flavour. RGB-triplet variables such as --fg: 226,224,247 were not recognised.

The policy gap is the one I find most useful. Should the system's own components be held to the system's scale? I left them in, so the check flags literals inside components/**: 23 findings in FakeOrders and 85 in PainSense. I count that as a true finding. A design system that does not use its own tokens has told you something. But it is a decision, and the engine should not make it for you.

Declared kind is not a classifier

The manifest labels every token with a kind. I didn't trust it. --fs-display: 40px is labelled spacing. It is a font size. --text-strong is labelled font. It is a color.

So the adapter classifies from the value first and the name second, and uses the manifest's kind only to report disagreement. It disagreed with that classification on 25 variables in FakeOrders, 25 in PainSense and 13 in AstroSense.

Aliases bit too. --brand points to var(--coral-500) which points to #FF4D3D. If value matching does not walk the chain, a hard-coded #FF4D3D never gets told about --brand. That was the same wall the hand-written lint config hit: it can flag a hex color, but it cannot say which token to use, and it cannot see rgb() or oklch() at all.

Two sources, one disagreement kind

The components have a second contract besides the type files: the lint config. I cross-checked them and reported every disagreement without resolving any. The count was 2 in FakeOrders, 3 in PainSense and 5 in AstroSense. All of them were about coverage, a component with no contract, or a contract for a component the manifest does not list. None were prop-level.

I would say that is weak evidence. Both sources come from the same tool, so agreement between them is not a second opinion.

What I could not see

Component rules did not run on the real kits. The kits get their components from const { X } = window.<namespace>, not from an import. The rules that check unknown props, unknown prop values, class names and component substitution count zero on all three exports. Zero here means the rule never ran, so that code is unchecked. Those rules only ran end to end against a synthetic export. This is the biggest gap, and I think the next packet is a source that reads the window pattern.

One theme. FakeOrders has no named theme, so that run exercised one. PainSense and AstroSense touched theme handling, but nothing here says the engine copes with a system that ships several themes. The adapter rejects a manifest with several named themes, with an error, rather than merging them.

Eleven findings withdrawn. The first run reported eleven findings I could not reproduce afterwards, three in PainSense (the oklch(var(...)) detection error) and eight in AstroSense (Unknown token --fg), because I had not kept the builds from before the fixes. They are written up as two fixed defects with tests, and the per-export counts above are the ones the repo script reproduces.

Tests that never failed. I did not run the new engine tests against the unfixed engine to watch them fail, except one, the spacing tie-break, which is built so the old code picks the wrong token. A test you have never seen fail may not be testing anything. The containment tests for the adapter's file writes I did watch fail with the fix removed.

Released, and new. The adapter is in @zenginui/adapter-css 0.6.0 with @zenginui/cli 0.10.0, both on npm since Oct 15, after the auditor accepted it on Oct 12 following four fix passes. It has run on three exports, all mine. Treat its findings as a first pass, and tell me where it is wrong.

What to check before a model-made design reaches your code

The list I would carry to any system:

  • Don't trust declared kinds. Classify tokens from the value.
  • Walk the alias chain to the real value.
  • If you have two contract sources, compare them and report the gap. Don't pick a winner.
  • For every rule, check that it ran on your export. A zero count is a question.
  • Check the system's own components against its own scale.
  • Fix each defect with a test, and watch the test fail first.

The part that takes judgment is the policy gap, because that is where the engine is right and you still have to decide.

The Zengin project page is here.

Design systemsField tests
Share

Enjoyed this? Get more like it.

I send a weekly briefing on technical strategy, high-end design, and operational excellence.

The Production Layer

Operator notes from building with AI in the loop: what shipped, what broke, and what I would do again.

About Timurtek

Design engineer and AI product builder. Inside product teams, shipping the products and AI systems engineers actually run. Previously at Disney, Apple, and a long list of startups.

Read full bio →