Skip to main content
Glama
README.md
# yardstick

**Measure a web page from the DOM and compare the numbers with two or three sites you admire.**

[![CI](https://github.com/matthewvilaysack/yardstick/actions/workflows/ci.yml/badge.svg)](https://github.com/matthewvilaysack/yardstick/actions/workflows/ci.yml)
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776ab)](pyproject.toml)
[![MIT](https://img.shields.io/badge/license-MIT-1c44b8)](LICENSE)
[![MCP](https://img.shields.io/badge/MCP-stdio%20server-f2c53d)](#install)

![A critique run: side-by-side table against stripe.com and linear.app, one contrast finding with a region](docs/demo.png)

Agent-built pages look generic and nobody can say why in terms the agent can act on. "Make it feel more like Stripe" is not a fix. "Your h1 is 36px over 18px body, Stripe holds 3.0 and Linear 4.9, set the h1 to 60px" is. Yardstick produces the second kind of sentence, and it never calls a model: fonts, type scale, spacing grid, palette, contrast pairs, hero geometry, and layout come from computed styles in headless Chromium, and every standard comes from the references you name, not from anyone's memory of what good looks like.

```
yardstick critique http://localhost:3000 --vs stripe.com,linear.app
```

## What it prints

A landing page I built with a coding agent, critiqued against stripe.com and linear.app, both captured live:

```
counts:
  sev3: 1
  sev2: 2
  sev1: 0
first_change: "K1: Darken the text or lighten the background until the pair reaches 4.5:1; keep the hue, move lightness"
table[23]{metric,mine,stripe.com,linear.app}:
  mode,light,light,dark
  heading font,Bricolage Grotesque,sohne-var,Inter Variable
  body font,Public Sans,sohne-var,Inter Variable
  h1 px,58,48,64
  body px,17,16,13
  h1/body,3.41,3,4.92
  h1 line-height,1.03,1.15,1
  body line-height,1.62,1.4,1.54
  measure ch,63.75,50.67,58.5
  on-grid %,39.5,86.2,80.6
  accent,#c4400a,#533afd,#e4f222
  gradient hero,false,false,true
  contrast fails,1,3,1
  radii,0/8/999,0/4/5/6/10,0/6/8/9/12/16/22/9999
  hero align,left,left,left
  hero CTAs,1,2,3
  ...
findings[3]:
  - id: K1
    severity: 3
    standard: "WCAG 1.4.3 needs 4.5:1 for body text and 3:1 for large text"
    gap: "#64584c on #16110d measures 2.72:1 (\"Illustrative ledger, shortened for the page. Every entry is \")"
    fix: "Darken the text or lighten the background until the pair reaches 4.5:1; keep the hue, move lightness"
    region[4]: 295,831,850,21
  - id: T2
    severity: 2
    standard: "Two families at most (stripe.com: 1, linear.app: 2)"
    gap: "3 families carry text: Public Sans, IBM Plex Mono, Bricolage Grotesque"
  - id: S1
    severity: 2
    standard: "References keep stripe.com 86%, linear.app 81% of spacing values on a 4px grid"
    gap: "Yours is 40% on-grid; frequent values are 18, 14, 20, 22, 12"
shots:
  mine: ~/.local/share/yardstick/shots/_critique/127.0.0.1-index/hero.png
  stripe.com: ~/.local/share/yardstick/shots/stripe.com/hero.png
  linear.app: ~/.local/share/yardstick/shots/linear.app/hero.png
```

The contrast finding is real: the italic caption under the dark ledger is 2.72:1, and the region points at it. A deliberately sloppy page (Inter everywhere, purple gradient hero, nine identical rounded cards, 95-character lines) draws 3 severity-3 and 6 severity-2 findings against the same two references; the two hand-made good pages in `tests/fixtures` draw no severity-3 against each other. That pair is the test suite's positive and negative check.

Every finding is the shape designers validated in the UICrit study: the expected standard, the gap, how to close it, the evidence, and a rectangle on your hero screenshot when it is about an element. The screenshots are on disk so the agent driving the tool can read them and do the subjective half itself.

## Install

```
uv tool install git+https://github.com/matthewvilaysack/yardstick   # or: uv build && uv tool install dist/*.whl
yardstick setup                                                       # installs headless Chromium once
yardstick seed                                                        # 1,160 domain pointers for `search`, optional
```

For hacking on it: `git clone`, `uv sync`, `uv run pytest -q`, and `bin/yardstick` runs from the checkout.

Claude Code can use the CLI as is, with the skill linked so `/yardstick` loads the loop, or the MCP shim, which returns the same text plus the hero images as image blocks:

```
ln -s ~/code/yardstick/skill ~/.claude/skills/yardstick
claude mcp add --transport stdio yardstick --scope user -- ~/.local/bin/yardstick mcp
```

## Three ways to use it

- **CLI**, for you and for coding agents that live in a terminal (the skill in `skill/SKILL.md` teaches Claude Code the loop).
- **MCP server**, `yardstick mcp`, five tools (`discover`, `search`, `show`, `measure`, `critique`) for Claude Code, Cursor, Codex, or any MCP client; hero screenshots come back as images.
- **Local app**, `yardstick app`, a one-screen interface on localhost: describe what you are building, pick the references it finds, run, read findings with the region outlined on your own screenshot, run again after fixes. History is kept on disk.

![The local app: severity tiles, first change, findings beside the outlined crop](docs/app.png)

## Finding references for an intent

```
yardstick discover "neighborhood dinner club that seats strangers together weekly" -n 5
```

Providers run in order and the first one to name a site wins: live web search (DuckDuckGo through `ddgs`, no key), the seeded catalog of 1,160 design-described domains, then any command you add. A command is how a subagent, an agent CLI, or a paid search API plugs in without touching this code: it gets the intent on stdin and prints a JSON list of `{domain, why}`.

```toml
# ~/.config/yardstick/providers.toml
[providers.claude]
command = "claude -p --output-format text 'Name 5 real websites whose design a $(cat) should learn from. Reply with a JSON list of {domain, why} only.'"
timeout = 120
```

`--providers web,claude` picks and orders them per call. `--add` captures every candidate as a reference in one go, so `discover` then `critique --vs` is the whole loop.

## Commands

```
yardstick discover "<intent>" [-n 5] [--providers web,pointers,...] [--add]
yardstick app [--port 7878] [--no-open]
yardstick add <url> [--name n]          capture, measure, store a reference
yardstick list                          stored references
yardstick search "<text>" [-n 5]        references and seeded pointers, with the matched facets as "why"
yardstick show <name> [--full]          measurement and screenshot paths
yardstick measure <url>                 measure without storing (localhost works)
yardstick critique <url> --vs a,b[,c]   table, findings, first change, screenshot paths; --html <path> writes a standalone report
yardstick rm <name>
yardstick seed [--file README.md]       load domain pointers (MIT catalog from scroobius-pip/fudge-design-md)
yardstick setup [--dry-run]             install the headless browser
yardstick mcp                           stdio MCP server
```

Output is TOON; add `--json` after the subcommand for JSON. Errors are structured on stdout: exit 1 for usage, exit 2 when a capture fails, with the stage named.

## Checks

| id | fires when |
|---|---|
| K1 | a text/background pair fails WCAG AA, verified against screenshot pixels; unverifiable pairs are counted, not reported |
| T4 | body paragraphs run outside 40 to 80 characters per line |
| H2 | no button-styled action above the fold while every reference has one |
| T1 | h1/body ratio below 75% of the references' minimum or above 150% of their maximum |
| T2 | more than two families carry text, or headings use Inter, Roboto, Arial, Helvetica, system-ui, Poppins when no reference does |
| T3 | h1 line-height above 1.3, or body line-height outside 1.4 to 1.75 |
| S1 | share of spacing values on a 4px grid more than 20 points under the references' minimum |
| C1 | gradient hero background when no reference has one |
| C2 | chromatic hue count above the references' maximum plus two |
| P1 | one radius on eight or more boxed elements while the references vary theirs |
| H1 | h1 pixel size more than 15% outside the references' range |
| H3 | dark versus light differs from every reference (informational) |

There is no 0 to 10 score. Counts by severity are honest; a score without calibration data is not.

## Layout

```
src/yardstick/walk.js        one page.evaluate: raw records per visible element (rect, text, computed type, colors, spacing, radius, shadow, parent)
src/yardstick/capture.py     Playwright: load, scroll to settle, remove cookie banners, run the walk, hero + full screenshots, sample pixels behind text
src/yardstick/measure.py     raw records -> measurement (pure Python, unit-tested without a browser)
src/yardstick/critique.py    measurement + references -> table, findings, first change
src/yardstick/store.py       SQLite with FTS5 over facets and pointers
src/yardstick/cli.py         subcommands, TOON output, exit codes
src/yardstick/mcp_server.py  five tools over the CLI, hero images as image blocks
src/yardstick/discover.py    reference discovery: web, catalog, and command providers (providers.toml)
src/yardstick/app.py + app.html   the local one-screen app over a small JSON API
src/yardstick/report.py      standalone HTML report with outlined crops
tests/fixtures/              editorial.html, dark.html (good, different), slop.html (deliberately bad)
docs/design.md               the design; docs/research.md the day of research it rests on
```

Data lives in `~/.local/share/yardstick/`: `refs.db` and `shots/<name>/`. Captures are private on disk, nothing is redistributed, and `yardstick rm` is the takedown.

## Tests

```
uv run pytest -q     # 86 passed; the browser tests take about 30 seconds
```

## For teams and products

Everything runs on the machine that runs it: no accounts, no uploads, no model calls. If you sell websites built with AI, the MCP is the piece your agents call before and after they build, and the app is the piece your clients can look at. The MIT license lets you bundle it; the provider file lets you swap in your own search or your own agent as the discovery step.

## Not in this version

Mobile and dark-scheme captures, embeddings, a numeric score, a hosted service, DESIGN.md or Tailwind export, font similarity. Each is a slice once the critique has been used on a few real pages.

TDQS

B3.4/5.0

Scored across 5 tools

Disambiguation4/5

Search and discover both return reference sites, but search targets stored items via text while discover finds new sites from a project description, so the intent is mostly clear. Show, measure, and critique are each distinct and unambiguous.

Naming Consistency5/5

All five tool names are single lowercase verbs following a consistent action-first pattern (search, discover, show, measure, critique), which is predictable and easy to follow.

Tool Count5/5

Five tools is well-scoped for a design reference, measurement, and critique server. Each tool represents a meaningful, distinct action without excess or redundancy.

Completeness3/5

The server covers searching, measuring, and critiquing reference sites, but there is no tool to store or manage new references, which leaves a notable lifecycle gap. Agents can work around it by using domains on the fly, but the stored-reference workflow feels incomplete.

Maintenance

ActivityMaintained
ResponsivenessNo issues