Repair
ui-repair
A linter says the file has a raw button and #8B5CF6. ui-repair applies system rules and writes a patch. On the fixture a 10-check rubric moves from 50 to 100. That is not a model call and not a ds-eval run.
Why
Finding a violation is the small half. The team needs a patch: a raw <button> becomes Button, #8B5CF6 becomes a token, a homemade modal becomes Dialog. ui-repair makes that replacement with rules and shows which check flipped.
A live agent should emit a patch of the same shape. This MVP keeps the rules deterministic so the example runs without a key.
Rules
Each rule reads the fixture text and the tokens.css beside it. On Acme, --color-accent is #8B5CF6 and --space-3 is 12px.
| Finds | Writes |
|---|---|
| a hex that matches a token value | var(--color-accent) |
padding: 13 | var(--space-3) |
<div className="modal"> | <Dialog> |
a raw <button> | <Button> and an import from @ds |
| a button whose only child is × | aria-label="Close" |
The import is one line, added when the file has no from "@ds" yet: import { Button, Dialog } from "@ds";
Run
python3 ui-repair/ui_repair.py scan ui-repair/fixtures
python3 ui-repair/ui_repair.py fix ui-repair/fixtures --out ui-repair/examplesscan prints the list and writes nothing. fix puts Settings.jsx in ui-repair/examples and prints the local score before and after. On the fixture, scan reports:
6 issues
raw <button>
hardcoded hex color
hand-rolled modal
padding 13 is off the space scale
icon button without an accessible name
no design-system importBefore
ui-repair/fixtures/Settings.jsx. A purple background as a number, padding 13, Save as a raw button, a div modal, and a close mark with no name.
export function Settings() {
const open = () => {};
return (
<section>
<h1>Settings</h1>
<div style={{ background: "#8B5CF6", padding: 13 }}>
<button onClick={open}>Save</button>
<div className="modal">
<h2>Delete account</h2>
<button>×</button>
</div>
</div>
</section>
);
}After
The same file after fix. This is the rule result, not a model reply.
import { Button, Dialog } from "@ds";
export function Settings() {
const open = () => {};
return (
<section>
<h1>Settings</h1>
<div style={{ background: "var(--color-accent)", padding: "var(--space-3)" }}>
<Button onClick={open}>Save</Button>
<Dialog>
<h2>Delete account</h2>
<Button aria-label="Close">×</Button>
</Dialog>
</div>
</section>
);
}Checks
Ten checks, equal weight. Before the repair the score is 50: five structural checks already passed, five system checks did not. After, it is 100.
| Check | Before | After |
|---|---|---|
| exports a component | pass | pass |
| handles a click | pass | pass |
| no dangerouslySetInnerHTML | pass | pass |
| has a heading | pass | pass |
| no !important | pass | pass |
| imports the design system | fail | pass |
| no raw button | fail | pass |
| no hardcoded hex | fail | pass |
| uses Dialog, not a modal div | fail | pass |
| no magic padding number | fail | pass |
This is not a ds-eval score and not a Claude result. The next step is to run the patch through that harness: build, axe, Playwright. What you see here is a rule repair and which checks it closed.
Where it sits
ds-context says which components and tokens exist. ui-repair moves a file onto that list. ds-eval checks whether the screen works, not only whether the tags look clean.
Limit
The rules know this fixture: one hex, padding 13, a modal class, a close mark. They will not repair an arbitrary codebase. There is no git-style diff, no screenshot, and no live agent. A jump from 50 to 100 is easy to read and easy to over-read.