Next.js template · vision model · deterministic scoring
Upload a design and the implementation that was built from it. A vision model returns findings only — each one tagged with a rubric category and a severity. The server turns those into a 0–100 score with a weighted formula the model never sees, and writes a ticket you can paste into an issue tracker.
Model: anthropic/claude-sonnet-5 · no API key set, so comparisons are disabled
The same engine, opened up to a visiting agent. Seven tools registered through document.modelContext or navigator.modelContext: the agent sweeps three bundled screen pairs, ranks them worst first and drafts the ticket, and you decide which differences are defects and which were on purpose. Runs on sample data, so it works in any browser with nothing configured.
Set OPENAI_COMPATIBLE_API_KEY to run a comparison. Everything else on this page works without it.
Locally: copy .env.example to .env.local, paste a key, restart npm run dev. On Vercel: add it under Project Settings → Environment Variables, then redeploy.
Vision models are good at seeing that a button moved. They are poor at converting what they saw into a number, and worse at doing it the same way twice. Run the same two screenshots through a “score this out of 100” prompt repeatedly and the answers cluster around 90 whatever the images show, then wander by ten or fifteen points between runs.
It returns findings. Each carries a category id from the rubric and one of three severities. The server rejects anything with an unknown category, dedupes, sorts, and then does the arithmetic in src/lib/score.ts. Same findings in, same number out — every time, and defensible to whoever the ticket lands on, because the rubric is a file they can read.
raw(c) = Σ severity_points[f.severity] × (weight(c) / 10)
deduction(c) = min(raw(c), weight(c)) ← a category never exceeds its weight
score = clamp(round(100 − Σ deduction(c)), 5, 100)
severity_points = { critical: 10, major: 4, minor: 1.25 }critical = 10 is chosen so that exactly one critical finding consumes its entire category. Severity therefore scales with how much a category is worth, with no second table to keep in sync. The per-category cap is what the weights are really for: it bounds how much damage one talkative run can do, which is where run-to-run variance comes from.
5 categories, 15 sub-checks, weights summing to 100. It lives in one file — src/lib/rubric.json — which the prompt, the validator, the scoring and this page all read. Add a category and the prompt grows a section for it automatically. This is the part you are meant to rewrite.
layout-spacing
Where things sit and how much room is between them. The single largest source of build drift, so it carries the largest weight.
missing-or-extra-elements
Something in the design that never shipped, or something in the build that was never designed. Cheap for a human to miss and expensive to ship.
typography
Size, weight, case, and line treatment of text. Judged by eye, so claims here must be hedged.
color
Fills, text colours, borders and states. Screenshots are re-encoded before the model sees them, so only differences a human would notice count.
alignment
Whether elements line up with each other and with the grid. Individually small, collectively what makes a build look unfinished.
Categories, weights, severity points and bands. Nothing else needs editing when you add a category — the prompt and the UI are generated from it.
The engine prose: what to ignore, how to hedge, what makes a finding. The taxonomy underneath it is generated from the rubric.
The arithmetic. Roughly forty lines, no dependencies, and trivially unit-testable — which is the point of keeping it out of the model.