open-ds-bench · case study · Aurora

We taught the agents about Aurora. They weren't listening.

Aurora shipped with everything an AI agent is supposed to need: a rulebook, generated documentation, a component catalog, even a bundle for agents with no network access. We had never sat down and watched an agent use any of it. When we finally did, some of what we found had less to do with the models than with what we had been telling them.

64.8 → 82.0AI-readiness surface score
1 → 0hard fails
6 × 2models × benchmark runs in the baseline
0components renamed

The thing we never checked

Design-system teams have settled on a fairly standard shelf of artifacts for machines: a rulebook at the root of the repository, a generated documentation index, a component catalog, a drop-in bundle for agents working offline. We had built all of it and were reasonably proud of it, and we had never checked whether any of it changed a line of what a model wrote.

As far as we can tell that is the normal position. Teams put real effort into teaching agents their system and then measure the effort rather than the result. So we borrowed two instruments and pointed them at ourselves.

The first is a static audit. Seven checks read the repository and ask what a machine would ask on arrival: is the public API reachable, are the docs findable, is the catalog any good, what enablement exists, do our names match the names models expect, can a tool read our tokens, are deprecations legible. The result is one score out of a hundred, and it takes seconds to produce.

The second is behavioral, and less comfortable to read. A fixed set of ordinary interface tasks runs through six model families at two levels of context: nothing at all, and the same tasks with our rulebook in the prompt. A grader then asks two questions of every result. Did it use our system, and does what it wrote compile against the package a consumer would install? We ran the whole matrix twice, once against our own source tree and once against the published packages, because consumers build against what we ship rather than against what we happen to have checked out.

What we saw when we looked

The audit came back at 64.8, in the tier the tool calls Invested: a repository that reads well. The behavioral composite was 44.9 against our source and 51.2 against the published packages.

Aurora read about twenty points better than it behaved, and the detail explained why. Nearly everything we had built described the system to a reader who was already looking for it. Very little of it gave an agent a reason to look.

The repo read better than it behaved

surface score (static audit) vs behavioral composite (six models) · 0 to 100

Surface score (audit) Behavioral, our own source Behavioral, shipped package 64.8 44.9 51.2 0255075100
Twenty points between how the repository reads and what agents produce with it. The audit scores what exists. The benchmark scores what six models did with it, graded by machine against the package a consumer installs.

Four findings accounted for most of the gap.

  1. 1
    A door locked from the inside

    One check failed outright. A folder of shared types was meant to be public and was not reachable at all: nothing pointed at it from the front door, and it was missing from the list of entry points the package exposes. Nobody had noticed, which is the sort of thing a static check is for.

  2. 2
    Left alone, not one model reached for the system

    Across both runs, all 113 unguided attempts used zero Aurora components. With the rulebook in context, roughly three-quarters of attempts still hand-rolled their own buttons and dialogs. Read from outside, our rulebook mostly warned models off other people's components without ever telling them to use ours. The correction turned out to be almost free, and we found it by accident: one sentence saying the package was already installed took hand-rolling down to about a fifth.

  3. 3
    The ones that engaged mostly did not compile

    Then we compiled the attempts that did use the system, against the published types rather than our own source. Ten of fifty-five passed. The couple of hundred errors clustered into five naming habits, two of which accounted for four fifths of the total. Models write the name their training expects, and Aurora calls a few of those things something else.

  4. 4
    Our own docs were teaching two of the mistakes

    This one was uncomfortable. While writing up the naming crosswalk we noticed that the rulebook, the design guide and the agent skill files all advertised two conveniences that do not exist in the library. The benchmark had already flagged both as things models commonly invent, so our own documentation was confirming a guess we should have been correcting.

Did the output use the system at all?

graded attempts · filled = used our components

Nothing in context0 of 113Rulebook in context, our source16 of 57Rulebook in context, shipped package43 of 55

Where the compile errors came from

55 engaged attempts · type-checked against the published package

one layout prop's vocabulary~100text where a number belongs99the wrong word for a heading16icons that don't exist15another library's callback name7
Two different failures, one after the other. Left: with no guidance, no model reached for the system at all; with the rulebook in context most still hand-rolled, until one sentence about the installed package flipped it. Right: the attempts that did engage walked into the same five naming habits, and two of them account for four fifths of the errors.

