Graders
How agent-generated code is scored, and how to write a new grader.
What
A grader reads code produced by an agent and returns a list of violations. The
benchmark adds them up into a score. Graders live in packages/bench/src/graders/:
| Grader | Rules | Catches |
|---|---|---|
raw-values | raw-color, raw-length, raw-number, escape-hatch | "#ff0000", "white", "12px", "1px solid #ccc", style={{ width: 320 }}, fontWeight: 600, Panda's "[#fff]" |
token-roles | wrong-role, palette | A real token on the wrong job (bg: "fg.muted", color: "danger.default", the mint accent.default as text or a focus ring), palette shades (blue.600) |
token-names | unknown-token, unknown-breakpoint | A value that looks like a token and is not one (fontWeight: "bold", fontSize: "3xl", p: "7", fontFamily: "Inter", bg: "surface.raised") and a breakpoint the system lacks ({ base: "column", tablet: "row" }) |
suppressions | ts-ignore, lint-disable, vektos-ignore | Comments that switch a check off |
protected-files | protected-edit | Changes to panda.config.ts, styled-system/, tsconfig*.json, lint config, package.json |
components | native-element | <a>, <button>, <input> (by type: checkbox, radio, text), <textarea>, <select>, <table>, <dialog> where the design system has a component (guide 11) |
outside-styling | styled-from-outside, unsafe-escape | <Card className={…}>, <Button style={…}>, <Button bg="…"> (a style prop that is not a layout prop), and every UNSAFE_className or UNSAFE_style (guide 06) |
use-client | missing-use-client | A page.tsx that needs the browser but has no "use client" (guide 11) |
Every grader returns the same Violation shape: grader, rule, file, line,
column, property, value and a message a person can act on.
gradeSource(filename, source) runs the right graders for a file: the first three
on .ts/.tsx/.js, the CSS versions of them on .css, nothing on other types.
findProtectedEdits(changedPaths) takes the output of git diff --name-only.
Why
tsc and oxlint block bad code. A benchmark needs the opposite: let the agent
write freely, then count what went wrong.
Only style values count. A value is graded when its key is a CSS property, a Panda
shorthand (bg, px, rounded) or a condition (_hover, base, md) under one.
Without that filter, fintech data like { change: "12%" } counts as a raw value (see
anecdote 03). The graders read css() and recipe
objects, style={{ … }}, JSX style props (<Box bg="…">), both branches of
a ? "x" : "y", and template literals, with ${} parts read as 000.
The token name carries its job. bg.* is for backgrounds, fg.* for text and
icons, border.* for borders. Status tokens split the same way: .default for fills,
borders and icons, .subtle for backgrounds, .text for text. token-roles checks
that the job of the property matches the job in the name. The table is data
(VEKTOS_ROLES), so another design system can pass its own.
A name that is not a token is its own mistake. fontWeight: "bold" is not a raw value,
so raw-values is silent, and the compiler only says "No overload matches this call".
token-names says what it is and lists the choices. It compares against a table built from
the design system's panda.config.ts (tokenTableFromConfig), so a token added there is a
valid name at once and another design system passes its own table. It reads css(),
cva() and sva() only, as Panda does: { width: "narrow" } in the page's data is not a
style. Raw values are left to raw-values, so one mistake is one finding
(anecdote 08: the compiler's error
on the same call is covered).
Gaming the benchmark costs points. An agent can hide a violation instead of fixing
it: a // vektos-ignore, a @ts-expect-error, a new "token" in panda.config.ts. The
graders honor vektos-ignore, because a raw value can be intentional, but
suppressions counts every one and protected-files counts every edit to the rules.
A page places a component; it does not restyle it. The components are governed
(guide 06): outside-styling
counts className, style and any style prop that is not a layout prop, on components
imported from @vektos/ds. The rules are data (GovernedSystem, built in vektos-ds.ts from
the design system's own layout-props.ts), so another system passes its own package and list.
A component's own prop that shares a name with CSS (size, a Tooltip's content, a
Pagination's page) is not counted: vektos-ds.ts reads every <Name>Props interface in the
design system, so a new component's props are known without a line of grader code.
The compiler rejects the same prop with TS2322 at the same attribute; that error is marked
covered, so one mistake counts once. The escape hatch (UNSAFE_*) counts every time and
vektos-ignore cannot silence it: a way out is never hidden by another.
Hand-written CSS is graded too. Panda's types stop at css(), so globals.css is
the obvious way around them. gradeCss runs the same checks through PostCSS, including
roles on var(--colors-fg-muted).
The graders use oxc-parser (an AST, not text search: a # in a comment or a URL is
not a violation) because TypeScript 7 has no JavaScript API for reading code.
Try it
cd packages/bench
bun test # 33 pass, 2 todoOr call it directly:
import { gradeSource } from "@vektos/bench";
gradeSource("page.tsx", `<p className={css({ bg: "fg.muted" })} style={{ marginTop: 12 }} />`);
// violations:
// token-roles/wrong-role bg = "fg.muted"
// "`fg.muted` used as background on `bg`: `fg.*` is for text and icons."
// raw-values/raw-number marginTop = "12"
// "`marginTop: 12` becomes 12px. Use a token through `css()`."To mark a raw value as intentional, put the reason in a comment on the line above:
// vektos-ignore: brand color from the client, not in the theme yet
bg: "#ff6b00",The violation moves to ignored, and the comment itself is a suppressions violation.
Extend it
A grader is a function (filename, source) => Violation[]. To write a new one:
- Create
packages/bench/src/graders/<name>.tsand its<name>.test.ts. Add the grader's name toGraderNameintypes.ts. - Use
parseandstyleValuesfromast.ts.styleValuesalready finds every literal written for a style property, with its position. - If the check applies to CSS too, export a
check…(property, value)function and call it fromcss.ts, asraw-valuesandtoken-rolesdo. - Wire it into
gradeSourceingrade.ts. - Add a
todotest for every gap you know about. The gap stays visible until fixed.
The test the Vektos design system and web app grade clean grades our own code like
agent code. If your grader flags something there, either the code or the grader is
wrong: fix one of them, or add a vektos-ignore with the reason.
Known gaps
token-nameschecks the categories the design system has tokens for. A property with no scale (opacity,transition) passes: the compiler lets any CSS value through there too. The property-to-category table is written out intoken-names.ts; a property missing from it is not checked.token-namesis not applied to old runs. The stored runs from before the grader have notoken-namesfindings, and were graded against an earlier token table (see the results page). Run it on the stored files to see what it would have said: against the table of the time it explains 98.6% of theTS2769errors, up from 91%.- A key that is a CSS property and a variant name (
variants: { color: { primary } }) is left alone, so a misspelled breakpoint is only found when its value is a plain value, not an object. - Values passed through a variable (
const c = "#fff"; css({ bg: c })) are not followed. Tracked as atodotest. - Numbers for spacing in
css()(p: 4) are not flagged: Panda may read them as a token. Instyle={{ … }}they are flagged, because React turns them into pixels. - A data key that is also a CSS property (
{ width: "Width 12px" }) is graded as a style. - Shorthand strings are not split into tokens for
token-roles: inborder: "1px solid fg.muted"only the raw part is seen. - Class-name styling (Tailwind) is not read yet. That arrives with the Tailwind adapter in phase 2.
- Each grader parses the file again. At benchmark sizes it does not matter.
tscand axe graders arrive with the runner in week 2.