Page graders
How the benchmark checks the page itself: typecheck, lint, a real render in Chrome, the mustShow texts, axe in both themes, native elements and missing use client.
What
The first graders read source code. A page can pass all of them and
still be broken: it may not compile, crash on render, show nothing, or use the right
tokens on a <button> that is not the design system's Button. This block adds the
checks that need the page to exist as a running page.
They come in two kinds.
Static, in gradeSource (they read one file, so they run on every grade):
| Grader | Rule | Catches |
|---|---|---|
components | native-element | <a>, <button>, <input>, <textarea>, <select>, <table>, <dialog> where @vektos/ds has Link, Button, Input, Textarea, Select, Table, Dialog; <input type="checkbox"> points to Checkbox or Switch, type="radio" to RadioGroup, and type="hidden" passes |
use-client | missing-use-client | a page.tsx that calls a hook or passes an on* handler but has no "use client". Next.js will not build it, and tsc does not notice |
Dynamic, in packages/bench/src/page/ (they need real files and a browser):
| Grader | Rules | How |
|---|---|---|
typecheck | TS2322, … | tsc --noEmit on a scratch folder |
lint | eslint(no-unused-vars), … | oxlint -f json with the repo's .oxlintrc.json |
render | build-failed, runtime-error, console-error, empty-page, missing-text | bundle, open in Chrome, read the screen |
a11y | axe rule ids: color-contrast, image-alt, … | axe-core, once per theme |
Every violation belongs to a group, so a report can answer three questions apart
(GRADER_GROUP in graders/types.ts):
| Group | Question | Graders |
|---|---|---|
design-system | Did the agent follow the design system? | raw-values, token-roles, token-names, components, outside-styling, suppressions, protected-files |
quality | Is the code sound? | typecheck, lint, a11y, use-client |
completeness | Is the page done? | render, behaviour |
A run is complete when it wrote page.tsx, the page mounted, every mustShow text is on
screen, and, since graders v3 (ch. 68), it does what its task's behaviour.json asks. RunResult has rendered, missing and complete, plus one
full-page screenshot per theme.
A violation can also be covered: a TS2769 ("No overload matches this call") inside a
css, cva or sva call that holds a raw-values, token-roles or token-names
violation. With
strictTokens the compiler rejects css({ bg: "#fff" }), so it is the same mistake as the
raw-value finding. It is stored with covered = true, shown in the report as "not
counted", and left out of every count (markCovered in graders/covered.ts).
Why
- One mistake, one count. Covered violations exist for the same reason lint skips two rules, except that the compiler's error does not say what it is about, so it is matched by position (anecdote 08). Without it, 87% of the quality violations of runs without context were design-system violations again.
- Three groups, not one score. A page with a native
<button>styled with perfect tokens is a design-system failure but not a quality failure. A page that crashes is the reverse. Mixed into one number you could not tell which context level helped with what. - Lint is "quality", and it skips two rules.
@pandacss/prefer-tokenandno-primitive-tokenreport the same mistake asraw-valuesandtoken-roles. Counting both would double every raw color. The graders also stay stack-neutral: they do not need to know Panda exists. - A real browser for
mustShow. Server-rendered HTML is not what the user sees. The check readsinnerTextand the value of every field, so a form filled from the fixture passes and an empty page does not. - Both themes. Anecdote 02: a token can
pass contrast in light and fail in dark. axe runs twice, adding the
darkclass the second time. One violation per rule and theme, with the number of elements in the message, so a table with fifty bad cells is one finding, not fifty. - React warnings count. The page is bundled in development mode so React reports a
missing
keyor an invalid nesting throughconsole.error, and that is a violation. native-elementis a design-system rule.vektos-ignorecan silence it, like a raw value. It cannot silencemissing-use-client: a page that does not build is not an intentional choice.- Only
page.tsxneeds"use client". A helper file may be imported by a client file, so flagging it would be wrong. Design-system components are already client components (Ark marks them), so only what the page itself calls counts.
How the page is checked
agent's files ──► apps/web/.bench/<id>/ (a scratch folder, deleted afterwards)
├─ tsc --noEmit → typecheck
├─ oxlint -f json → lint
├─ panda cssgen → the page's CSS
├─ Bun.build → one script
└─ Chrome + axe-core → render, a11y, screenshots- Why inside
apps/web. That is where@vektos/ds,reactand@types/reactresolve, and wherestyled-system/*is mapped by the generatedtsconfig.json. The folder is gitignored, and.bench/**is in Panda'sinclude, so the lint rulefile-not-includedpasses. - Protected files never reach the folder.
panda.config.ts,tsconfig.jsonandpackage.jsonare skipped, so an agent cannot loosen its own typecheck. Theprotected-filesgrader still reports the edit. - One Chrome per batch.
createPageChecker()starts it on the first check andclose()shuts it down. The CLI callsready()first, so a missing Chrome stops the run before any paid model call. - Errors in our own sources are not the agent's.
tscalso reads the design system; only errors in files the agent wrote become violations.
Try it
bun run bench --model mock/idealtask page done ds qual steps tokens in/out cost
kyc-form yes yes 0 0 3 3917/1460 $0.0112
statement yes yes 0 0 3 3834/729 $0.0075ds is the design-system group. qual is quality plus completeness. done is
complete. Open results/<runId>/statement/light.png and dark.png.
bun run bench --model mock/lazy --task statement # page NO, done NO (5 missing)
bun run bench --model mock/ideal --no-page # the static graders only, no Chrome
bun test packages/bench/src/page # unit tests, no browser
bun run check:page # the same plus the Chrome testscheck:page puts the reference solution of every task through these checks; each must come out
with no findings. bun run check skips the Chrome tests so it stays fast. Run check:page before
touching anything in packages/bench/src/page/.
Extend it
- Another native element. Add a
{ tag, component }toVEKTOS_COMPONENTS(withtypesto match an<input>by itstype; the first matching rule wins), or pass your own list tofindNativeComponents. - Another browser check.
renderPageinpage/render.tshas the page open intab: add the check there and return aViolationwith arenderrule. - Another axe tag set. Edit
AXE_OPTIONSinpage/render.ts. A test checks that axe knows every tag: an unknown one matches no rule and raises no error (anecdote 19). Batches before graders v2 (ch. 59) ran WCAG 2.0 and 2.1, A and AA, only; since v2 axe'sbest-practicerules run too (regionstays off: landmarks around a page are the layout's). A change of tags changesgraderHash, and/regressionsnames it (guide 23). - Another browser. Set
VEKTOS_BROWSER_PATHto any Chrome or Chromium executable.
Upgrading the browser: from system Chrome to a pinned Chromium
The checker uses the Google Chrome installed on the machine (channel: "chrome"):
nothing to download and it works on a fresh clone. That is right for what the checks
look at (does it mount, what does it say, does axe pass), which does not change between
Chrome versions.
It is not enough for a visual diff (PLAN, phase 2). A screenshot comparison needs the same browser, fonts and rendering on every machine and in CI, or every Chrome update looks like a regression. When that block starts:
- add
playwright(the full package, which downloads browsers) next toplaywright-core, and runbunx playwright install chromiumin setup and in CI; - pin the version in the lockfile and launch with
chromium.launch()instead ofchannel: "chrome": that is one line inlaunchBrowser()inpage/render.ts; - keep
VEKTOS_BROWSER_PATHas the override; - take the screenshots at a fixed viewport (
VIEWPORTis already fixed at 1280×800) and disable animations.
Nothing else in the checker depends on which browser it is.
Behaviour
Five tasks list what the page must do after the user acts, in tasks/<id>/behaviour.json
(page/behaviour.ts): a few steps (fill a field by its label, check a box, submit) and one
expectation (invalid: the field is marked invalid, by aria-invalid or the browser's own check;
or shows: a text appears). Each runs on a fresh copy of the page, after axe. A failure is a
behaviour violation in the completeness group, named after the behaviour, and the run is not
done.
| Task | Behaviours |
|---|---|
transfer-form | an amount above the balance is an error on its field; a valid transfer confirms with the beneficiary |
kyc-form | an empty submit is an error on "Date of birth" |
card-dispute | a description under 30 characters is an error on "What happened" |
goods-receipt | a quantity above the ordered one is an error on its field |
stock-adjustment | an empty save is an error on "New quantity"; typing one shows the difference with its sign |
- Fair by construction. Labels come from the brief or the data (a test checks it);
submitnever names a button, it clicks the form's submit button or the page's last one, and leaves a disabled one alone, since keeping the user from submitting is an answer. - Accessible by construction. An error counts when it is tied to its field: a message the screen reader never hears is not an error message.
- Graded, not briefed.
behaviour.jsonis in the graders' hash, not the task's: the briefs did not change, the grading did.
Known gaps
- Token names that do not exist are invisible to the design-system graders.
fontWeight: "bold"is not a raw value; only an uncoveredTS2769reports it. - Quality mixes units. a11y counts one per rule and theme, type and lint errors one per occurrence.
- One viewport, no mobile. 1280×800 only. A layout that breaks at 375 px passes.
- Little interaction. Five tasks have behaviours, and only forms: a dialog that never opens, a tab that does not switch, a select that cannot be opened are not tested yet.
- No screenshot comparison. Screenshots are saved, not compared. See above.
- axe finds about a third of accessibility problems. A page with no axe findings is not proven accessible.
- Fragment, not document. The
regionrule is off, because the layout around a page provides the landmarks. - Pages only. The agent writes one page. Several routes, server actions or data
fetching would need a real
next build. - Needs Chrome. On a machine without it, run with
--no-pageor setVEKTOS_BROWSER_PATH.