What we changed

The plan that fell out of the findings had eleven items across three phases. We shipped nine of them, under a rule we took from the plan itself: nothing here games a number without fixing the thing the number measures. The nine sorted into four moves.

Unlock the door

The shared types are now a published entry point of their own. The obvious fix was one line: re-export the folder from the front door and move on. It would also have been wrong. Two unrelated types in the codebase share a name, and the compiler resolves that kind of collision by exporting neither of them, so the one-line version would have passed the audit while quietly removing a type our consumers depend on. Giving the shared types their own address avoids the collision and leaves both types reachable.

A component publishes a props type under a name A folder of shared types same name, a different contract The front door one public entry point re-exported the one-line fix one name, two types → the compiler ships neither Its own entry point published separately, nothing collides what we shipped instead: a second entry in the exports map
The obvious fix would have broken a public type. Two different things share one name. A compiler resolves that collision by exporting neither, so the shared types were given an address of their own.

Make it findable, and make it generated

The documentation index already existed. It is now also written at the root of the repository, where the ecosystem has settled on looking, and it names every export of every multi-export family, so a model can find any of the eighty-nine by searching one file. One component had no documentation reach at all; it was missing from the generator entirely, so we wrote a description at the source and let it flow through the catalog into every surface downstream, which is where a fix has to go when the docs are generated. A handful of pass-through components documented nothing, honestly enough, because they forward whatever they are handed; they now document the one prop everybody passes. That last one took a failed attempt to learn, because the tidy way of writing it made the generator credit the prop to React and filter it out.

One generator an index, a full text, and a page for every component A hosted URL always the latest release · for tools that fetch the web, with no repo to read Inside the installed package version-exact · it matches the API the consumer actually installed At the repository root where the ecosystem looks · audits, crawlers, agents working in the repo
Three addresses, one source. The same generated files go wherever a given kind of reader looks, so the version an agent reads is the version it installed, and the version a tool fetches is the latest release.

The largest new asset is a machine-readable token export, generated by reading the stylesheet rather than editing it: a couple of hundred variables with their aliases intact, dark-mode and responsive overrides preserved, and the comments people had left in the CSS carried across as descriptions. On top of that sits a derived layer for the roles that live as utility classes instead of variables, which would otherwise be invisible to any tool that only looks at variables. The first line of the file says that web code still styles through the classes. We did not want it read as permission to start pasting hex values.

Say it plainly

The rulebook got the sentence it had been missing: every interactive element comes from the package, and a hand-rolled button, dialog, toggle or toast is a defect. Two lines of yes-and-no example underneath it. Then a short crosswalk of what we call things and what the published types will reject, covering the habits the benchmark had caught us on. Condensed copies are generated into the consumer-facing surfaces rather than pasted into them, so they cannot drift away from the original. A deprecation policy and two small editor entry files pointing back at the rulebook finished the move.

Delete the phantoms

The two conveniences that do not exist are gone from the seven files that had been advertising them. Both are now rows in the crosswalk, so a model that guesses at them finds the real name sitting next to the wrong one.

What moved

The audit is free and takes seconds, so we re-ran it after every phase, with the tool pinned to its baseline version so the before and after stay comparable.

Every check, before and after

baseline audit → re-run after the changes · tool pinned · grey = before, green = after

0255075100 Overall surface score64.8 → 82Export hygienehard fail65 → 100Enablement surface55 → 90Docs greppability69.3 → 100Token machine-readability66.6 → 81.6Catalog quality86.4 → 88.1Vocabulary61.4, by designDeprecation legibility40, by design
Five checks moved, two stayed put on purpose. The hard failure sat on export hygiene. The two flat rows are the ones where the honest fix was a document, not a score.
CheckBeforeAfterWhat moved it
Overall surface score64.882.0Everything below, in aggregate.
Export hygiene65 + fail100The shared types got their own entry point; the hard failure is cleared.
Docs greppability69.3100An index at the root, every export named, the last undocumented component documented.
Enablement surface5590The root index, agent bundles, editor rules, credit for fresh docs. One hint still missing.
Token machine-readability66.681.6The machine-readable token export.
Catalog quality86.488.1Exports with no documented props, nine down to three, all described.
Vocabulary61.461.4Did not move, by design (see below).
Deprecation legibility4040Did not move, by design (see below).

