Design-Fidelity — Visual Design-Fidelity Loop
Scope contract (Invariant): this skill ONLY shoots screenshots, diffs them, reads computed CSS, and SUGGESTS CSS changes for the operator to apply. It NEVER deploys, NEVER edits live-site config or DNS, and NEVER pushes to a live site. It reads; the operator decides and edits. Screenshot output is always written OUTSIDE any git repo so a capture can never be committed.
Related: drawing/annotating a schematic diagram (not matching a live page)? See docs/guides/diagram-annotation-qa.md (../dotfiles/docs/guides/... from a consumer repo) — it reuses this skill's shoot.py for the render-and-inspect step of the Hybrid diagram pipeline.
Hard dependency: the three helpers scripts/shoot.py, scripts/compare_computed_style.py, and scripts/pixel-drift.sh; the two mechanics guides references/playwright-fallback.md (capture + computed-CSS + pixel-diff command bodies) and references/chrome-devtools-mcp.md (MCP route); the playwright Python lib + its bundled chromium_headless_shell (per-repo .venv OR global python3 — the helpers fail loud with the install command if it is missing); and ImageMagick compare (the pixel-diff engine — pixelmatch is NOT installed). The skill is available in every repo where its directory is present on the Claude Code skills path.
SST3 Anti-Patterns Governing This Skill
Respect these APs (full detail in ANTI-PATTERNS.md):
- AP #10 Duplicate Rules: the capture / computed-CSS / pixel-diff MECHANICS live in the two guides. This skill points at them — it does NOT restate the command bodies. Grep the guides before adding any mechanic doc here.
- AP #16 Monitor, Don't Fire-and-Forget: after every
shoot.py / compare_computed_style.py / pixel-drift.sh run, VERIFY the exit code and read the stderr telemetry (which wait-strategy fired, normalisation WxH, compare rc). "Ran the script" is not "got the shot".
- AP #17 Keep Going Until Done: a per-page 3x loop runs its iterations without pausing to ask "should I continue?". Stop ONLY at the ~50%-remaining compact trigger (on long work, auto-
/handover + compact + continue; never below 50% remaining), a destructive action needing consent, genuinely stuck, or task complete.
- AP #18 Sample-Invocation: a real end-to-end shoot + compare + drift on one live page IS the Workflow/E2E-tier verification. Keep the per-repo
.design-fidelity.json + the produced numbers as the evidence.
Operator Prerequisites
- playwright + chromium: per-repo
.venv/bin/python -m playwright install chromium (or global). WSL works out of the box with the bundled chromium_headless_shell — no Windows Chrome, no extra flags.
- ImageMagick:
compare, convert, identify on PATH. On a host whose policy.xml denies the PNG coder, pixel-drift.sh's probe guard fails loud — re-allow the PNG coder.
- A reachable base_url: the operator runs the local build server (e.g.
hugo server) OR supplies a live URL. The skill does not own the server lifecycle by default (an opt-in hugo block exists for convenience).
- A per-repo
.design-fidelity.json (optional): the page→path map, viewports, and out_dir for that repo. The sample values are an EXAMPLE, never a default.
When to Invoke
- "Screenshot this page / the whole site at desktop and mobile."
- "Compare my local build against the live site and tell me what's off."
- "Match the design of — measure how close we are and what to change."
- "Show me the pixel-diff drift between local and live for the home page."
- Greenfield capture-only (no live reference yet): just shoot the build for review.
Mandatory Reading on Invoke
references/playwright-fallback.md — the canonical capture + "Live computed-CSS reads" + "Pixel-diff scoring" mechanics (command bodies live HERE, not in this file).
references/chrome-devtools-mcp.md — the MCP route + the browser-state security note.
- The per-repo
.design-fidelity.json if it exists (page map / viewports / out_dir).
- The screenshots output directory for the active repo under
~/DevProjects/screenshots/<repo>/ (where captures and diffs land — OUTSIDE the repo).
The Per-Page 3x Iterate Loop
Work ONE page at a time. Each iteration runs this concrete dual-URL recipe:
- Shoot both sides at both viewports. Shoot the LOCAL build (
shoot.py with base_url = the local server) for the target page path at desktop (1440) + mobile (390), and shoot/cache the LIVE reference (shoot.py with base_url = the live base) for the same path at the same viewports. Capture local and live at identical viewport widths so the drift number is not inflated by stretch.
- Measure the gap. Run
compare_computed_style.py --local-url <local> --live-url <live> --selector <target> on the selector(s) you are matching (exact spacing / colour / font deltas per viewport), AND run pixel-drift.sh <local.png> <live.png> on the PNG pair (a measurable AE + closeness% + a diff-highlight image).
- Tweak and re-shoot. Apply the smallest CSS change the deltas point to, rebuild the local site, then re-shoot and re-measure.
Repeat at most 3 times per page. Watch the drift number trend DOWN and the computed-CSS deltas shrink toward zero. Then move to the next page.
Sub-Commands
/design-fidelity shoot — capture only (shoot.py). Works STANDALONE with no live reference (greenfield repo): just produces full-page desktop+mobile screenshots for review.
/design-fidelity compare — run the full per-page loop above against a live reference: shoot.py (local + live) → compare_computed_style.py → pixel-drift.sh. Fires ONLY when a live reference URL is supplied.
/design-fidelity diff — re-run pixel-drift.sh on an existing local/live PNG pair to re-measure drift after a tweak.
Triple-Check Discipline
Maps to the Three-Tier Testing Framework (SST3 STANDARDS.md "Three-Tier Testing Framework").
- Unit Tier:
tests/run_unit_tests.sh asserts each helper's contract (config-merge + parsers, the null-selector _error path, and the pixel-drift output binary) with no browser/live-site dependency.
- Workflow Tier: a real
shoot.py capture run against a reachable base_url, exit 0, with the expected PNGs written outside the repo.
- E2E Tier: a full shoot + compare + drift on one live page end-to-end, producing a real drift number + diff image — the AP #18 sample-invocation.
Security: chrome-devtools browser state
When you drive a LIVE site via the chrome-devtools MCP (the alternative to the headless playwright route), the MCP runs with full browser access — it can read open-tab cookies, auth tokens in request/response headers, pasted secrets in form fields, and cached credentials surfaced by the page (live browser state). Disable chrome-devtools via /plugin BEFORE pasting any secret. The DEFAULT route in this skill is headless playwright, which carries no live browser state, so prefer it. Full detail: references/chrome-devtools-mcp.md "Security note".
Anti-Overfit Constraint
The pixel-drift number is a TREND diagnostic — the operator watches it decrease across iterations; it is NEVER a hard pass-gate. The target is a STRUCTURAL match, not pixel-perfection (anti-aliasing, font-hinting, and dynamic content make pixel-perfect impossible and pointless). The page→path map, base_url, and viewport list are PER-REPO config — the proto values are an example, NOT a default.
File Locations
| File |
Location |
| Capture helper |
scripts/shoot.py |
| Computed-CSS delta helper |
scripts/compare_computed_style.py |
| Pixel-drift helper |
scripts/pixel-drift.sh |
| Unit tests |
tests/run_unit_tests.sh |
| Capture / computed-CSS / pixel-diff mechanics |
references/playwright-fallback.md |
| MCP route + browser-state security |
references/chrome-devtools-mcp.md |
| Per-repo config |
<consumer-repo>/.design-fidelity.json |
| Screenshot output (OUTSIDE any repo) |
~/DevProjects/screenshots/<repo>/ |
Skill Surface Contract
- Inputs: a
base_url (local build or live), a page→path map, viewports, and optional selectors. Via .design-fidelity.json and/or CLI overrides.
- Outputs: full-page PNGs + diff-highlight PNGs under
~/DevProjects/screenshots/<repo>/; computed-CSS deltas + drift numbers on stdout; decision-branch telemetry on stderr.
- Never writes into the consumer repo tree; never deploys; never edits the live site.
- Exit codes are load-bearing (AP #16): the helpers fail loud on a missing dependency, a blocked PNG coder, or a missing selector rather than producing a silent empty result.
- The
.design-fidelity.json config is treated as UNTRUSTED (it is repo-local, so a cloned repo could ship a hostile one). shoot.py: the opt-in hugo.cmd launcher spawns NOTHING unless the operator passes the explicit --allow-hugo-launcher CLI flag (config alone can never start a process); when enabled it allowlists the executable to STATIC dev-servers only (hugo/jekyll/vite/serve/http-server — bare PATH names, no ./hugo path component — and no interpreters or npm run-style wrappers) and rejects inline-code/eval flags; base_url is http(s)-only (no file://); page labels + viewport names must be [A-Za-z0-9_-]+; viewport width/height must be ints in 1..8192; scroll.step must be a positive int (settle clamped); out_dir must resolve strictly UNDER $HOME or the system temp dir. Each violation is a loud exit 2.
- Residual trust the threat model deliberately does NOT remove — only pass
--allow-hugo-launcher on a repo you TRUST: (1) vite and jekyll execute the repo's OWN build config (vite.config.{js,ts}, Jekyll _config.yml/_plugins) by design when they start, so launching them runs that repo's build code; hugo/serve/http-server do not. (2) base_url/page paths are scheme-restricted to http(s) but NOT host-filtered (the tool MUST reach http://127.0.0.1:<port> for local builds), so a hostile config can point captures at internal/loopback/cloud-metadata HTTP endpoints (e.g. 169.254.169.254) — do not run an untrusted repo's config on a host with sensitive internal HTTP reachability.
1---2name: design-fidelity3description: Screenshot a webpage, compare a local build against the live site, match the design, and pixel-diff drift. Visual-design-fidelity loop — headless full-page captures (desktop+mobile), live computed-CSS reads, and ImageMagick pixel-drift scoring — reusable in any repo.4---56# Design-Fidelity — Visual Design-Fidelity Loop78> **Scope contract (Invariant)**: this skill ONLY shoots screenshots, diffs them, reads computed CSS, and SUGGESTS CSS changes for the operator to apply. It NEVER deploys, NEVER edits live-site config or DNS, and NEVER pushes to a live site. It reads; the operator decides and edits. Screenshot output is always written OUTSIDE any git repo so a capture can never be committed.910> **Related**: drawing/annotating a schematic diagram (not matching a live page)? See `docs/guides/diagram-annotation-qa.md` (`../dotfiles/docs/guides/...` from a consumer repo) — it reuses this skill's `shoot.py` for the render-and-inspect step of the Hybrid diagram pipeline.1112> **Hard dependency**: the three helpers `scripts/shoot.py`, `scripts/compare_computed_style.py`, and `scripts/pixel-drift.sh`; the two mechanics guides `references/playwright-fallback.md` (capture + computed-CSS + pixel-diff command bodies) and `references/chrome-devtools-mcp.md` (MCP route); the `playwright` Python lib + its bundled `chromium_headless_shell` (per-repo `.venv` OR global python3 — the helpers fail loud with the install command if it is missing); and ImageMagick `compare` (the pixel-diff engine — pixelmatch is NOT installed). The skill is available in every repo where its directory is present on the Claude Code skills path.1314## SST3 Anti-Patterns Governing This Skill1516Respect these APs (full detail in `ANTI-PATTERNS.md`):1718- **AP #10 Duplicate Rules**: the capture / computed-CSS / pixel-diff MECHANICS live in the two guides. This skill points at them — it does NOT restate the command bodies. Grep the guides before adding any mechanic doc here.19- **AP #16 Monitor, Don't Fire-and-Forget**: after every `shoot.py` / `compare_computed_style.py` / `pixel-drift.sh` run, VERIFY the exit code and read the stderr telemetry (which wait-strategy fired, normalisation WxH, `compare` rc). "Ran the script" is not "got the shot".20- **AP #17 Keep Going Until Done**: a per-page 3x loop runs its iterations without pausing to ask "should I continue?". Stop ONLY at the ~50%-remaining compact trigger (on long work, auto-`/handover` + compact + continue; never below 50% remaining), a destructive action needing consent, genuinely stuck, or task complete.21- **AP #18 Sample-Invocation**: a real end-to-end shoot + compare + drift on one live page IS the Workflow/E2E-tier verification. Keep the per-repo `.design-fidelity.json` + the produced numbers as the evidence.2223## Operator Prerequisites2425- **playwright + chromium**: per-repo `.venv/bin/python -m playwright install chromium` (or global). WSL works out of the box with the bundled `chromium_headless_shell` — no Windows Chrome, no extra flags.26- **ImageMagick**: `compare`, `convert`, `identify` on PATH. On a host whose `policy.xml` denies the PNG coder, `pixel-drift.sh`'s probe guard fails loud — re-allow the PNG coder.27- **A reachable base_url**: the operator runs the local build server (e.g. `hugo server`) OR supplies a live URL. The skill does not own the server lifecycle by default (an opt-in `hugo` block exists for convenience).28- **A per-repo `.design-fidelity.json`** (optional): the page→path map, viewports, and out_dir for that repo. The sample values are an EXAMPLE, never a default.2930## When to Invoke3132- "Screenshot this page / the whole site at desktop and mobile."33- "Compare my local build against the live site and tell me what's off."34- "Match the design of <live site> — measure how close we are and what to change."35- "Show me the pixel-diff drift between local and live for the home page."36- Greenfield capture-only (no live reference yet): just shoot the build for review.3738## Mandatory Reading on Invoke39401. `references/playwright-fallback.md` — the canonical capture + "Live computed-CSS reads" + "Pixel-diff scoring" mechanics (command bodies live HERE, not in this file).412. `references/chrome-devtools-mcp.md` — the MCP route + the browser-state security note.423. The per-repo `.design-fidelity.json` if it exists (page map / viewports / out_dir).434. The screenshots output directory for the active repo under `~/DevProjects/screenshots/<repo>/` (where captures and diffs land — OUTSIDE the repo).4445## The Per-Page 3x Iterate Loop4647Work ONE page at a time. Each iteration runs this concrete dual-URL recipe:48491. **Shoot both sides at both viewports.** Shoot the LOCAL build (`shoot.py` with `base_url` = the local server) for the target page path at desktop (1440) + mobile (390), and shoot/cache the LIVE reference (`shoot.py` with `base_url` = the live base) for the same path at the same viewports. Capture local and live at identical viewport widths so the drift number is not inflated by stretch.502. **Measure the gap.** Run `compare_computed_style.py --local-url <local> --live-url <live> --selector <target>` on the selector(s) you are matching (exact spacing / colour / font deltas per viewport), AND run `pixel-drift.sh <local.png> <live.png>` on the PNG pair (a measurable AE + closeness% + a diff-highlight image).513. **Tweak and re-shoot.** Apply the smallest CSS change the deltas point to, rebuild the local site, then re-shoot and re-measure.5253Repeat at most 3 times per page. Watch the drift number trend DOWN and the computed-CSS deltas shrink toward zero. Then move to the next page.5455## Sub-Commands5657- **`/design-fidelity shoot`** — capture only (`shoot.py`). Works STANDALONE with no live reference (greenfield repo): just produces full-page desktop+mobile screenshots for review.58- **`/design-fidelity compare`** — run the full per-page loop above against a live reference: `shoot.py` (local + live) → `compare_computed_style.py` → `pixel-drift.sh`. Fires ONLY when a live reference URL is supplied.59- **`/design-fidelity diff`** — re-run `pixel-drift.sh` on an existing local/live PNG pair to re-measure drift after a tweak.6061## Triple-Check Discipline6263> Maps to the Three-Tier Testing Framework (SST3 `STANDARDS.md` "Three-Tier Testing Framework").6465- **Unit Tier**: `tests/run_unit_tests.sh` asserts each helper's contract (config-merge + parsers, the null-selector `_error` path, and the pixel-drift output binary) with no browser/live-site dependency.66- **Workflow Tier**: a real `shoot.py` capture run against a reachable `base_url`, exit 0, with the expected PNGs written outside the repo.67- **E2E Tier**: a full shoot + compare + drift on one live page end-to-end, producing a real drift number + diff image — the AP #18 sample-invocation.6869## Security: chrome-devtools browser state7071When you drive a LIVE site via the chrome-devtools MCP (the alternative to the headless playwright route), the MCP runs with full browser access — it can read open-tab cookies, auth tokens in request/response headers, pasted secrets in form fields, and cached credentials surfaced by the page (live browser state). Disable chrome-devtools via `/plugin` BEFORE pasting any secret. The DEFAULT route in this skill is headless playwright, which carries no live browser state, so prefer it. Full detail: `references/chrome-devtools-mcp.md` "Security note".7273## Anti-Overfit Constraint7475The pixel-drift number is a TREND diagnostic — the operator watches it decrease across iterations; it is NEVER a hard pass-gate. The target is a STRUCTURAL match, not pixel-perfection (anti-aliasing, font-hinting, and dynamic content make pixel-perfect impossible and pointless). The page→path map, `base_url`, and viewport list are PER-REPO config — the proto values are an example, NOT a default.7677## File Locations7879| File | Location |80|------|----------|81| Capture helper | `scripts/shoot.py` |82| Computed-CSS delta helper | `scripts/compare_computed_style.py` |83| Pixel-drift helper | `scripts/pixel-drift.sh` |84| Unit tests | `tests/run_unit_tests.sh` |85| Capture / computed-CSS / pixel-diff mechanics | `references/playwright-fallback.md` |86| MCP route + browser-state security | `references/chrome-devtools-mcp.md` |87| Per-repo config | `<consumer-repo>/.design-fidelity.json` |88| Screenshot output (OUTSIDE any repo) | `~/DevProjects/screenshots/<repo>/` |8990## Skill Surface Contract9192- Inputs: a `base_url` (local build or live), a page→path map, viewports, and optional selectors. Via `.design-fidelity.json` and/or CLI overrides.93- Outputs: full-page PNGs + diff-highlight PNGs under `~/DevProjects/screenshots/<repo>/`; computed-CSS deltas + drift numbers on stdout; decision-branch telemetry on stderr.94- Never writes into the consumer repo tree; never deploys; never edits the live site.95- Exit codes are load-bearing (AP #16): the helpers fail loud on a missing dependency, a blocked PNG coder, or a missing selector rather than producing a silent empty result.96- **The `.design-fidelity.json` config is treated as UNTRUSTED** (it is repo-local, so a cloned repo could ship a hostile one). `shoot.py`: the opt-in `hugo.cmd` launcher spawns NOTHING unless the operator passes the explicit `--allow-hugo-launcher` CLI flag (config alone can never start a process); when enabled it allowlists the executable to STATIC dev-servers only (hugo/jekyll/vite/serve/http-server — bare PATH names, no `./hugo` path component — and no interpreters or `npm run`-style wrappers) and rejects inline-code/eval flags; `base_url` is http(s)-only (no `file://`); page labels + viewport names must be `[A-Za-z0-9_-]+`; viewport width/height must be ints in `1..8192`; `scroll.step` must be a positive int (settle clamped); `out_dir` must resolve strictly UNDER `$HOME` or the system temp dir. Each violation is a loud exit 2.97- **Residual trust the threat model deliberately does NOT remove — only pass `--allow-hugo-launcher` on a repo you TRUST**: (1) `vite` and `jekyll` execute the repo's OWN build config (`vite.config.{js,ts}`, Jekyll `_config.yml`/`_plugins`) by design when they start, so launching them runs that repo's build code; `hugo`/`serve`/`http-server` do not. (2) `base_url`/page paths are scheme-restricted to http(s) but NOT host-filtered (the tool MUST reach `http://127.0.0.1:<port>` for local builds), so a hostile config can point captures at internal/loopback/cloud-metadata HTTP endpoints (e.g. `169.254.169.254`) — do not run an untrusted repo's config on a host with sensitive internal HTTP reachability.