See every visual change a pull request makes, and confirm it is the change you meant.
StyleProof opens the UI states you care about in a real browser, records the computed CSS of every element on the
base branch and on the pull request, and posts what changed as a PR comment. The reviewer, a person or a coding agent,
reads the report and confirms that it shows the visual change the pull request set out to make. Approve the changes
you meant. In review-gate mode, the StyleProof check stays red until a reviewer approves them.
This is the comment StyleProof posts on a pull request. In this example the PR changed one button's padding and background colour, and one link's hover, focus, and active colours. What the report tells you walks through it.
How this example was made: a real run, not a mockup
scripts/live-readme-report.mjs captures example/demo/index.html in Chromium twice, injecting a
fixed CSS change between the captures, then renders the report and the PR comment it would post. The
block is that comment, with three edits for this page: crop links repointed at
docs/readme/live-report/crops/, element-level sections reordered so Save at rest comes before the
Docs hover, focus, and active states, and report.json left out of the committed bundle. Every
finding, value, and crop is what the run produced. Both sides of each interaction-state crop are in
that state.
1 change needs review
1 distinct change (2 computed-style difference(s) · 3 state-delta difference(s)) in 1 changed surface base with an existing baseline.
demo-button @ 900
padding 14px 28px → 18px 32px
background-color #14b8a6 → #dc2626
◀ before · after ▶ — demo-button @ 900
Btn button.btn
Style:
| Property | Before | After |
|---|---|---|
padding |
14px 28px |
18px 32px |
background-color |
#14b8a6 |
#dc2626 |
demo-button @ 900
Both sides are :hover. Left is the old :hover. Right is the new :hover.
:hover color #a5f3fc → #fca5a5
◀ base :hover · head :hover ▶ — both sides are :hover
Link a.link
Interactive-state changes:
| State | Property | Before → After |
|---|---|---|
:hover |
color |
#a5f3fc → #fca5a5 |
:hover |
↳ caret-color (currentColor) |
#a5f3fc → #fca5a5 |
:hover |
↳ outline-color (currentColor) |
#a5f3fc → #fca5a5 |
:hover |
↳ column-rule-color (currentColor) |
#a5f3fc → #fca5a5 |
:hover |
↳ row-rule-color (currentColor) |
#a5f3fc → #fca5a5 |
:hover |
↳ text-decoration-color (currentColor) |
#a5f3fc → #fca5a5 |
:hover |
↳ text-emphasis-color (currentColor) |
#a5f3fc → #fca5a5 |
:hover |
↳ -webkit-text-fill-color (currentColor) |
#a5f3fc → #fca5a5 |
:hover |
↳ -webkit-text-stroke-color (currentColor) |
#a5f3fc → #fca5a5 |
demo-button @ 900
Both sides are :focus. Left is the old :focus. Right is the new :focus.
:focus outline-color #5eead4 → #fca5a5
◀ base :focus · head :focus ▶ — both sides are :focus
Link a.link
Interactive-state changes:
| State | Property | Before → After |
|---|---|---|
:focus |
outline-color |
#5eead4 → #fca5a5 |
demo-button @ 900
Both sides are :active. Left is the old :active. Right is the new :active.
:active color #2dd4bf → #f87171
◀ base :active · head :active ▶ — both sides are :active
Link a.link
Interactive-state changes:
| State | Property | Before → After |
|---|---|---|
:active |
color |
#2dd4bf → #f87171 |
:active |
↳ caret-color (currentColor) |
#2dd4bf → #f87171 |
:active |
↳ outline-color (currentColor) |
#2dd4bf → #f87171 |
:active |
↳ column-rule-color (currentColor) |
#2dd4bf → #f87171 |
:active |
↳ row-rule-color (currentColor) |
#2dd4bf → #f87171 |
:active |
↳ text-decoration-color (currentColor) |
#2dd4bf → #f87171 |
:active |
↳ text-emphasis-color (currentColor) |
#2dd4bf → #f87171 |
:active |
↳ -webkit-text-fill-color (currentColor) |
#2dd4bf → #f87171 |
:active |
↳ -webkit-text-stroke-color (currentColor) |
#2dd4bf → #f87171 |
Nothing else — no other element or inventory changes in this compare.
- Approve all changes
Tick Approve all changes to turn the StyleProof check green — write access required, and not the pull request author. One tick signs it off. A new push that changes styles or surfaces re-opens it. Quickstart · Read the catch contract
- The headline says how many distinct changes need review. Here that is one change, made of 2 computed-style differences and 3 interaction-state differences on one page.
- Each card is one changed element. It names the element (
button.btn), the page and viewport width it was captured at (demo-button @ 900), and the property changes in one line. - The before/after crop shows the element on the base branch and on the pull request. Cards for
:hover,:focus, and:activeshow both sides in that state. A highlight overlay marking the change is one click away. - The table lists every changed computed property with its old and new value. Properties that follow
currentColorare nested under the colour that drove them. - Warnings appear only when something is wrong, such as a page that was not captured or captures that may not show the same product state. A fully certified run keeps that evidence collapsed.
- Approve all changes turns the
StyleProofcheck green once a reviewer with write access, other than the PR author, ticks it. A later push that changes styles or surfaces reopens it.
The check compares the browser's computed CSS, not pixels. The crops are there so a person can see the change. The
pass or fail decision runs on the exact values in the table. Surface-key vocabulary (@width, live-state and popup
variants) is in docs/REFERENCE.md.
Where the report lives. On a pull request, the PR comment is the same linked summary for public and private repositories. Reports publish as workflow artifacts by default. The comment links the artifact on the run, and nothing is written to repository git history. Images travel inside the self-contained HTML.
Report artifacts contain only report.html, report.md and report.json. Open HTML offline for embedded images. Set report-max-bytes (default 2000000) to cap their combined size; HTML gets what Markdown (400 KB cap) and JSON (450 KB findings cap) leave. For pre-existing receipts that exhaust the combined budget, the library, CLI and Action retain them and apply the cap to HTML when the remaining bytes cannot hold report text; HTML explicitly announces this combined-budget overrun. Crops may be downscaled to fit the 300 KB per-image limit. Full findings remain in the original diff input, which is not included in the report artifact; preserve that input separately. Omissions are named; capped JSON adds findingsBudget and positive per-surface findingsOmitted counts. Local reports support --report-max-bytes and --html-max-bytes. Branch storage keeps crop files.
Reports cap crops at 60 files and 900,000 bytes across all surfaces. Crops over 300 KB are downscaled, then omitted if they still exceed the per-file cap. Local reports support --max-crop-files and --max-crop-bytes (0 disables crops); omissions are named, and every change remains listed and counted.
Every report viewer needs an authenticated GitHub session, because artifact downloads are never anonymous.
report-storage: branch opts into the styleproof-reports orphan branch for an in-browser rendered report, with
anonymous access on public repositories only. PR reports use pr-<n>/; runs without a resolved PR use
run-<run_id>/. Branch sweeps prune closed PR reports and dated run reports oldest first, protecting open PRs
and folders whose age is unknown. If publication or receipt verification fails, StyleProof posts no delivery claim for that run.
Use StyleProof when a PR can change CSS, design tokens, component classes, layout, or hidden/open UI states and you want CI to say whether the browser's rendered styles actually changed. Unit and e2e tests prove behavior; StyleProof proves the visual contract for the states you declared.
It catches:
- a button recoloured by a token, utility class, CSS module, inline style, or design-system change;
- a layout shift at one breakpoint but not another;
- a dropped
:hover,:focus, or:activestyle; - a modal, menu, listbox, popover, sheet, or toast whose open state changed;
- a supposedly no-op refactor, such as CSS-to-Tailwind, that changed rendered output;
- a restyle on an element the PR also re-nested (a wrapper added or removed), paired back by geometry so the structural churn cannot hide it;
- a required route, component, or UI state that exists but has no capture.
The end-to-end catch contract lives in docs/what-it-catches.md.
- A surface is one UI state to certify: a route, tab, modal-open state, dropdown-open state, toast-visible state, loading state, etc.
- You list or auto-discover surfaces in a Playwright-style spec.
- StyleProof opens each surface at real breakpoint widths and records computed styles for every captured element.
- On a PR, it compares base vs head and reports exactly which rendered styles changed.
- The PR gets a
StyleProofstatus: green when nothing changed, red until someone approves intentional changes, or failing when certification mode is configured.
Selective remap
is opt-in. A changed file reached only through a computed import re-captures
every surface; recovered imports can widen a resolved dependency, but cannot
make an unplaced change selective. Changes reaching a conventional framework
wrapper (Next layout/_app, SPA App/main, Remix root, SvelteKit
+layout, Nuxt app.vue/layouts/, Gatsby gatsby-browser) before a declared
surface also re-capture everything. SPA page changes stay selective, and test, story,
script, or undeclared-route importers alone do not force a full re-capture.
StyleProof is not a screenshot diff. Screenshots appear in the report so humans
can see the change, but the gate compares browser-computed CSS: resolved
longhands, pseudo-elements, layout boxes, motion longhands, and forced
:hover/:focus/:active deltas.
Forced-state reads share a browser round trip with their liveness check and style flush. Each reset still flushes before the next state or target; captures remain serial, with the same coverage and resource limits.
Nested overflow scrollers are expanded for full-page report screenshots. <body>
is a candidate only when it scrolls itself: the root's computed overflow-y must
not be visible, which would propagate body overflow to the viewport. Capture
keeps rect in the driven page's document coordinates for correspondence and layout
comparisons; optional screenshotRect locates the element in the expanded image.
Crops and pixel attribution prefer screenshotRect, falling back to rect.
Recapture nested-scroller baselines for aligned evidence.
When a surface scrolls the document, capture also records the viewport's scroll
origin, the driven document's scroll height, and viewport.screenshotHeight measured
with screenshot scrollers expanded. A non-hidden element addition or removal requires
review if either its non-zero driven rect intersects the original driven bounds,
or its non-zero screenshotRect ?? rect intersects the screenshot bounds. Both use
horizontal bounds scrollX to scrollX + width and vertical top min(0, scrollY).
The driven bottom remains max(scrollY + height, documentHeight ?? Infinity);
the screenshot bottom is screenshotHeight. The gate is never laxer than before:
more additions/removals can become reviewable after expansion.
When any scroller is expanded, elements inside unexpanded scrollers can also get
screenshotRect; the gate does not infer per-ancestor clipping. Missing or invalid
(non-finite or non-positive) screenshotHeight fails closed with an unbounded
bottom, even with documentHeight. Hidden and zero-opacity additions stay advisory;
zero-size additions stay advisory when neither box has non-zero size.
Horizontal-scrolled changes retain report crops.
Older maps lack the origin and assume zero; recapture affected scrolled baselines
after upgrading.
StyleProof requires Node 22.13 or later, locally and in CI.
StyleProof 8 runs in CI. Upgrade the npm package and the report Action to major 8 together, then refresh and verify the generated workflows with the same server/spec options you used at install:
npm install --save-dev styleproof@^8
npx styleproof-init --upgrade
npx styleproof-init --check--upgrade moves the generated workflows to @v8 and removes the pre-push hook
an earlier release generated. Under --storage branch it also adds the
publish-base job, which publishes each default-branch commit's map from CI.
styleproof-map keeps a local capture local; pass --upload when a script of
yours publishes to the map store. See the
8.0.0 changelog.
Upgrade the npm package and the report Action to major 9 together, then refresh and verify the generated workflows:
npm install --save-dev styleproof@^9
npx styleproof-init --upgrade
npx styleproof-init --check--upgrade moves the generated workflows to @v9. Update any hand-written
uses: BenSheridanEdwards/StyleProof@v8 or styleproof-approve-reusable.yml@v8
pin to @v9 too: the Action runs its own copy of StyleProof, so an @v8 pin
keeps the 8.x gate. Then re-capture the baseline once. See the migration list
in the 9.0.0 changelog.
Empty hash home routes (#/, #!/, including a query) count as the page without a fragment, so home links do not capture the root twice.
For a defineCrawlCapture root that lands on a hash route such as /#/home,
replace the old index key with home in crawl expected / exclude lists
and acknowledgement files. Hash-route fragments stay verbatim: if your app
treats #/settings and #/settings/ as aliases, render one canonical link or
use defineCrawlCapture's match option to select one form.
npx styleproof setupThat one command detects npm, pnpm, Yarn, or Bun; installs StyleProof and
Playwright; installs Chromium; scaffolds the capture spec, a dedicated
production-build Playwright config, and one GitHub workflow; then verifies
every machine-owned file against the installed release. The default scaffold is
the smallest honest gate: one pull_request job captures base and head in the
same run and diffs them — maps live as workflow artifacts and the gate runs
entirely in CI — and it runs advisory: it reports evidence on every
PR without blocking. Preview the exact operations without writing with
npx styleproof setup --dry-run. In a monorepo, target the consumer
application explicitly:
styleproof setup --project-dir apps/web--project-dir changes where dependencies are installed and all setup commands
run. --dir remains the capture-spec path inside that project, for example
--project-dir apps/web --dir e2e/styleproof.custom.spec.ts.
Opt-in flags reproduce the heavier architectures:
--workflow split— a read-onlypull_requestcapture job plus a trustedworkflow_runreport job. Required when the repository accepts fork or Dependabot PRs, whose tokens are read-only. Fork verdicts are advisory: the fork's code produces both map sets, so a fork PR'sStyleProofstatus stayspendinguntil a maintainer ticks Approve all changes — never green automatically.--storage branch— the SHA-keyedstyleproof-mapsstore branch, a CI job that publishes each default-branch commit's map, so CI restores maps instead of recapturing them.--mode certify/--mode review-gate— fail on any diff, or hold theStyleProofstatus red until a reviewer approves (scaffolds the approval workflow). Under--workflow split, the failing report job runs on the default branch, so certify also sets the PR'sStyleProofstatus red on any diff; certify approves nothing, so a fork PR's status stayspending.split + branch + review-gatereproduces the pre-v7 scaffold exactly.
Existing scaffolds keep their architecture: a marker in the generated workflow
records the chosen mode, and styleproof-init --check / --upgrade verify
that mode's file set instead of rewriting it. To switch an existing scaffold,
pass the new flag to --upgrade, for example
styleproof-init --upgrade --mode review-gate. A plain re-run with a different
flag stops before writing anything.
In cache-first CI, ancestor baseline reuse requires no capture-relevant changes.
Every supported styleproof.config.ts / .mjs / .js / .json is relevant,
including package configs; source paths retain Unicode and control characters,
and renames check both locations. A relevant change forces a fresh base capture.
After a cold base capture, CI can reuse head npm dependencies when the consumer started at head,
package metadata is unchanged, and npm's hidden lockfile matches (allowing platform-excluded optional packages).
This does not verify install scripts ran; cached or --ignore-scripts installs are used as-is.
Parallel head captures run largest base maps first; parallel: false and report ordering stay unchanged.
The map CLI evaluates JavaScript and TypeScript config in a separate process, so cold-base npm checkouts can use their own Playwright installation without conflicting with the head CLI. JSON config loads without an extra process. Config errors retain concise messages without a Node stack trace.
JavaScript config (styleproof.config.mjs or .js) also supplies coverage and
product-state policy to capture specs, diff, and report. It takes precedence over
sibling JSON; evaluation errors and invalid policy fail closed. A .js file
follows its package's Node module format, and dynamic policy sees the caller's
working directory.
styleproof capture # capture this commit from the generated spec
styleproof crawl <url> # direct URL or rendered-nav crawl
styleproof compare [base] # fail-closed base/head comparison
styleproof report [base] # generate the review report on command
styleproof variants # inspect surface/state variants
styleproof affected # resolve surfaces affected by source changes
styleproof ci # CI orchestration: restore probes, cold captureCapture workers default to max(ceil(cpus / 2), cpus - 2), using Node's
os.availableParallelism() (12 CPUs → 10 workers; 4 → 2; 2 or 1 → 1).
Set STYLEPROOF_WORKERS to a positive integer to override this. Explicit
Playwright --workers / -j arguments take precedence over the environment;
otherwise the environment overrides top-level workers in the Playwright config.
Without an environment override, config workers are preserved, and an unloadable
config also keeps Playwright's own worker selection.
Cold-base CI captures reuse the head’s StyleProof tarball or Git spec (local tarball paths resolve against the head package directory). Git branch and tag refs are resolved again and may change; use a full commit SHA to pin the build. Registry declarations, directory artifacts, link: specs, and missing consumer manifests use the running CLI’s exact version.
If the map capture process is terminated by a signal, CI stops and cleans up its temporary worktrees. An interrupted baseline does not start a head dependency install or capture; the command exits with the signal's conventional exit code (130 for SIGINT, 143 for SIGTERM).
Diff and report operations reuse parsed maps across their audits, including catalogues larger than the 128-map process cache. The operation cache is released on success or failure.
Run styleproof --help for the whole journey or styleproof <command> --help
for command-specific options. The existing styleproof-* binaries remain as
backwards-compatible aliases for one major version.
Capture directories recognize only <key>@<width>.json and <key>@<width>.json.gz
map filenames (a nonempty key and integer width). Other JSON files are ignored.
Writing diff or audit JSON beside captures changes the directory's evidence digest
for later diff or report runs; keep outputs outside capture folders when matching evidence receipts.
npx styleproof capture # capture this commit's computed styles
npx styleproof compare # compare against the base branchcapture writes the current commit's map under .styleproof/maps/current
with a manifest; compare restores base and head and fails closed when the
evidence cannot certify. Maps travel as workflow artifacts by default —
never committed to the PR branch — or via the opt-in styleproof-maps store
branch. Nothing under .styleproof/ belongs in a commit.
compare --capture-base opts into capturing missing maps locally, installing dependencies in a temporary detached base worktree under <repo>/.worktrees/ and removing it before processing the head. Without a stored head bundle for HEAD, head is captured in the current checkout, preserving uncommitted edits; otherwise the stored bundle wins. Stop any app server first so both captures use their own builds. The flag is off by default and cannot be enabled through config or used with two explicit directories.
During base install/capture, compare ignores SIGINT/SIGTERM and follows the child; Ctrl-C (process-group SIGINT/SIGTERM) exits 130/143 after cleanup. SIGTERM sent only to compare is ignored: container/systemd stops that signal only PID 1 wait for the child or escalate to SIGKILL. Interrupts at other moments, SIGHUP, SIGKILL or a crash can leave <repo>/.worktrees/styleproof-base-* behind; the next --capture-base run removes only hour-old, current-user scratch dirs with confirmed dead owners. The base checkout is nested under the repository: Node module resolution, postcss/browserslist config and pnpm workspace detection can pick up head root files when the base lacks its own.
compare restores the base map from the map store, so on a local checkout without
one it stops and prints the local steps. Capture this checkout as head, then the
base from a clean worktree, so uncommitted changes never leak into the base
(switching branches would carry them along and hide them from the diff):
npx styleproof capture --dir head
git worktree add --detach .styleproof/base-checkout origin/main
(cd .styleproof/base-checkout && npm ci && npx styleproof capture --base-dir ../maps --dir base)
git worktree remove --force .styleproof/base-checkout
npx styleproof-diff .styleproof/maps/base .styleproof/maps/headStop any server already running on the app's port first: outside CI Playwright reuses it, so both captures would see the same build.
For the hand-wired Action form, the fork/Dependabot split, the map-store and report branches, the CI base publisher, and every config key and flag, see docs/REFERENCE.md.
The report Action defaults to comparison-base: pull-request-base: it expects
captures from the trusted PR base tip B and captured head H, without a comparison
API call. To compare the fork point instead, explicitly set
comparison-base: merge-base on an Action ref containing this feature. It derives
A from GitHub's immutable B...H comparison, then requires genuine A/H captures.
This does not change the capture workflow or relabel existing maps. See
merge-base setup for producer/verifier
agreement, private-repository permissions, stale-history limits and fork approval.
Action jobs reaching the approval gate (review-gate, advisory, migration, require-approval)
require actions: read to verify the issue-comment approval run.
The scaffold's default gate is advisory — the evidence posts on every PR without blocking. These are the two gate modes you opt into:
Review-gate mode (require-approval: true) is for normal feature work:
every visual change is reported with evidence, the StyleProof status stays red
until a reviewer ticks Approve all changes, and approved changes become the
new baseline on merge. A surface that exists only on the PR head is still
reviewable: it holds the status red until approved, then becomes part of the
baseline once merged.
In both the reusable and example approval workflows, re-running an approve run by anyone other than the original ticker is refused; tick the approval box again.
Certify mode (fail-on-diff: true) proves a change touched nothing
visual: any difference at all fails the job. It's the job StyleProof was born
for — certifying a CSS-to-Tailwind migration rendered byte-for-byte identical.
Reach for it on any change whose whole promise is "the output is unchanged": a
utility-class migration, a design-system swap, a dependency or build-tooling
bump. Zero diff is the contract; one drifting longhand is a regression to
investigate, not a change to approve.
When running multiple Action instances, pass each instance's comment-marker
to the reusable approval workflow as well (default <!-- styleproof-report -->).
Approval uses it to select the matching comment and report artifact.
There's also a third, spec-less use — pointing the one-shot capture at a design mockup and diffing your build against it until the number hits zero. See Match a design pixel-for-pixel.
A passing check is more than "no style changed". Four gates qualify it, and the report leads with their verdicts:
- Coverage — the
expectedregistry travels with the captured bundle as a ledger, so the gate states its completeness basis:✓ coverage complete,✗ coverage INCOMPLETE(blocks — a registered surface wasn't captured, even on an empty diff), or✗ completeness NOT asserted(no registry / filtered capture — blocks certification unless--allow-unasserteddiagnostic mode). - Determinism — the ledger records how each capture proved itself
(
self-checked/replayed); a green from anunprovenor unknown capture blocks — including areplayedrun where any surface had no replay HAR and was captured live (the map records it) — because a clean diff of two nondeterministic (or pre-ledger) reads could just be luck. Pass--allow-unassertedonly for explicit diagnostic compares (certifiesFully: falsein JSON). When stylesheet-free UA defaults differ between base and head, the CLI warns on stderr with the affected surface count and up to three distinct tag.property changes, plus an overflow count. Only tags and properties recorded on both sides are compared, since defaults are collected lazily. This host drift warning is advisory; findings remain except for proven placeholder reflow echoes. - Inventory — with
inventory: true(on instyleproof-initscaffolds), each capture harvests the surface's navigable affordances (links, tabs, menu items, keyed by stable identity, not label text). A removal that makes a feature unreachable gates — in thestyleproof-diffCLI and in the Action, both modes — until acknowledged instyleproof.inventory.json({"<key>": "<why>"}); a stale acknowledgement is flagged so the ledger can't rot. Details and the keying rules: docs/inventory-guard.md. Make it advisory in the Action with"gateInventoryRemovals": falseinstyleproof.config.json. Withoutinventory: truethe check does not run, and StyleProof says so: the report reads⚠ not checkedrather than✓ navigable set unchanged, and the Action warns that the gate could not run. An empty inventory diff is indistinguishable from "nothing was removed", so a ✓ there would claim a guarantee the run never earned. - Failed data request: a data-boundary request that failed during capture
means the fallback UI was captured, not the state its responses drive.
Gating is the default (
dataResidue: 'gate'): an unacknowledged failing endpoint blocks until declared instyleproof.data-residue.json, and a stale declaration also fails. Failures from the surviving document remain attributable after a canceled navigation or download followed by SPA navigation. Opt down withdataResidue: 'warn'. See Failed data request. - Product-state identity — a pair with matching
productState {id, revision}is comparable and can certify (the only certifying path). Pairs with no declared product state stay on the visual-review path until you arm the declare gate: addstyleproof.product-state.json({"<surface>": "<why>"}), pass--legacy-pairs, or setproductState.requireIdentity: true/--require-state-identity. StyleProof's own live dogfood stampshome@*with matching identity and keeps the ledger file armed but empty so new undeclared pairs cannot stay green. Armed and undeclared → fail closed (CERTIFICATION_FAILED) in the CLI, Action verdict, and PR comment. Armed and declared → explicit advisory, never a certified green. Details: docs/product-state-comparability.md.
Those verdicts roll up into one more line the report always states: the
confidence ledger (styleproof-confidence.json, bundled next to the maps).
It assigns every surface one status — captured, excluded-with-reason,
inaccessible (an auth wall or blocked continuation), unknown (declared but
never captured), or unproven-determinism — and renders a completeness badge
(✓ complete, ⚠ limited, ⚠ unasserted, ⚠ unknown) separate from the
visual verdict: a visual PASS and a complete capture are two claims, never one
green. Crawl captures persist the ledger themselves (auth walls travel with the
bundle); spec captures derive it from the coverage ledger; bundles from before
the ledger existed read ⚠ unknown and are never blocked retroactively. No
coverage percentage is ever invented for surfaces that cannot be enumerated.
The same summary lands machine-readably in report.json (confidence).
StyleProof certifies only what it captured: a surface that was never captured
has no base and no head map, so its change can never appear in a diff and the
gate stays green having never looked at it. The coverage guard
(expected/exclude), the confidence ledger, the inventory guard, the
data-residue gate, and product-state identity exist to make every one of those
gaps loud instead of silent — full rules in
docs/REFERENCE.md.
The sharpest form of the boundary: maps prove only the states the spec
exercises. A restyle confined to a conditional render branch the fixture never
drove — a fault overlay, an empty state — produces byte-identical honest
captures on both sides. Every conditional branch whose styling matters needs a
surface that exercises it; see
the un-exercised-state gap
and what the crawler can and cannot reach.
With captureReach and changeReach: 'gate', a changed CSS rule no captured
state matched fails certify mode and needs approval in review-gate mode.
A changed stylesheet no capture loaded, such as a lazy route's or code-split chunk's CSS, is not checked yet: the report labels it Not checked, gives capture guidance, and the gate does not hold it.
A matched rule whose declarations lost the cascade, or changed no computed value StyleProof records, in every captured state still reads reached.
Reader failures preserve the capture and record reach unknown with no rule evidence, listed under changeReach: 'warn' and held only under opt-in changeReach: 'gate'.
Rules matched only outside compared elements stay unmeasured and also hold
the opt-in gate. Global state and pseudo-element probes exclude the head subtree and
framework-ignored elements, including the hover sink. Full-page capture preserves inline style attributes, including
on nested scrollers under strict style CSP, so its temporary expansion does not invalidate reach.
Render-code reach (J2/J3) remains a bench prototype; the
Change Reach 0.2 receipt records capture cost and a limited
six-PR noise replay, with its exclusions and labelling limits.
If settling times out with requests in flight, capture continues with a warning and records the request
count in metadata.settleTimedOutWithRequestsInFlight. Settle the page in go() or raise
stabilize.timeout if the capture shows a loading state.
Every flag, config key, Action input, workflow file, hook behavior, exit code, and optional layer lives in docs/REFERENCE.md. Deeper contracts: what it catches · component manifests · inventory guard · product-state comparability · report delivery · setup server contract · forced-state limits · consumer pin bump.
Failed capture setup steps report their one-based step number, action, and author-written
selector (except for goto). Driver URLs, field values, and original causes are omitted.
Repository CI runs the full browser suite in four file-level shards alongside the
unit matrix on Node 22.13.0 (the supported floor), 22, and 24. The e2e (node 22)
aggregate compares completed test IDs against a separately collected full
inventory and requires the five-run determinism receipt. Missing, duplicated,
skipped, failed, or retried tests fail verification.
The e2e-shard-* artifacts are kept for 1 day and used only by the e2e (node 22)
check. Local pnpm run test:e2e still runs the complete suite.
Pull-request CI skips browser lanes only after proving that the checked-out
two-parent merge has the event's base as its first parent and exactly the same
root Git tree as that base. scripts/browser-lanes.mjs makes this decision
without listing filenames or querying the changed-file API. Any tracked content,
name, mode or gitlink difference runs the lanes, including prose and tooling
changes. Unknown or invalid evidence runs them too. Push CI always runs them,
including empty-tree pushes. The live StyleProof dogfood job always runs for
eligible PRs because it compares the PR head with its base, not the merge tree.
pnpm run demo:check compares demo Markdown and HTML plus decoded crop pixels.
The local pre-push check uses the same proof policy over pushed endpoint trees
and the staged index. Git reads disable replacement objects and remove local Git
environment overrides. An up-to-date or deletion-only push may skip browsers
only with an unchanged staged tree; first pushes and missing metadata run them.
A file deletion still changes the tree. Missing Chromium or malformed push
records block the push; unreadable Git evidence runs browsers or blocks it.
The browser command checks the current checkout. Install Chromium with
pnpm exec playwright install chromium before pushing changes.
See CONTRIBUTING
for the dev loop, and .agents/README.md
(where AGENTS.md and CLAUDE.md point) for the operating rules and agent tooling. The repo
is wired for Claude Code with Ponytail (default lazy-coding mode), GitNexus
(code-intelligence graph — MCP server in .mcp.json, skills in
.claude/skills/gitnexus/), and Graphify (/graphify knowledge graph). The
GitNexus index (.gitnexus/) and Graphify output (graphify-out/) are gitignored;
build the index with npx gitnexus analyze --skip-agents-md.
MIT © Ben Sheridan-Edwards