Two checks did not move. Vocabulary compares our API names against a lexicon mined from nearly nine hundred graded generations, and the only thing that moves it is renaming the API: a breaking change for every consumer, chasing a target that shifts with the next model release. We wrote the mapping down instead and left the score alone. Deprecation legibility has a floor and we are sitting on it, because static analysis cannot tell nothing is deprecated from deprecations are undocumented. In our case nothing is deprecated, and the checker has no way to know that, so we adopted the annotation as policy and the score will move the first time a real deprecation ships with one.

The behavioral side has not been re-measured yet. The harness is landing a fix that changes the prompts, and changing a prompt moves engagement by itself, so the six-model matrix waits for that. When it runs we will read it model by model rather than pooled: across the two baseline runs the same rulebook was worth +35.9 to one model and −2.1 to another, and an average would have hidden the regression.

Keeping it true

The score is the part you can see; the change underneath it is that every surface an agent can read is now generated from the same sources the humans edit. One build command regenerates the catalog, the documentation index, the token export and the icon registry. One layer is still written by hand, the rulebook, and even its consumer-facing copies come out of a generator rather than out of somebody's clipboard. That is what keeps the surfaces from drifting apart the next time the team gets busy.

SOURCES OF TRUTH ONE BUILD WHAT AGENTS READ · WHERE IT LIVES Component source doc comments · stories · examples The stylesheet tokens & semantic classes, read-only Icon files named exactly as design names them The rulebook the one layer still written by hand One build, every surface npm run build catalog props from doc comments docs index the full text set token export machine-readable icon registry typed names design mappings for Figma deterministic, so nothing can drift A documentation index ⤷ hosted · in the package · at the root A token export ⤷ hosted · in the design package An offline bundle ⤷ a folder you can hand an agent Typed icon names ⤷ in the API · parity with design Design-tool mappings ⤷ for design-to-code handoff Agents in this repo ⤷ the rulebook, read directly
Four sources, one build, six surfaces. The solid lines are generated. The dashed one is the single thing still written by hand, which agents working inside the repository read directly. Each surface goes where its readers already are: a hosted URL for tools that fetch the web, the installed package for version-exact offline reading, a drop-in folder for agents with no repository or registry access, and the design tool for handoff.

What we'd tell another design-system team

  1. 1
    Watch the behavior, not the shelf

    A respectable surface score was sitting on top of 113 out of 113 attempts ignoring the system. A static audit finds what is missing; a behavioral run tells you whether what exists changes anything. Do both, and grade against what you publish rather than what you have checked out, because that is what surfaced the type-level traps our source run could not see.

  2. 2
    Generate the surfaces, then put them where agents already look

    An index inside the installed package can be read offline and always matches the version the consumer has. Tools without repository access will only ever see a hosted copy, and audits and crawlers go looking at the repository root, so all three addresses need the same file. Tokens want the same treatment. Hand-written copies drift inside a week; we know, because ours did.

  3. 3
    Name the traps instead of renaming the API

    Models arrive with a prior about what things are called. A short "what we call things" table, kept next to the mandate and regenerated into every consumer surface, costs a fraction of a breaking rename. It also catches your own documentation teaching the wrong names, which is how we found ours.

  4. 4
    Let a number stay where it honestly belongs

    Two checks did not move and one of them is capped, on purpose. A score moved by inventing props, or by annotating nothing, would tell the next reader something false about the system, and the next reader is probably an agent.

What's left

We still haven't given agents a way to query the catalog from an editor. Search the components, ask what props one takes. We already generate that information; we just never put a small read-only service in front of it. The audit still warns about the gap. It's also the leftover that would help people who aren't sitting in our harness.

The six-model re-run is waiting on a harness change that rewrites the prompts. Changing the prompts moves engagement by itself, so running now would mix two things. Opening the stylesheet so roles are real variables, not something we parse out of class bodies, is a bigger call than this round was. It would pick up the leftover token score. That can wait until someone wants to open that file.

How this was measured

One baseline audit and two full benchmark runs, with the published packages as the primary result and our own source tree as a cross-check, plus an earlier two-model pilot. The after-scores are audit re-runs against the working tree after those changes, with the benchmark pinned to its baseline version. Every number on this page comes out of those runs and regenerates from the same commands.