styleproof 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,86 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.1.3] - 2026-06-29
11
+
12
+ ### Added
13
+
14
+ - **Optional automatic popup capture.** `defineStyleMapCapture` and
15
+ `defineCrawlCapture` now accept `popups: true` or `{ max, triggers, overlays,
16
+ timeoutMs }`. When enabled, StyleProof clicks visible safe triggers after each
17
+ base surface capture and saves persistent dialogs, popovers, menus, listboxes,
18
+ tooltips, and open data-state overlays as `<surface>-popup-XX` captures with
19
+ popup labels in reports.
20
+ - **Local-first reusable map bundles are now the default v3 flow.** `styleproof-map`
21
+ writes `.styleproof/maps/current`, records a `styleproof-manifest.json` with the
22
+ commit SHA and capture compatibility key, and uploads the bundle to a dedicated
23
+ `styleproof-maps` branch when the working tree was clean and a git remote is
24
+ available. The upload happens from the explicit `styleproof-map` command, not
25
+ from a git hook, so generated maps no longer enter PR branch history.
26
+ - **The report now surfaces tiny changes by default.** Every changed region shows
27
+ the clean before/after crop **and** a highlighted twin (magenta boxes marking each
28
+ change) without expanding anything, and names the changed element next to the image
29
+ (e.g. `changed: span.caret`). When the changed element's footprint is small
30
+ (≤ `zoomBelow`, default 64px) a magnified zoom crop is added so a sub-pixel change
31
+ (e.g. a 2px caret bump) is obvious at a glance instead of hiding in a collapsed
32
+ section. New `zoomBelow` report option tunes or disables it. ([#97](https://github.com/BenSheridanEdwards/StyleProof/issues/97))
33
+
34
+ ### Changed
35
+
36
+ - **`styleproof-init` no longer generates or activates a pre-push hook.** It now
37
+ scaffolds `.gitignore` cache entries and a cache-first PR workflow that restores
38
+ base/head maps from `styleproof-maps`, runs the full StyleProof report action
39
+ when both bundles are present, and recaptures both sides in CI only on cache miss
40
+ or incompatible cache.
41
+ - **`styleproof-diff` and `styleproof-report` now default to cached maps by commit
42
+ SHA.** No-arg and single base argument usage restores base/head bundles from the map
43
+ store, while explicit `<beforeDir> <afterDir>` usage remains for CI fallback
44
+ captures and custom comparisons. The old committed-map `--base-ref`/`--maps-dir`
45
+ mode is removed from v3 so generated maps no longer have any supported path into
46
+ PR branch history.
47
+
48
+ ### Fixed
49
+
50
+ - `styleproof-diff` and report generation now ignore `styleproof-manifest.json`
51
+ when discovering surface map files, so manifest-backed bundles do not get parsed
52
+ as captures.
53
+
54
+ ## [3.1.2] - 2026-06-28
55
+
56
+ ### Added
57
+
58
+ - Documented the sound way to skip StyleProof work: skip the **entire** CI
59
+ workflow for docs-only / non-rendered paths with native path filters, but do
60
+ not skip individual surfaces based on changed-file guesses.
61
+
62
+ ### Fixed
63
+
64
+ - **Live-region detection now respects the surface's `ignore` list.** A region
65
+ the caller ignored was still scanned for `aria-live`/`role` and surfaced (and
66
+ persisted into the committed map) as a live-state candidate, unlike every other
67
+ capture pass which honours the merged `ignore`. Ignored regions are now excluded
68
+ from `liveCandidates` too.
69
+ - **The scaffolded CI workflow no longer blocks PRs that only add new surfaces.**
70
+ `styleproof init` generated a workflow that failed on any non-zero diff exit
71
+ code, so a new-surface-only diff (exit `3`) held the check red and the PR
72
+ receipt claimed "Visual changes detected" — contradicting the documented
73
+ contract that new surfaces never block. The gate now fails only on reviewable
74
+ diffs (exit `1`) and errors (exit `2`), matching `action.yml`, and the receipt
75
+ reports new surfaces explicitly.
76
+ - **A truncated forced-state layer is no longer silently certified as
77
+ identical.** When a surface had more interactive elements than
78
+ `maxInteractive`, capture truncated the `:hover/:focus/:active` layer but left
79
+ `statesSkipped` unset, so the uncaptured states read as "identical" against a
80
+ fully-captured side — the exact false-certification the flag exists to prevent.
81
+ Truncation now sets `statesSkipped`, and the diff reports the layer as "not
82
+ fully captured" on the affected side.
83
+ - **Release workflow no longer passes an empty `body_path` to the GitHub Release
84
+ step.** Previously a release whose CHANGELOG version section was absent (e.g. a
85
+ hotfix tag with no entry) passed `body_path: ''`, which `softprops/action-gh-release`
86
+ treats as a file to read and fails with ENOENT instead of auto-generating notes.
87
+ The step is now split into two mutually-exclusive steps: one with
88
+ `body_path: notes.md`, one with `generate_release_notes: true`.
89
+
10
90
  ## [3.1.1] - 2026-06-27
11
91
 
12
92
  ### Changed
@@ -922,7 +1002,9 @@ number)`), so each viewport band can capture at its own height. Default remains
922
1002
  - `styleproof-diff` CLI: certifies a refactor (exit 0) or names the exact element,
923
1003
  property, and state that drifted (exit 1).
924
1004
 
925
- [Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.1...HEAD
1005
+ [Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.3...HEAD
1006
+ [3.1.3]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.2...v3.1.3
1007
+ [3.1.2]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.1...v3.1.2
926
1008
  [3.1.1]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.0...v3.1.1
927
1009
  [3.1.0]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.0.2...v3.1.0
928
1010
  [3.0.2]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.0.1...v3.0.2
package/README.md CHANGED
@@ -18,7 +18,7 @@ On every PR, StyleProof diffs the PR head's computed-style map against the base
18
18
  - A single **Approve all changes** checkbox in the comment, driving a `StyleProof` commit status: red until one tick signs off every change, green when there are none. The reviewer who ticks it is recorded inline (_approved by @them_), sourced from the commit status so it survives a report re-run.
19
19
  - **Clean runs leave a receipt too.** When no visual changes are detected, StyleProof still creates or updates the PR comment with `✓ No visual changes detected.`, so a green status has a visible record instead of only a silent check.
20
20
  - **New surfaces don't block.** A surface that exists only on the PR head (no baseline to diff — e.g. the bootstrap PR that first adds the capture spec, or a brand-new page) is shown in the report under a `🆕 new surface` heading but never holds the status red and needs no sign-off. It becomes part of the baseline once merged.
21
- - The diff is always **head-vs-base**, so the report is _exactly what this PR changes_ — whether the maps are captured fresh in CI or (by default) committed pre-push and diffed browser-free in CI. See [Quickstart](#quickstart).
21
+ - The diff is always **head-vs-base**, so the report is _exactly what this PR changes_ — whether the maps are restored from the `styleproof-maps` cache or recaptured in CI on a cache miss. See [Quickstart](#quickstart).
22
22
 
23
23
  ## Don't let a new page ship uncaptured
24
24
 
@@ -118,10 +118,12 @@ defineStyleMapCapture({
118
118
 
119
119
  ## What a report looks like
120
120
 
121
- One change — the hero CTA recoloured cyan → amber — appears as a single section in the report: a side-by-side before/after cropped screenshot, a one-line summary, then the exact property change folded under a toggle.
121
+ One change — the hero CTA recoloured cyan → amber — appears as a single section in the report: a side-by-side before/after cropped screenshot, the same crop again with magenta boxes marking exactly what changed, a one-line summary, then the exact property change folded under a toggle. A change too small to see at 1:1 (say a 2px icon tweak) also gets a magnified zoom crop, so a sub-pixel change can't slip past a reviewer.
122
122
 
123
123
  ![A StyleProof report: the CTA button before (cyan) and after (amber), side by side](https://raw.githubusercontent.com/BenSheridanEdwards/StyleProof/main/docs/demo-composite.png)
124
124
 
125
+ 📄 **[See a full live report](docs/demo/report.md)** — rendered by the current code with real images (clean before/after, the highlighted twin, a magnified zoom for a sub-pixel change, and a `🆕 new surface`). It's regenerated and verified on every PR (`npm run demo:report`), so it always reflects exactly what StyleProof produces today.
126
+
125
127
  As it renders in the committed report (a plain-English bullet first — naming the theme token and showing the hex with a live colour swatch — then the exact table inside the toggle). The PR comment itself stays lean — a summary plus the approval box — and links here:
126
128
 
127
129
  ```text
@@ -158,7 +160,7 @@ Requires **Node ≥ 18** (ESM), **`@playwright/test` ≥ 1.40** (peer dep). Forc
158
160
 
159
161
  ## Quickstart
160
162
 
161
- After installing (above), one command sets up — and **activates** — the whole gate:
163
+ After installing (above), one command sets up the whole gate:
162
164
 
163
165
  ```bash
164
166
  npx styleproof-init
@@ -168,8 +170,8 @@ It scaffolds:
168
170
 
169
171
  - a **capture spec** (`e2e/styleproof.spec.ts`) describing your surfaces (a Next.js app gets its routes _and_ the coverage guard wired automatically — see below);
170
172
  - a **`playwright.config.ts`** that builds and serves a **production build** (never a flaky dev server) and captures surfaces **in parallel** (`fullyParallel`);
171
- - a **pre-push hook** (`.githooks/pre-push`, activated for you via `core.hooksPath` — it won't clobber an existing husky setup) that captures the map, restores no-op map churn, commits real map changes as lean `.json.gz` files under `stylemaps/`, and **pushes them with your branch — one `git push`, never two**;
172
- - a **browser-less CI workflow** that diffs your committed map against the base branch.
173
+ - `.gitignore` entries for `.styleproof/`, `test-results/`, and `playwright-report/`;
174
+ - a **cache-first CI workflow** that restores reusable maps from the `styleproof-maps` branch and generates the report without a browser when both maps are already built.
173
175
 
174
176
  Describe your surfaces — **omit `widths`** and StyleProof sweeps your real `@media` breakpoints automatically:
175
177
 
@@ -196,47 +198,76 @@ npx styleproof-map
196
198
  npx styleproof-diff
197
199
  ```
198
200
 
199
- `styleproof-map` captures this branch into `stylemaps/current` as lean committed
200
- maps: HAR recordings are removed by default so private API responses do not land
201
- in git. Keep them only for an explicit record/replay workflow with
202
- `styleproof-map --keep-har` (or `STYLEPROOF_KEEP_HAR=1`). `styleproof-diff` then
203
- compares the committed map against the base branch automatically: in GitHub
204
- Actions it uses the PR base; locally it checks `branch.<name>.gh-merge-base`,
205
- then the current GitHub PR base via `gh pr view` (handy for stacked PRs), then
206
- `origin/main`, `origin/master`, `main`, and `master`. Pin it when needed with
207
- `styleproof-diff main`, `styleproof-diff master`, or `styleproof-diff --base-ref
208
- origin/my-base`.
209
-
210
- **That's the whole loop.** Capture happens **pre-push on your machine** (where the app already builds and serves); the hook first diffs the refreshed map against `HEAD` and restores it when the semantic map is unchanged, otherwise it commits and pushes the lean map; and **CI is a browser-less diff of two precomputed maps** — no build, no browser, just a fast comparison (~5400× cheaper on the compare step than re-capturing both sides in CI). `main` always carries a base map, so every PR is _your code vs the committed base_. Require the `StyleProof` status in branch protection and an unverified visual change can't merge.
211
-
212
- > **Same-environment note.** Computed styles depend on the browser build and installed fonts, so maps are only comparable when captured in the same environment (your machine, or a pinned container). The self-check fails loudly on a non-deterministic capture, so drift never passes silently.
201
+ `styleproof-map` captures the current commit into `.styleproof/maps/current`,
202
+ writes a manifest, and uploads the bundle
203
+ to the dedicated `styleproof-maps` branch when the working tree was clean and a
204
+ git remote is available. Nothing under `.styleproof/` belongs in the PR branch.
205
+ HAR recordings are removed before upload by default so private API responses do
206
+ not land in the map store. Keep them locally only for an explicit record/replay
207
+ workflow with `styleproof-map --keep-har` (or `STYLEPROOF_KEEP_HAR=1`).
208
+
209
+ `styleproof-diff` restores the base and head maps from `styleproof-maps`
210
+ automatically: in GitHub Actions it uses the PR base/head SHAs; locally it checks
211
+ `branch.<name>.gh-merge-base`, then the current GitHub PR base via `gh pr view`
212
+ (handy for stacked PRs), then `origin/main`, `origin/master`, `main`, and
213
+ `master`. Pin the base with `styleproof-diff main` or `styleproof-diff master`.
214
+
215
+ **That's the whole loop.** Build the map outside CI when possible by running
216
+ `styleproof-map` after committing. On the PR, CI first restores the base/head
217
+ bundles and only generates the report — no build, no browser. If either bundle
218
+ is missing or incompatible, CI recaptures both sides in the same pinned
219
+ environment before reporting. Correctness wins over a stale cache, but the hot
220
+ path is report-only.
221
+
222
+ > **Same-environment note.** Computed styles depend on the browser build and installed fonts, so maps are only comparable when captured in the same runtime environment. StyleProof records a compatibility key to select the right cached bundle and refuses to compare maps captured under different browser/platform settings; CI then recaptures both sides instead of producing a bogus report.
213
223
 
214
224
  **Want the local side-by-side report** (not just a pass/fail diff)? Run `npx
215
225
  styleproof-report` after `styleproof-map`; it uses the same inferred base ref and
216
- `stylemaps/current` defaults as `styleproof-diff`. Pin the base with
226
+ the same cached-map defaults as `styleproof-diff`. Pin the base with
217
227
  `styleproof-report main` or keep the manual form with `styleproof-report before
218
228
  after --out report`.
219
229
 
220
- **Want the side-by-side report + one-click approval**? Use the Action, pointing it at the base ref:
230
+ **Want the side-by-side report + one-click approval**? `styleproof-init` scaffolds
231
+ this for you. If you wire it by hand, restore or capture two dirs first, then use
232
+ the Action on those dirs:
221
233
 
222
234
  ```yaml
223
235
  # .github/workflows/styleproof.yml
224
236
  - uses: actions/checkout@v4
225
- with: { fetch-depth: 0 } # need the base ref's committed map
237
+ - run: npx styleproof-map --restore --sha "${{ github.event.pull_request.base.sha }}" --dir base --base-dir __stylemaps__
238
+ - run: npx styleproof-map --restore --sha "${{ github.event.pull_request.head.sha }}" --dir head --base-dir __stylemaps__
226
239
  - uses: BenSheridanEdwards/StyleProof@v3
227
240
  with:
228
- base-ref: origin/${{ github.event.pull_request.base.ref }} # read the base map from git
229
- fresh-dir: stylemaps/current # your committed maps
241
+ baseline-dir: __stylemaps__/base
242
+ fresh-dir: __stylemaps__/head
230
243
  require-approval: true # review-gate mode (omit / use fail-on-diff: true to certify)
231
244
  ```
232
245
 
233
246
  Then copy [`example/styleproof-approve.yml`](https://github.com/BenSheridanEdwards/StyleProof/blob/main/example/styleproof-approve.yml) to `.github/workflows/` **on your default branch** (GitHub only runs `issue_comment` workflows from there, so the approval checkbox is inert until it's merged).
234
247
 
235
- **Prefer to capture in CI instead of committing maps?** For a repo with many outside contributors on different machines, StyleProof can capture **both** base and head in CI and diff them there — no committed maps. See **[Forks and Dependabot](#forks-and-dependabot)** for that flow (it's also the fork-safe split). The committed-map flow above is the default because it's far faster and simpler for a single team.
248
+ **Prefer to always capture in CI?** For a repo with many outside contributors on different machines, StyleProof can capture **both** base and head in CI and diff them there. See **[Forks and Dependabot](#forks-and-dependabot)** for that flow (it's also the fork-safe split). The default cache-first flow is faster for same-repo teams because local `styleproof-map` can build the head map before CI starts.
249
+
250
+ **Want to skip work safely?** Skip the **whole** StyleProof workflow only for
251
+ changes that cannot affect rendered output, such as docs-only edits, using your
252
+ CI provider's native path filters. Do not skip individual surfaces from a
253
+ StyleProof run based on a changed-file guess: shared CSS, tokens, resets,
254
+ themes, layout primitives, and runtime styling can repaint any surface, and a
255
+ missed surface would certify green without being measured. If you want faster
256
+ feedback, order the highest-signal surfaces first in your spec, but still let
257
+ the full sweep finish before treating the gate as passed.
258
+
259
+ ```yaml
260
+ on:
261
+ pull_request:
262
+ paths-ignore:
263
+ - '**/*.md'
264
+ - 'docs/**'
265
+ - '.github/ISSUE_TEMPLATE/**'
266
+ ```
236
267
 
237
268
  ## Forks and Dependabot
238
269
 
239
- If you **capture in CI** rather than committing maps (the alternative noted at the end of the Quickstart — a better fit when many outside contributors push from different machines), the simplest setup runs the whole gate in one `pull_request` job that captures base + head and diffs them. That job needs a **write** token to push the report branch, post the comment, and set the `StyleProof` status. That's fine for same-repo PRs, but **fork and Dependabot PRs run with a read-only `GITHUB_TOKEN`** (GitHub's security default for untrusted PRs). So the job can't post the status — and a required `StyleProof` check then sits `pending` forever, blocking the PR even though a dependency or fork change usually touches no UI at all.
270
+ If you **always capture in CI** rather than restoring maps from `styleproof-maps` (a better fit when many outside contributors push from different machines), the simplest setup runs the whole gate in one `pull_request` job that captures base + head and diffs them. That job needs a **write** token to push the report branch, post the comment, and set the `StyleProof` status. That's fine for same-repo PRs, but **fork and Dependabot PRs run with a read-only `GITHUB_TOKEN`** (GitHub's security default for untrusted PRs). So the job can't post the status — and a required `StyleProof` check then sits `pending` forever, blocking the PR even though a dependency or fork change usually touches no UI at all.
240
271
 
241
272
  Fix it by splitting capture from reporting, the way the approve workflow is already split out:
242
273
 
@@ -281,15 +312,16 @@ Anything still moving on its own after that is detected as a volatile region and
281
312
  | Live / volatile regions (tickers, third-party embeds) | auto-detected as still-moving and excluded from direct element comparison |
282
313
  | Non-deterministic capture (replay gap, unseeded randomness) | self-check flags it _while recording_, with a named error |
283
314
 
284
- | You set this — only because it's app-specific | Why it exists |
285
- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
286
- | `STYLEPROOF_REPLAY_FROM` (record / replay) | Base and head capture at different times against a live backend; replaying the base's recorded data pins the head to the same inputs, so the diff is **your code, not data drift**. The one piece of real setup. |
287
- | `replayUrl` / `STYLEPROOF_REPLAY_URL` | Your data endpoints aren't under `**/api/**`. |
288
- | `ignore: ['.selector']` | You want a region gone **explicitly** — auto-exclude already handles most live regions, but a known-noisy element reads clearer named. |
289
- | `liveStates: [{ key, setup, go }]` | A live feature has real states to certify. Capture each state on base and head (`surface-loading`, `surface-loaded`) instead of relying on a single moving page state. |
290
- | `variants: [{ key, setup, go }]` | Non-live deterministic variants, such as nav-open or modal-open states. |
291
- | `clockTime` | Your styling keys off a **specific** date, not just "now". |
292
- | `stabilize: { quietFor, timeout }` | An unusually slow surface needs a longer quiet window before the map is read. |
315
+ | You set this — only because it's app-specific | Why it exists |
316
+ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
317
+ | `STYLEPROOF_REPLAY_FROM` (record / replay) | Base and head capture at different times against a live backend; replaying the base's recorded data pins the head to the same inputs, so the diff is **your code, not data drift**. The one piece of real setup. |
318
+ | `replayUrl` / `STYLEPROOF_REPLAY_URL` | Your data endpoints aren't under `**/api/**`. |
319
+ | `ignore: ['.selector']` | You want a region gone **explicitly** — auto-exclude already handles most live regions, but a known-noisy element reads clearer named. |
320
+ | `liveStates: [{ key, setup, go }]` | A live feature has real states to certify. Capture each state on base and head (`surface-loading`, `surface-loaded`) instead of relying on a single moving page state. |
321
+ | `variants: [{ key, setup, go }]` | Non-live deterministic variants, such as nav-open or modal-open states. |
322
+ | `popups: true` | Visible click-triggered overlays should be discovered automatically. Captures persistent dialogs, popovers, menus, listboxes, and open data-state overlays as `surface-popup-XX`; keep hover-only or destructive states as explicit `variants`. |
323
+ | `clockTime` | Your styling keys off a **specific** date, not just "now". |
324
+ | `stabilize: { quietFor, timeout }` | An unusually slow surface needs a longer quiet window before the map is read. |
293
325
 
294
326
  ## Optional: content layer (advisory)
295
327
 
@@ -332,14 +364,13 @@ When a PR **adds** an element, StyleProof now reports its **full resting compute
332
364
 
333
365
  **Action `BenSheridanEdwards/StyleProof@v3`** — key inputs:
334
366
 
335
- | Input | Default | Purpose |
336
- | ------------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
337
- | `fresh-dir` | _required_ | PR-head (or committed) captures to compare. |
338
- | `base-ref` | _none_ | Read the baseline from a git ref (e.g. the PR base branch) instead of `baseline-dir` the committed-map flow. Needs `fetch-depth: 0`. |
339
- | `baseline-dir` | _required\*_ | Base-branch captures dir. _\*Not required when `base-ref` is set._ |
340
- | `require-approval` | `false` | Review-gate mode: set the `StyleProof` status instead of failing. |
341
- | `fail-on-diff` | `true` | Certify mode: fail on any diff. Ignored when `require-approval` is true. |
342
- | `status-context` | `StyleProof` | Commit-status name. Must match the approve workflow and branch protection. |
367
+ | Input | Default | Purpose |
368
+ | ------------------ | ------------ | ----------------------------------------------------------------------------------- |
369
+ | `fresh-dir` | _required_ | PR-head captures restored from `styleproof-maps` or freshly captured in CI. |
370
+ | `baseline-dir` | _required_ | Base-branch captures dir restored from `styleproof-maps` or freshly captured in CI. |
371
+ | `require-approval` | `false` | Review-gate mode: set the `StyleProof` status instead of failing. |
372
+ | `fail-on-diff` | `true` | Certify mode: fail on any diff. Ignored when `require-approval` is true. |
373
+ | `status-context` | `StyleProof` | Commit-status name. Must match the approve workflow and branch protection. |
343
374
 
344
375
  Outputs: `changed` (`"true"` when anything changed), `report-url`. Other inputs (`report-branch`, `github-token`) have sensible defaults — see [`action.yml`](https://github.com/BenSheridanEdwards/StyleProof/blob/main/action.yml).
345
376
 
@@ -361,41 +392,44 @@ It's **asynchronous by design**: approval is a checkbox tick handled by a separa
361
392
 
362
393
  **Capture spec `defineStyleMapCapture({ surfaces, … })`** — determinism is on by default; you rarely set more than `surfaces` and `dir`:
363
394
 
364
- | Option | Default | Purpose |
365
- | ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
366
- | `surfaces` | _required_ | Page states to certify — each `{ key, go, widths?, ignore?, height?, liveStates?, variants? }`. `go(page)` drives to a settled state. Omit `widths` to auto-detect the app's `@media` breakpoints and sweep one width per band. |
367
- | `liveStates` | _none_ | Optional pinned live product states. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<state>` and is labeled as a live state in reports. |
368
- | `variants` | _none_ | Optional non-live deterministic states under a surface. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<variant>` so base/head compare matching states. |
369
- | `expected` | _none_ | Your route/view universe. Emits a coverage-guard test (runs without a capture dir) that fails when a route has no surface and isn't excluded so a new page can't ship uncaptured. |
370
- | `exclude` | `{}` | `key reason` for routes deliberately not captured. Keeps the guard green for known gaps; a key absent from `expected` fails the guard, so the ledger can't go stale. |
371
- | `dir` | `STYLEMAP_DIR` | Output label (`base`/`head`); the spec is **inert until set**, so it sits safely beside your other specs. |
372
- | `replayFrom` | `STYLEPROOF_REPLAY_FROM` | Baseline dir whose recorded responses to replay. Unset this run **records** its HAR for the comparison to use. |
373
- | `replayUrl` | `**/api/**` (`…REPLAY_URL`) | URL glob for the data boundary to record/replay; everything else (JS/CSS/fonts) loads live so the code runs. |
374
- | `freezeClock` | `true` | Pin `Date.now()`/`new Date()` so time-derived styling can't drift; timers keep running so settling still works. |
375
- | `clockTime` | `2025-01-01T00:00:00Z` | The frozen instant. |
376
- | `selfCheck` | on while recording | Capture each surface twice and fail on any difference — proves the capture is deterministic. Off on the replay run; `STYLEPROOF_SELFCHECK=1` forces both. |
377
- | `screenshots` | `true` | Save full-page screenshots for the report's before/after crops. |
378
- | `baseDir` | `__stylemaps__` | Output root directory. |
395
+ | Option | Default | Purpose |
396
+ | ------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
397
+ | `surfaces` | _required_ | Page states to certify — each `{ key, go, widths?, ignore?, height?, liveStates?, variants?, popups? }`. `go(page)` drives to a settled state. Omit `widths` to auto-detect the app's `@media` breakpoints and sweep one width per band. |
398
+ | `liveStates` | _none_ | Optional pinned live product states. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<state>` and is labeled as a live state in reports. |
399
+ | `variants` | _none_ | Optional non-live deterministic states under a surface. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<variant>` so base/head compare matching states. |
400
+ | `popups` | `false` | Optional automatic popup capture. Set `true` or `{ max, triggers, overlays, timeoutMs }` to click visible safe triggers and save discovered overlay states as `<surface>-popup-XX`. |
401
+ | `expected` | _none_ | Your route/view universe. Emits a coverage-guard test (runs without a capture dir) that fails when a route has no surface and isn't excluded so a new page can't ship uncaptured. |
402
+ | `exclude` | `{}` | `key reason` for routes deliberately not captured. Keeps the guard green for known gaps; a key absent from `expected` fails the guard, so the ledger can't go stale. |
403
+ | `dir` | `STYLEMAP_DIR` | Output label (`base`/`head`); the spec is **inert until set**, so it sits safely beside your other specs. |
404
+ | `replayFrom` | `STYLEPROOF_REPLAY_FROM` | Baseline dir whose recorded responses to replay. Unset this run **records** its HAR for the comparison to use. |
405
+ | `replayUrl` | `**/api/**` (`…REPLAY_URL`) | URL glob for the data boundary to record/replay; everything else (JS/CSS/fonts) loads live so the code runs. |
406
+ | `freezeClock` | `true` | Pin `Date.now()`/`new Date()` so time-derived styling can't drift; timers keep running so settling still works. |
407
+ | `clockTime` | `2025-01-01T00:00:00Z` | The frozen instant. |
408
+ | `selfCheck` | on while recording | Capture each surface twice and fail on any difference — proves the capture is deterministic. Off on the replay run; `STYLEPROOF_SELFCHECK=1` forces both. |
409
+ | `screenshots` | `true` | Save full-page screenshots for the report's before/after crops. |
410
+ | `baseDir` | `__stylemaps__` | Output root directory. |
379
411
 
380
412
  Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<style>`/… and `next-route-announcer`) are skipped automatically; a surface's `ignore` adds to that default, it doesn't replace it.
381
413
 
382
414
  **Capture env vars** (wire CI without editing the spec):
383
415
 
384
- | Env | Purpose |
385
- | ------------------------ | --------------------------------------------------------------------------------------------- |
386
- | `STYLEMAP_DIR` | Output label; the capture is skipped entirely when unset. |
387
- | `STYLEPROOF_BASEDIR` | Output root dir (default `__stylemaps__`). The committed-map hook sets this to a tracked dir. |
388
- | `STYLEPROOF_SCREENSHOTS` | `0` to skip full-page screenshots lean, commit-friendly `.json.gz`-only maps. |
389
- | `STYLEPROOF_REPLAY_FROM` | Baseline dir to replay recorded data from — set this on the **head** capture. |
390
- | `STYLEPROOF_REPLAY_URL` | Override the `**/api/**` data-boundary glob. |
391
- | `STYLEPROOF_SELFCHECK` | `1` to capture each surface twice and fail if the two differ. |
416
+ | Env | Purpose |
417
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
418
+ | `STYLEMAP_DIR` | Output label; the capture is skipped entirely when unset. |
419
+ | `STYLEPROOF_BASEDIR` | Output root dir (runner default `__stylemaps__`; `styleproof-map` CLI default `.styleproof/maps`). |
420
+ | `STYLEPROOF_SCREENSHOTS` | `0` to skip full-page screenshots. The CLI keeps screenshots by default so reports can crop maps restored from cache. |
421
+ | `STYLEPROOF_REPLAY_FROM` | Baseline dir to replay recorded data from — set this on the **head** capture. |
422
+ | `STYLEPROOF_REPLAY_URL` | Override the `**/api/**` data-boundary glob. |
423
+ | `STYLEPROOF_SELFCHECK` | `1` to capture each surface twice and fail if the two differ. |
424
+ | `STYLEPROOF_UPLOAD` | `1` to require map-store upload; `0` to capture locally only. |
425
+ | `STYLEPROOF_CACHE_BRANCH` | Map store branch (default `styleproof-maps`). |
392
426
 
393
427
  **CLIs** (every flag accepts `--flag value` and `--flag=value`; `--help` lists all):
394
428
 
395
- - `styleproof-init` — scaffold **and activate** the whole gate: the capture spec, a `playwright.config.ts` (production-build `webServer`, parallel capture), the pre-push capture-and-push hook, and the browser-less CI workflow. One command. Generated commands follow the repo's lockfile (`bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, or npm by default) instead of assuming npm.
396
- - `styleproof-map` — capture the current branch's computed-style map through Playwright. By default it writes lean committed maps to `stylemaps/current`; pass `--spec`, `--dir`, `--base-dir`, or `--screenshots` when you need a custom capture.
397
- - `styleproof-diff` — the certify gate. With no args, it diffs `stylemaps/current` against the inferred base branch (`GITHUB_BASE_REF`, `branch.<name>.gh-merge-base`, `gh pr view`, then main/master fallbacks); `styleproof-diff main` / `styleproof-diff master` pins the base; `styleproof-diff <beforeDir> <afterDir>` keeps the manual two-directory form. Exits `0` certified (identical), `1` on a diff, `2` on a usage/capture error, `3` when only new surfaces are present (no baseline to diff against). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`.
398
- - `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports `stylemaps/current` against the inferred base branch; `styleproof-report main` / `styleproof-report master` pins the base; `styleproof-report <beforeDir> <afterDir> --out <dir>` keeps the manual two-directory form. Add `--include-content` for the opt-in, advisory content section (see above).
429
+ - `styleproof-init` — scaffold the gate: the capture spec, a `playwright.config.ts` (production-build `webServer`, parallel capture), `.gitignore` cache entries, and the cache-first report workflow. One command. Generated commands follow the repo's lockfile (`bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, or npm by default) instead of assuming npm.
430
+ - `styleproof-map` — capture the current commit's computed-style map through Playwright. By default it writes `.styleproof/maps/current`, keeps screenshots for reports, writes a manifest, and uploads to `styleproof-maps` outside CI when the working tree was clean and a git remote exists. Pass `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, or `--no-screenshots` for custom flows.
431
+ - `styleproof-diff` — the certify gate. With no args, it restores cached maps for the current commit and inferred base (`GITHUB_BASE_REF`, `branch.<name>.gh-merge-base`, `gh pr view`, then main/master fallbacks); `styleproof-diff main` / `styleproof-diff master` pins the base; `styleproof-diff <beforeDir> <afterDir>` keeps the manual two-directory form for CI fallback captures. Exits `0` certified (identical), `1` on a diff, `2` on a usage/capture error, `3` when only new surfaces are present (no baseline to diff against). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`.
432
+ - `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports cached maps for the current commit against the inferred base; `styleproof-report main` / `styleproof-report master` pins the base; `styleproof-report <beforeDir> <afterDir> --out <dir>` keeps the manual two-directory form. Add `--include-content` for the opt-in, advisory content section (see above).
399
433
 
400
434
  A programmatic API is also exported — `captureStyleMap`, `diffStyleMaps`, `generateStyleMapReport`, and the breakpoint helpers `detectViewportWidths` / `widthsFromBoundaries`, among others. For the capture internals, the approve-workflow trust model, and how to contribute, see [CONTRIBUTING](https://github.com/BenSheridanEdwards/StyleProof/blob/main/CONTRIBUTING.md) and the [`example/`](https://github.com/BenSheridanEdwards/StyleProof/tree/main/example) workflows.
401
435
 
@@ -2,8 +2,7 @@
2
2
  /**
3
3
  * Diff two computed-style map captures (see styleproof).
4
4
  *
5
- * styleproof-diff [baseRef] [--maps-dir <dir>] [--max N] [--json <file>]
6
- * styleproof-diff --base-ref <gitref> [mapsDir] [--max N] [--json <file>]
5
+ * styleproof-diff [baseRef] [--max N] [--json <file>]
7
6
  * styleproof-diff <beforeDir> <afterDir> [--max N] [--json <file>]
8
7
  *
9
8
  * Reports, per surface:
@@ -15,12 +14,9 @@
15
14
  * longer does (or now changes differently) — the classic dropped
16
15
  * `hover:` variant a screenshot can never catch.
17
16
  *
18
- * --base-ref reads the base from a git ref instead of a second directory: it
19
- * materialises the captures committed at <mapsDir> as of <gitref> (e.g. `main`)
20
- * and diffs them against your working <mapsDir>. That's the "base map lives on
21
- * main, CI just diffs" flow — commit each branch's maps (pre-push), and the gate
22
- * never recomputes the base. Both sides must be captured in the same environment
23
- * (browser + fonts) for the diff to be meaningful.
17
+ * No-arg and single base argument usage restore base/head maps from the StyleProof
18
+ * map store branch by commit SHA. To compare already-restored/captured maps,
19
+ * pass explicit before/after directories.
24
20
  *
25
21
  * Custom properties (--*) are ignored: they are inputs, not outcomes (see
26
22
  * README). Exit code 0 = identical, 1 = reviewable differences, 2 = usage/capture
@@ -29,7 +25,13 @@
29
25
  import fs from 'node:fs';
30
26
  import path from 'node:path';
31
27
  import { diffStyleMapDirs, findingLabel } from '../dist/diff.js';
32
- import { cleanupBaseRefCaptureDirs, resolveBaseRefCaptureDirs } from '../dist/cli-base-ref.js';
28
+ import {
29
+ DEFAULT_MAP_STORE_BRANCH,
30
+ DEFAULT_REMOTE,
31
+ assertCompatibleMapDirs,
32
+ cleanupCachedCaptureDirs,
33
+ resolveCachedCaptureDirs,
34
+ } from '../dist/map-store.js';
33
35
  import {
34
36
  cliErrorMessage,
35
37
  isHelpArg,
@@ -39,19 +41,19 @@ import {
39
41
  } from '../dist/cli-errors.js';
40
42
 
41
43
  const COMMAND = path.basename(process.argv[1] ?? 'styleproof-diff').replace(/\.mjs$/, '');
42
- const DEFAULT_MAPS_DIR = 'stylemaps/current';
43
44
 
44
45
  const HELP = `${COMMAND} — certify a CSS refactor by diffing two computed-style map captures
45
46
 
46
47
  usage: ${COMMAND} [baseRef] [options]
47
- ${COMMAND} --base-ref <gitref> [mapsDir] [options]
48
48
  ${COMMAND} <beforeDir> <afterDir> [options]
49
49
 
50
50
  options:
51
- --base-ref <ref> diff <mapsDir> as committed at <ref> (e.g. main) against your
52
- working <mapsDir> — base from git, no recapture
53
- --maps-dir <dir> committed map dir for the base-ref flow
54
- (default: ${DEFAULT_MAPS_DIR})
51
+ --spec <path> StyleProof spec used to select compatible cached maps
52
+ (default: e2e/styleproof.spec.ts)
53
+ --cache-branch <b>
54
+ map store branch for default cached-map mode
55
+ (default: ${DEFAULT_MAP_STORE_BRANCH})
56
+ --remote <name> git remote for the map store (default: ${DEFAULT_REMOTE})
55
57
  --max <n> max lines printed per surface before truncating (default: 40)
56
58
  --json <file> also write the full structured diff to <file>
57
59
  -h, --help show this help
@@ -64,18 +66,21 @@ const argv = process.argv.slice(2);
64
66
  const args = [];
65
67
  let MAX = 40;
66
68
  let jsonOut = null;
67
- let baseRef = null;
68
- let mapsDir = DEFAULT_MAPS_DIR;
69
+ let spec = 'e2e/styleproof.spec.ts';
70
+ let cacheBranch = process.env.STYLEPROOF_CACHE_BRANCH ?? DEFAULT_MAP_STORE_BRANCH;
71
+ let remote = process.env.STYLEPROOF_REMOTE ?? DEFAULT_REMOTE;
69
72
  for (let i = 0; i < argv.length; i++) {
70
73
  if (isHelpArg(argv[i])) showHelpAndExit(HELP);
71
74
  else if (argv[i] === '--max') MAX = Number(argv[++i]);
72
75
  else if (argv[i].startsWith('--max=')) MAX = Number(argv[i].slice(6));
73
76
  else if (argv[i] === '--json') jsonOut = argv[++i];
74
77
  else if (argv[i].startsWith('--json=')) jsonOut = argv[i].slice(7);
75
- else if (argv[i] === '--base-ref') baseRef = argv[++i];
76
- else if (argv[i].startsWith('--base-ref=')) baseRef = argv[i].slice(11);
77
- else if (argv[i] === '--maps-dir') mapsDir = argv[++i];
78
- else if (argv[i].startsWith('--maps-dir=')) mapsDir = argv[i].slice(11);
78
+ else if (argv[i] === '--spec') spec = argv[++i];
79
+ else if (argv[i].startsWith('--spec=')) spec = argv[i].slice(7);
80
+ else if (argv[i] === '--cache-branch') cacheBranch = argv[++i];
81
+ else if (argv[i].startsWith('--cache-branch=')) cacheBranch = argv[i].slice(15);
82
+ else if (argv[i] === '--remote') remote = argv[++i];
83
+ else if (argv[i].startsWith('--remote=')) remote = argv[i].slice(9);
79
84
  else if (argv[i].startsWith('--')) {
80
85
  console.error(unknownFlagMessage(COMMAND, argv[i]));
81
86
  process.exit(2);
@@ -84,26 +89,34 @@ for (let i = 0; i < argv.length; i++) {
84
89
 
85
90
  let dirA;
86
91
  let dirB;
87
- let baseCapture = null;
88
- if (baseRef || args.length <= 1) {
92
+ let cacheCapture = null;
93
+ if (args.length <= 1) {
89
94
  if (!Number.isFinite(MAX)) {
90
- console.error(`usage: ${COMMAND} [baseRef] [--maps-dir <dir>] [--max N] [--json <file>]`);
95
+ console.error(`usage: ${COMMAND} [baseRef] [--max N] [--json <file>]`);
91
96
  process.exit(2);
92
97
  }
93
98
  try {
94
- baseCapture = resolveBaseRefCaptureDirs({
99
+ cacheCapture = resolveCachedCaptureDirs({
95
100
  command: COMMAND,
96
- baseRef,
97
- mapsDir,
98
101
  args,
99
- usage: `usage: ${COMMAND} --base-ref <gitref> [mapsDir] [--max N] [--json <file>]`,
102
+ spec,
103
+ branch: cacheBranch,
104
+ remote,
105
+ baseUrl: process.env.BASE_URL,
106
+ usage: `usage: ${COMMAND} [baseRef] [--max N] [--json <file>]`,
100
107
  });
108
+ dirA = cacheCapture.beforeDir;
109
+ dirB = cacheCapture.afterDir;
101
110
  } catch (e) {
102
- console.error(cliErrorMessage(e));
111
+ console.error(
112
+ [
113
+ `${COMMAND}: cached maps are not available for this comparison`,
114
+ cliErrorMessage(e),
115
+ 'Next: run styleproof-map on the base and head commits to upload maps, or let CI recapture both sides.',
116
+ ].join('\n'),
117
+ );
103
118
  process.exit(2);
104
119
  }
105
- dirA = baseCapture.beforeDir;
106
- dirB = baseCapture.afterDir;
107
120
  } else {
108
121
  if (args.length !== 2 || !Number.isFinite(MAX)) {
109
122
  console.error(`usage: ${COMMAND} <beforeDir> <afterDir> [--max N] [--json <file>] (--help for all options)`);
@@ -120,12 +133,13 @@ if (baseRef || args.length <= 1) {
120
133
 
121
134
  let result;
122
135
  try {
136
+ assertCompatibleMapDirs(dirA, dirB);
123
137
  result = diffStyleMapDirs(dirA, dirB);
124
138
  } catch (e) {
125
139
  console.error(e.message);
126
140
  process.exit(2);
127
141
  } finally {
128
- cleanupBaseRefCaptureDirs(baseCapture);
142
+ cleanupCachedCaptureDirs(cacheCapture);
129
143
  }
130
144
  const { surfaces, counts, compared } = result;
131
145