styleproof 3.18.0 → 3.20.0
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 +191 -0
- package/README.md +62 -6
- package/bin/styleproof-capture.mjs +4 -1
- package/bin/styleproof-diff.mjs +41 -32
- package/bin/styleproof-init.mjs +49 -20
- package/bin/styleproof-map.mjs +8 -0
- package/dist/affected-surfaces.d.ts +24 -0
- package/dist/affected-surfaces.js +115 -17
- package/dist/capture-url.d.ts +1 -1
- package/dist/capture.js +40 -26
- package/dist/cli-errors.js +7 -2
- package/dist/coverage.d.ts +35 -0
- package/dist/coverage.js +54 -0
- package/dist/crawl-surfaces.js +14 -5
- package/dist/crawl.d.ts +16 -1
- package/dist/crawl.js +67 -7
- package/dist/danger.d.ts +13 -0
- package/dist/danger.js +13 -0
- package/dist/diff.js +38 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/inventory.js +9 -3
- package/dist/map-store.d.ts +5 -1
- package/dist/map-store.js +10 -3
- package/dist/report.js +23 -6
- package/dist/runner.d.ts +17 -0
- package/dist/runner.js +159 -35
- package/dist/variant-crawler.js +16 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,197 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.20.0] - 2026-07-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Crawl no longer double-captures `/` and `/index.html` as two surfaces.** On a
|
|
15
|
+
static multi-page site whose nav links the `.html` files, `/` and `/index.html`
|
|
16
|
+
(and `/dir/` vs `/dir/index.html`) are the same route but were captured twice as
|
|
17
|
+
byte-near-identical maps — doubling the capture work and duplicating every finding
|
|
18
|
+
in the diff. The crawl's dedup identity now normalizes a trailing `index.html` to
|
|
19
|
+
its directory path, so they collapse to one surface (first-seen href keeps its
|
|
20
|
+
original navigable form). Only the literal `index.html` filename normalizes — a
|
|
21
|
+
genuine `about.html` stays a distinct surface from `about`.
|
|
22
|
+
- **`styleproof-init` now states exactly which files it wrote — and that it did NOT
|
|
23
|
+
touch `package.json` or your lockfile.** Adopters attributed the `styleproof`
|
|
24
|
+
dependency entry (added by their package manager's `install`) to init; init only
|
|
25
|
+
ever reads `package.json` and writes the spec, the dedicated Playwright config,
|
|
26
|
+
`.gitignore` lines, and the CI workflow. The summary now enumerates those files and
|
|
27
|
+
says plainly that the manifest and lockfile were left untouched.
|
|
28
|
+
- **The no-comparison outcome of `styleproof-diff` / `styleproof-report` names both
|
|
29
|
+
ways forward.** When the no-args (inferred-base) path can restore no base map — no
|
|
30
|
+
map-store remote, no cached bundle — nothing is compared. That already exits `2`
|
|
31
|
+
(never a soft `0` a newcomer could read as "certified clean"); the message now says
|
|
32
|
+
"nothing was compared" and names the two working alternatives: run in CI (or a repo
|
|
33
|
+
with the `origin` remote) where the base is restorable, or the two-directory form
|
|
34
|
+
`styleproof-diff <beforeDir> <afterDir>`. A regression test pins the exit-2 contract
|
|
35
|
+
in a remote-less repo for both commands.
|
|
36
|
+
- **README: the Next.js coverage guard is described accurately.** The docs conflated
|
|
37
|
+
two behaviors. With the auto-wired spec, `surfaces` and `expected` both derive from
|
|
38
|
+
the same `discoverNextRoutes()` call, so a new static route is captured and expected
|
|
39
|
+
together — **auto-covered, never a guard failure**. The guard **fails** only on
|
|
40
|
+
genuine divergence (a dynamic route, a hand-maintained registry, or a route dropped
|
|
41
|
+
from `surfaces` while still `expected`). Rewrote the overclaiming passages and the
|
|
42
|
+
generated-spec comments to state both behaviors.
|
|
43
|
+
- **Layout-equivalent margin suppression no longer drops a real one-sided margin
|
|
44
|
+
change.** `dropLayoutEquivalentMarginProps` suppressed any horizontal
|
|
45
|
+
`margin-left/right/inline-start/inline-end` change whenever the element's rect
|
|
46
|
+
was unchanged — reasoning that a margin that doesn't move the box is cosmetic
|
|
47
|
+
drift. But a one-sided change (e.g. `margin-left: 0 → 40px` with `margin-right`
|
|
48
|
+
untouched) that leaves the rect identical only stayed put because _something
|
|
49
|
+
else compensated_; that is a genuine restyle, and it was silently dropped. The
|
|
50
|
+
suppression now fires only when there is no **demonstrable px imbalance**
|
|
51
|
+
between a side and its opposite — balanced drift (both sides move together) and
|
|
52
|
+
forced-state deltas are still suppressed exactly as before, but a one-sided
|
|
53
|
+
real change surfaces. A residual, consciously-deferred corner remains (a
|
|
54
|
+
perfectly _balanced_ change held in place by external compensation), documented
|
|
55
|
+
inline; closing it needs cross-element layout reasoning.
|
|
56
|
+
- **`styleproof-init` no longer imports the whole library barrel (fixes a CI
|
|
57
|
+
flake).** The scaffolder only needs `discoverNextRoutes`, but it imported it
|
|
58
|
+
from `dist/index.js` — dragging capture, the crawler, the report renderer, and
|
|
59
|
+
six Playwright-importing modules into a tool that writes files and captures
|
|
60
|
+
nothing. Loading that oversized module graph concurrently (init's own suite
|
|
61
|
+
spawns the CLI many times, alongside the rest of `node --test`) is what made
|
|
62
|
+
init's tests flake in CI, red-flagging releases with no code cause. It now
|
|
63
|
+
imports from the `dist/routes.js` leaf (`fs` + `path` only): init's transitive
|
|
64
|
+
module graph drops from 21 dist modules to 1, with zero Playwright modules on
|
|
65
|
+
its load path. Behaviour is unchanged; a regression test pins the leaf import.
|
|
66
|
+
- **Popup capture: verified reset + identity-bound triggers (no leaked-overlay
|
|
67
|
+
contamination, no wrong-trigger keying).** On a surface whose `go()` doesn't
|
|
68
|
+
navigate (SPA variants), the between-popups reset was Escape-only and assumed:
|
|
69
|
+
a toast or `[role="status"]` overlay Escape can't dismiss leaked into the next
|
|
70
|
+
popup's capture, and each reopen re-enumerated triggers positionally, so a
|
|
71
|
+
shifted trigger set (e.g. a click that adds a button) could key a popup under a
|
|
72
|
+
different trigger than the one originally enumerated. Triggers are now re-bound
|
|
73
|
+
by the DOM identity recorded at first enumeration, and the reset is verified
|
|
74
|
+
against the surface's pristine overlay set; a candidate that can't be opened
|
|
75
|
+
safely is skipped loudly (a `styleproof:` warning naming the popup and the
|
|
76
|
+
leaked overlay or missing trigger) instead of being captured contaminated,
|
|
77
|
+
mis-keyed, or — with self-check on — saved unproven. That identity is the
|
|
78
|
+
trigger's DOM path **and** its accessible label, not the path alone: for an
|
|
79
|
+
id-less trigger the path ends in `:nth-of-type`, which is still position within
|
|
80
|
+
a parent, so a same-tag same-parent sibling injected earlier in DOM order
|
|
81
|
+
between enumeration and reopen would slide the recorded path onto a different
|
|
82
|
+
trigger and key its popup under the wrong one — silently. Requiring the label
|
|
83
|
+
(the same aria-label/name/text/title accessible name the crawler reads) to match
|
|
84
|
+
too turns that mismatch into the same loud skip. Navigating surfaces are
|
|
85
|
+
unaffected.
|
|
86
|
+
|
|
87
|
+
## [3.19.0] - 2026-07-06
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **Selective-remap wiring: `explainAffectedSurfaces` + the pre-push recipe.** The
|
|
92
|
+
sound core (`affectedSurfaces`) shipped in 3.17.0 returns a bare `Set | 'all'` that
|
|
93
|
+
names nothing; the new pure `explainAffectedSurfaces(result, allSurfaceKeys, reason?)`
|
|
94
|
+
formatter renders the verdict as reviewer-checkable lines — which surfaces re-capture
|
|
95
|
+
and which reuse their committed base map — so a pre-push hook or CI log can print the
|
|
96
|
+
skip list before anyone trusts it. `affectedSurfaces`'s return shape is unchanged
|
|
97
|
+
(backward-compatible; the helper takes the surface keys as a second argument rather
|
|
98
|
+
than extending the sentinel). README's selective-remap section gains the helper, its
|
|
99
|
+
output, and the full `git diff → dependency-cruiser → affectedSurfaces → capture subset`
|
|
100
|
+
pre-push recipe. Opt-in and advisory throughout — the default full-coverage gate is
|
|
101
|
+
untouched.
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- **Report tables escape hostile CSS values (no Markdown breakout).** A property
|
|
106
|
+
value carrying a `|` used to split a report table row, and a backtick used to close
|
|
107
|
+
the code span and leak live Markdown (`content:"…"`, `url(…)`, `font` strings). The
|
|
108
|
+
render boundary now escapes values instead of stripping them — `|` → `\|`, and the
|
|
109
|
+
code fence widens past the value's longest backtick run — so hostile values render
|
|
110
|
+
as one intact, readable cell. `report.md` only; the privileged review comment was
|
|
111
|
+
never affected.
|
|
112
|
+
- **The navigable-removal guard now sees SVG `<a>` nav links.** The in-page harvest
|
|
113
|
+
keyed anchors off `tagName === 'A'`, which is HTML-only (SVG reports lowercase `a`),
|
|
114
|
+
so an SVG nav link never entered the inventory and its removal never gated. The tag
|
|
115
|
+
check is now case-insensitive, the selector matches any-namespace `href`
|
|
116
|
+
(`a[*|href]`), and the target falls back to `xlink:href` — an `<svg><a>` link now
|
|
117
|
+
resolves like an HTML one.
|
|
118
|
+
- **Single-value `transform-origin`/`perspective-origin` jitter is suppressed.** The
|
|
119
|
+
sub-pixel-origin equality check required at least two length components, so a
|
|
120
|
+
one-value origin (`50px`) leaked rounding jitter as a false diff. One to three
|
|
121
|
+
components are now all suppressed within `ORIGIN_EPSILON_PX`; a real, larger change
|
|
122
|
+
still reports.
|
|
123
|
+
- **`styleproof-diff --json` exits 2 (not 1) when the file cannot be written.** A bad
|
|
124
|
+
`--json` path is a usage/setup error, but the write sat outside the guarded blocks
|
|
125
|
+
and let the failure fall through to exit 1 — which CI reads as a real diff. It now
|
|
126
|
+
reports to stderr and exits 2.
|
|
127
|
+
- **Crawl flag docs corrected.** `--max-depth`'s default is documented as 16 (not
|
|
128
|
+
"unbounded" in `--help`, not "3" in the JSDoc); `--until-covered` is now listed in
|
|
129
|
+
`styleproof-capture --help`.
|
|
130
|
+
- **Crawl no longer silently drops surfaces to key collisions, and mapping no longer
|
|
131
|
+
clicks title-only destructive controls.** Five soundness fixes across the crawl path:
|
|
132
|
+
- `selectCrawlLinks` deduped by raw URL, so `/about` and `/about/` — one route — were
|
|
133
|
+
two links that keyed identically and the second capture overwrote the first. Trailing
|
|
134
|
+
slashes are now normalized in the dedup identity (root `/` and the query string are
|
|
135
|
+
left intact), so a route is captured once.
|
|
136
|
+
- Genuinely distinct surfaces whose derived keys collide (e.g. `/a/b` and `/a-b` both
|
|
137
|
+
slugify to `a-b`) previously overwrote each other's map file. `selectCrawlLinks` now
|
|
138
|
+
disambiguates with a `-2`, `-3`, … suffix (mirroring the surface crawler), so every
|
|
139
|
+
surface survives.
|
|
140
|
+
- `defaultLinkKey` joined query-param values in iteration order, so `?tab=a&x=b` and
|
|
141
|
+
`?x=b&tab=a` — the same route — keyed as `a-b` vs `b-a` and flapped the coverage
|
|
142
|
+
guard into phantom regressions. Params are now sorted by name before joining.
|
|
143
|
+
- The surface crawler's clickable-candidate label omitted the `title` attribute, so an
|
|
144
|
+
icon-only `<button title="Delete">` labeled as `button` and slipped past the
|
|
145
|
+
destructive-action guard — mapping would click it. `title` is now part of the label
|
|
146
|
+
input in both crawlers.
|
|
147
|
+
- The variant crawler carried a weaker, divergent copy of the destructive-action word
|
|
148
|
+
list (missing `revoke|reset|wipe|drop|rotate|provision|seal|regenerate|renew`). Both
|
|
149
|
+
crawlers now share one `DANGER_SOURCE` constant, so mapping refuses the same set of
|
|
150
|
+
destructive controls everywhere.
|
|
151
|
+
- `defineStyleMapCapture` and `defineCrawlCapture` now assert every expanded capture
|
|
152
|
+
key is unique before running: the `surface.key-variant.key` join is ambiguous
|
|
153
|
+
(`a` + `b-c` and `a-b` + `c` both expand to `a-b-c`), which used to overwrite a map
|
|
154
|
+
file with no error. The key format is unchanged (it's public — filenames and report
|
|
155
|
+
identities); a collision now throws up front and names both origins so the author can
|
|
156
|
+
rename one.
|
|
157
|
+
- **Sass `@use`/`@forward` in a CSS Module now fails closed to `'all'`.** A
|
|
158
|
+
`.module.scss`/`.module.sass` that loads a partial via `@use`/`@forward` can pull in
|
|
159
|
+
global rules the JS import graph can't see, so `classifyStyleChange` now treats any
|
|
160
|
+
such file as global (`'all'`) — a sound over-approximation, no heuristics. A plain CSS
|
|
161
|
+
Module with only class selectors stays `'scope'` as before.
|
|
162
|
+
- **Legacy Sass/CSS `@import` in a CSS Module now fails closed too.** The fail-closed
|
|
163
|
+
load check missed the `@import` form, so a `.module.scss` loading a partial via
|
|
164
|
+
`@import "vars"` — whose members merge in exactly like `@use` — classified as `'scope'`
|
|
165
|
+
and could silently skip a re-capture. `classifyStyleChange` now treats any
|
|
166
|
+
`@use`/`@forward`/`@import` load as global (`'all'`), covering both the Sass partial
|
|
167
|
+
load and the plain-CSS pass-through (`@import url(x.css)`, whose selectors are not
|
|
168
|
+
hashed into the module's per-file scope, so it escapes the module). Only ever widens
|
|
169
|
+
toward `'all'`; a module with no load directive still stays `'scope'`.
|
|
170
|
+
- **`affectedSurfaces` now canonicalizes paths across all inputs, closing a silent
|
|
171
|
+
unsound skip.** `surfaces` entries, `changedFiles`, graph edges, and `files` are now
|
|
172
|
+
normalized to one spelling (strip a leading `./`, collapse `//`, resolve `.`/`..` as
|
|
173
|
+
pure string math — no fs), so a `./pages/Home.tsx` surface entry no longer misses a
|
|
174
|
+
reachability hit spelled `pages/Home.tsx` and gets dropped from the affected set. And a
|
|
175
|
+
declared surface whose entry path appears in neither `files` nor any graph edge is now
|
|
176
|
+
unplaceable → `'all'`, the same fail-closed rule as an unplaceable changed file.
|
|
177
|
+
- **Stale browser-build sidecar can no longer stamp a false fingerprint.**
|
|
178
|
+
`styleproof-map` now deletes any prior run's `styleproof-browser.json` before Playwright
|
|
179
|
+
runs, and `writeBrowserBuildSidecar(dir, undefined)` now removes an existing sidecar
|
|
180
|
+
rather than leaving it. Previously a reused capture dir plus a run that recorded no
|
|
181
|
+
browser version would fold the _previous_ run's build into this run's manifest, and
|
|
182
|
+
`assertCompatibleMapDirs` would trust that false `browserVersion` fingerprint.
|
|
183
|
+
- **`captureStyleMap` no longer leaks its motion-freeze `<style>` onto a reused page.**
|
|
184
|
+
The freeze injected for the base/forced-state reads was re-applied without a handle and
|
|
185
|
+
never removed, so on a page recaptured **without a reload** (an SPA `go()` that doesn't
|
|
186
|
+
navigate, multi-surface reuse, the self-check's re-run) the next capture's motion pass
|
|
187
|
+
read the still-frozen transition/animation longhands (`none`/`0s`) as its baseline —
|
|
188
|
+
phantom drift that surfaced as a **false "non-deterministic capture"** self-check
|
|
189
|
+
failure. The re-applied tag is now tracked and removed in a `finally`, so throw paths
|
|
190
|
+
clean up too. (No API change.)
|
|
191
|
+
- **`liveStates` + `expected` no longer reports a false coverage gap.** A surface with
|
|
192
|
+
`liveStates` is captured only as its split expansions (`home-loading`, `home-loaded`) —
|
|
193
|
+
the bare base key is dropped by design — but the coverage ledger recorded `expected`
|
|
194
|
+
in base keys and the gate compared captured keys literally, so a fully-captured app
|
|
195
|
+
failed the gate with `uncovered: ['home']` (live in 3.18.0). The ledger writer now
|
|
196
|
+
records `expected` already translated through the same liveState expansion, and the
|
|
197
|
+
suite-side guard maps each capture back to its originating `surfaceKey` — a precise
|
|
198
|
+
mapping via real metadata, so an unrelated `home-banner` never satisfies an uncaptured
|
|
199
|
+
`home`. Both the spec-driven and crawl capture paths are fixed consistently.
|
|
200
|
+
|
|
10
201
|
## [3.18.0] - 2026-07-06
|
|
11
202
|
|
|
12
203
|
### Added
|
package/README.md
CHANGED
|
@@ -57,8 +57,12 @@ surface. `expected` turns that inventory into a guard: a key it lists that is
|
|
|
57
57
|
neither captured nor excluded fails as missing coverage instead of silently
|
|
58
58
|
passing. What's guarded depends on how `expected` is fed —
|
|
59
59
|
|
|
60
|
-
- **Next.js:** auto-
|
|
61
|
-
|
|
60
|
+
- **Next.js:** auto-covered. `styleproof-init` derives both `surfaces` and
|
|
61
|
+
`expected` from the same `discoverNextRoutes()` call, so a new static route lands
|
|
62
|
+
in both at once — captured and expected together, with nothing to keep in sync.
|
|
63
|
+
The guard then fails only on genuine divergence between the two: a dynamic route
|
|
64
|
+
(pre-excluded, so its reason surfaces), a hand-maintained registry, or a route
|
|
65
|
+
dropped from `surfaces` while still in `expected`.
|
|
62
66
|
- **Link-crawled SPAs:** pass `expected` to `defineCrawlCapture` and the crawl
|
|
63
67
|
reconciles it against the _rendered nav_ (the route universe for such an app),
|
|
64
68
|
both directions — a new linked route with no `expected` entry fails, and an
|
|
@@ -152,7 +156,7 @@ _and_ no head capture, so it never appears in any diff, and the status goes
|
|
|
152
156
|
green having never looked at it. This is the one thing the captures can't catch
|
|
153
157
|
on their own: a capture that was never taken.
|
|
154
158
|
|
|
155
|
-
Declare your app's route/view universe in `expected` and StyleProof emits a coverage-guard test in your **normal** suite (it runs even without `STYLEMAP_DIR` — it's a static check, no browser). It fails
|
|
159
|
+
Declare your app's route/view universe in `expected` and StyleProof emits a coverage-guard test in your **normal** suite (it runs even without `STYLEMAP_DIR` — it's a static check, no browser). It fails when `expected` and your captured surfaces diverge — a route you listed in `expected` with no surface and no `exclude` entry fails as missing coverage, so a registry entry can't quietly ship uncaptured:
|
|
156
160
|
|
|
157
161
|
```ts
|
|
158
162
|
import { defineStyleMapCapture } from 'styleproof';
|
|
@@ -168,7 +172,7 @@ defineStyleMapCapture({
|
|
|
168
172
|
|
|
169
173
|
A route that's neither a captured surface nor an `exclude` entry fails the guard; an `exclude` key that isn't in `expected` (a renamed/removed route) fails too, so the opt-out ledger can't quietly rot. Captured surfaces beyond `expected` are fine — one route can have several states (`landing`, `landing-nav-open`). Omit `expected` and behaviour is unchanged.
|
|
170
174
|
|
|
171
|
-
**Next.js: wired for you.** Run `styleproof-init` in a Next.js project and the generated spec discovers your routes (App Router `app/` + Pages Router `pages/`) at run time and
|
|
175
|
+
**Next.js: wired for you.** Run `styleproof-init` in a Next.js project and the generated spec discovers your routes (App Router `app/` + Pages Router `pages/`) at run time and derives **both** the surfaces and `expected` from that same `discoverNextRoutes()` call. Because they share one source, a static route you add later is captured and expected in the same step — auto-covered, never a guard failure, with nothing to keep in sync. The guard exists for the cases where the two genuinely diverge: a dynamic `[param]` route (it can't be navigated without a value, so it's placed in `exclude` with a reason rather than captured), a registry you hand-maintain instead of the live call, or a route you drop from `surfaces` while it's still `expected`:
|
|
172
176
|
|
|
173
177
|
```ts
|
|
174
178
|
import { defineStyleMapCapture, discoverNextRoutes } from 'styleproof';
|
|
@@ -292,6 +296,17 @@ semantic roots that were actually present in the computed-style map, so tests ca
|
|
|
292
296
|
assert a capture reached `role="dialog"`, `aria-modal`, `role="menu"`,
|
|
293
297
|
`role="listbox"`, or hot-toast text.
|
|
294
298
|
|
|
299
|
+
Triggers are enumerated once per surface and every reopen re-binds to that same
|
|
300
|
+
element by identity — its DOM path **and** its accessible label — never by
|
|
301
|
+
position. Between popups the surface is reset (Escape + `go()`) and the reset is
|
|
302
|
+
verified: if an overlay a previous popup left behind is still visible (Escape
|
|
303
|
+
closes dialogs, not toasts or status regions), or an enumerated trigger
|
|
304
|
+
disappeared or changed identity (e.g. a same-tag sibling shifted in earlier),
|
|
305
|
+
that candidate is **skipped loudly** — a `styleproof:` warning names the popup and
|
|
306
|
+
why — instead of capturing contaminated state or keying a popup under the wrong
|
|
307
|
+
trigger. Dismiss the leaking overlay in the surface's `go()`, or capture it as an
|
|
308
|
+
explicit variant.
|
|
309
|
+
|
|
295
310
|
**Harvest one-step variants.** Routes are not the whole UI: drawers, tabs,
|
|
296
311
|
dialogs, empty form errors, selects, and other one-step states need their own
|
|
297
312
|
captures. `styleproof-variants` opens a running app, tries semantic controls
|
|
@@ -733,7 +748,48 @@ const result = affectedSurfaces({
|
|
|
733
748
|
// → 'all' (some change couldn't be bounded — capture everything)
|
|
734
749
|
```
|
|
735
750
|
|
|
736
|
-
Two honest limits, both resolving to `'all'`: a computed `import(`../dir/${x}`)` is treated as a bundler **context module** (every file under that dir is a possible target, so precision there is directory-level, never a miss); and the CSS-in-JS global list (`createGlobalStyle`, `injectGlobal`, `globalStyle`, …) must match the libraries you use — an unrecognized global API is the one way a scoped verdict could be unsound
|
|
751
|
+
Two honest limits, both resolving to `'all'`: a computed `import(`../dir/${x}`)` is treated as a bundler **context module** (every file under that dir is a possible target, so precision there is directory-level, never a miss); and a CSS-Module (`.module.scss`/`.module.sass`) that carries a Sass `@use`/`@forward` load resolves to `'all'`, because those pull in a partial the JS import graph can't bound. One honest **residual** stays `'scope'` by design: the CSS-in-JS global list (`createGlobalStyle`, `injectGlobal`, `globalStyle`, …) must match the libraries you use — an allowlist can't fail closed on an _unknown_ member, so an unrecognized global API in a `.tsx` is the one way a scoped verdict could be unsound. Treat an unsupported styling system as a reason to skip selective remap. Because a PR-time miss would be silent, always let `main` (or a scheduled run) capture **all** surfaces as the trust-but-verify net.
|
|
752
|
+
|
|
753
|
+
### Show the skip list, then wire the pre-push hook
|
|
754
|
+
|
|
755
|
+
Before you trust a skip, print it. `explainAffectedSurfaces(result, allSurfaceKeys)` renders the verdict as reviewer-checkable lines — which surfaces re-capture and which reuse their committed base map — and takes an optional reason string for the `'all'` case:
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
import { affectedSurfaces, explainAffectedSurfaces } from 'styleproof';
|
|
759
|
+
|
|
760
|
+
const result = affectedSurfaces(/* … */);
|
|
761
|
+
console.log(explainAffectedSurfaces(result, Object.keys(surfaces)));
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
A scoped change (only `dashboard`'s subtree touched) prints:
|
|
765
|
+
|
|
766
|
+
```
|
|
767
|
+
selective remap: ON → re-capture 1, reuse 2 from base
|
|
768
|
+
↻ dashboard (re-capture — a changed file reaches it)
|
|
769
|
+
✓ home (reuse base map — no changed file reaches it)
|
|
770
|
+
✓ pricing (reuse base map — no changed file reaches it)
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
A global/token change fails closed to a full re-capture:
|
|
774
|
+
|
|
775
|
+
```
|
|
776
|
+
selective remap: OFF → re-capture all 3 surface(s) — src/tokens.css is a global (unscoped) stylesheet
|
|
777
|
+
↻ dashboard (re-capture)
|
|
778
|
+
↻ home (re-capture)
|
|
779
|
+
↻ pricing (re-capture)
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Wired into a pre-push hook, the whole recipe is: diff the changed files, produce the graph, ask `affectedSurfaces`, print the skip list, then capture only the subset and reuse the committed base maps for the rest.
|
|
783
|
+
|
|
784
|
+
```sh
|
|
785
|
+
#!/usr/bin/env sh
|
|
786
|
+
# .husky/pre-push (opt-in; the default CI gate still captures every surface)
|
|
787
|
+
CHANGED=$(git diff --name-only origin/main...HEAD)
|
|
788
|
+
npx dependency-cruiser src --no-config --output-type json > dc.json
|
|
789
|
+
node scripts/selective-remap.mjs "$CHANGED" dc.json # affectedSurfaces + explain + capture subset
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
`scripts/selective-remap.mjs` is yours to own — it maps `dc.json` into `{ from, to }` edges (as above), calls `affectedSurfaces`, prints `explainAffectedSurfaces`, then captures only the returned keys and copies each reused surface's committed base map forward. `main` re-captures everything, so a PR-time miss is still caught at merge.
|
|
737
793
|
|
|
738
794
|
## Newly-added elements show their full style
|
|
739
795
|
|
|
@@ -810,7 +866,7 @@ Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<styl
|
|
|
810
866
|
|
|
811
867
|
- `styleproof-init` — scaffold the gate: the capture spec, a dedicated `playwright.styleproof.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), respect pnpm/Corepack version pins, and detect Vite/Next production preview commands instead of assuming every repo has `start`.
|
|
812
868
|
- `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 `--crawl-base-url` plus repeated `--crawl-route` to run `styleproof-variants` before capture, `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, or `--no-screenshots` for custom flows.
|
|
813
|
-
- `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 reviewable diff — computed-style/DOM/state differences, and equally an unacknowledged inventory removal, an incomplete coverage registry, or an unproven-determinism capture; `2` on a usage/capture error (including a **missing map** — a bundle that claims to exist yet holds zero captures, i.e. a `styleproof-manifest.json` present with no maps, on either side, or a head capture that produced nothing; refused loudly rather than mislabelled as all-new); `3` when only new surfaces are present (no baseline for _those_ surfaces to diff against — new surfaces against an existing baseline, or a base dir with no manifest at all, meaning no baseline was ever captured: the first-adoption review path; approval policy decides whether to gate). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`.
|
|
869
|
+
- `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 reviewable diff — computed-style/DOM/state differences, and equally an unacknowledged inventory removal, an incomplete coverage registry, or an unproven-determinism capture; `2` on a usage/capture error (including a **missing map** — a bundle that claims to exist yet holds zero captures, i.e. a `styleproof-manifest.json` present with no maps, on either side, or a head capture that produced nothing; refused loudly rather than mislabelled as all-new — **and** the no-args case where the cached base map can't be restored at all: no map-store remote, no cached bundle, nothing to compare. A "nothing was compared" outcome always exits `2`, never a soft `0` that would read as certified; the error names the two ways forward — run in CI where the base is restorable, or use the two-directory form); `3` when only new surfaces are present (no baseline for _those_ surfaces to diff against — new surfaces against an existing baseline, or a base dir with no manifest at all, meaning no baseline was ever captured: the first-adoption review path; approval policy decides whether to gate). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`.
|
|
814
870
|
- `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).
|
|
815
871
|
- `styleproof-variants` — crawl a running app for one-step state variants and write `styleproof.variants.generated.json`. Pass `--base-url`, repeat `--route`, and use `--strict` when unresolved skipped/live candidates should fail automation.
|
|
816
872
|
|
|
@@ -55,7 +55,10 @@ whole surface: --crawl
|
|
|
55
55
|
fulfilled with 500 → error render)
|
|
56
56
|
--workers <n> concurrent sweep workers (default 4); same surface set as a
|
|
57
57
|
serial crawl — pass 1 for byte-stable key attribution
|
|
58
|
-
--
|
|
58
|
+
--until-covered stop the crawl early the moment every stylesheet class has
|
|
59
|
+
been rendered — a coverage-oriented sweep for design mockups
|
|
60
|
+
--max-depth <n> throttle recursion depth (default: 16 — backstop for
|
|
61
|
+
append-generator UIs)
|
|
59
62
|
--max-actions <n> throttle controls tried per state (default: unbounded)
|
|
60
63
|
--max-states <n> throttle total surfaces (default: unbounded)
|
|
61
64
|
--no-reset-storage don't clear localStorage between steps (default: clear)
|
package/bin/styleproof-diff.mjs
CHANGED
|
@@ -303,39 +303,48 @@ const invRemovals = printInventoryAudit(inventoryAudit);
|
|
|
303
303
|
const coverageFails = printCoverageVerdict(coverageVerdict);
|
|
304
304
|
const determinismFails = printDeterminismVerdict(determinismVerdict);
|
|
305
305
|
|
|
306
|
-
if (jsonOut)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
unacknowledged:
|
|
323
|
-
|
|
306
|
+
if (jsonOut) {
|
|
307
|
+
// A write failure (bad --json path, unwritable dir) is a usage/setup error, not a
|
|
308
|
+
// "reviewable differences" result — exit 2, never leak the exit-1 that CI reads as
|
|
309
|
+
// a real diff.
|
|
310
|
+
try {
|
|
311
|
+
fs.writeFileSync(
|
|
312
|
+
jsonOut,
|
|
313
|
+
JSON.stringify(
|
|
314
|
+
{
|
|
315
|
+
counts,
|
|
316
|
+
surfaces,
|
|
317
|
+
compared,
|
|
318
|
+
coverage: coverageVerdict,
|
|
319
|
+
determinism: determinismVerdict,
|
|
320
|
+
// The inventory verdict, machine-readable — parallel to coverage/determinism and
|
|
321
|
+
// to the report's certification block. `null` when no capture carried inventory.
|
|
322
|
+
// `unacknowledged` is the gating set: a CI can hard-fail on `unacknowledged.length`.
|
|
323
|
+
inventory: inventoryAudit && {
|
|
324
|
+
removed: inventoryAudit.delta.removed.map((i) => i.key),
|
|
325
|
+
added: inventoryAudit.delta.added.map((i) => i.key),
|
|
326
|
+
unacknowledged: inventoryAudit.unexplained.map((i) => i.key),
|
|
327
|
+
staleAcknowledgements: inventoryAudit.staleAllowances,
|
|
328
|
+
},
|
|
329
|
+
// Explain the `inventory: null` so a gate reading this JSON can tell "armed but no
|
|
330
|
+
// data" apart from "audited, nothing removed". Neither map carried inventory → set
|
|
331
|
+
// `inventory: true` in the capture spec (styleproof-init scaffolds it).
|
|
332
|
+
...(inventoryAudit
|
|
333
|
+
? {}
|
|
334
|
+
: {
|
|
335
|
+
inventoryNote:
|
|
336
|
+
'no captured map carried an inventory — set `inventory: true` in the capture spec to arm the navigable-removal gate',
|
|
337
|
+
}),
|
|
324
338
|
},
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
},
|
|
335
|
-
null,
|
|
336
|
-
2,
|
|
337
|
-
),
|
|
338
|
-
);
|
|
339
|
+
null,
|
|
340
|
+
2,
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error(`${COMMAND}: could not write --json ${jsonOut}: ${e.message}`);
|
|
345
|
+
process.exit(2);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
339
348
|
|
|
340
349
|
const total = counts.dom + counts.style + counts.state;
|
|
341
350
|
const newSurfaces = surfaces.filter((s) => s.missing).length;
|
package/bin/styleproof-init.mjs
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* Writes:
|
|
8
8
|
* - <dir> (default e2e/styleproof.spec.ts): a starter capture spec with a
|
|
9
9
|
* minimal settle() helper (triggers scroll-reveal content; StyleProof itself
|
|
10
|
-
* handles fonts, animation freeze, and the settle). For a detected Next.js app it derives
|
|
11
|
-
* the `expected` coverage guard from the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* handles fonts, animation freeze, and the settle). For a detected Next.js app it derives BOTH the
|
|
11
|
+
* surfaces AND the `expected` coverage guard from the same `discoverNextRoutes()`
|
|
12
|
+
* call, so a static route added later is captured and expected together —
|
|
13
|
+
* auto-covered, never a guard failure; the guard fails only on genuine
|
|
14
|
+
* divergence (a dynamic route, a hand-maintained registry, or a route dropped
|
|
15
|
+
* from surfaces but still expected). Otherwise it writes one sample surface
|
|
16
|
+
* plus a commented guard block to wire to your own route registry.
|
|
14
17
|
* - playwright.styleproof.config.ts: a dedicated production-build Playwright
|
|
15
18
|
* config for StyleProof captures, so an existing app Playwright config is
|
|
16
19
|
* never disturbed or accidentally reused.
|
|
@@ -23,7 +26,12 @@
|
|
|
23
26
|
*/
|
|
24
27
|
import fs from 'node:fs';
|
|
25
28
|
import path from 'node:path';
|
|
26
|
-
|
|
29
|
+
// Import from the leaf module, not the barrel: styleproof-init only scaffolds
|
|
30
|
+
// files and never captures. Pulling `../dist/index.js` here dragged the whole
|
|
31
|
+
// library — capture, crawler, report, and six Playwright-importing modules —
|
|
32
|
+
// into a tiny scaffolder's load path, and that oversized concurrent module
|
|
33
|
+
// graph is what made init's tests flake in CI. routes.js needs only fs + path.
|
|
34
|
+
import { discoverNextRoutes } from '../dist/routes.js';
|
|
27
35
|
import { isHelpArg, showHelpAndExit } from '../dist/cli-errors.js';
|
|
28
36
|
|
|
29
37
|
const HELP = `styleproof-init — scaffold a styleproof capture spec
|
|
@@ -39,9 +47,10 @@ options:
|
|
|
39
47
|
|
|
40
48
|
What it writes:
|
|
41
49
|
- the spec at --dir, with a minimal settle() helper (scroll-reveal only).
|
|
42
|
-
In a Next.js app it discovers your routes at run time and
|
|
43
|
-
surfaces and the \`expected\` coverage guard
|
|
44
|
-
|
|
50
|
+
In a Next.js app it discovers your routes at run time and derives both the
|
|
51
|
+
surfaces and the \`expected\` coverage guard from that one call, so a new static
|
|
52
|
+
route is auto-covered (captured + expected together); the guard fails only when
|
|
53
|
+
the two diverge. Otherwise it writes one sample surface + a commented guard block.
|
|
45
54
|
- playwright.styleproof.config.ts, a dedicated production-build Playwright config
|
|
46
55
|
- .github/workflows/styleproof.yml, a cache-first PR report workflow
|
|
47
56
|
|
|
@@ -114,9 +123,12 @@ const HEADER = `/**
|
|
|
114
123
|
* npx styleproof-diff # compare cached base/head maps by commit SHA
|
|
115
124
|
*/`;
|
|
116
125
|
|
|
117
|
-
// Next.js detected: derive
|
|
118
|
-
// routes AT RUN TIME,
|
|
119
|
-
//
|
|
126
|
+
// Next.js detected: derive BOTH surfaces and the coverage guard from the app's
|
|
127
|
+
// routes AT RUN TIME, from one `discoverNextRoutes()` call — so a static page added
|
|
128
|
+
// later is a captured surface AND `expected` in the same step (auto-covered, never a
|
|
129
|
+
// guard failure), with no static list to drift. The guard fires only when the two
|
|
130
|
+
// diverge (a dynamic route, a hand-maintained registry, or a route dropped from
|
|
131
|
+
// surfaces but still expected).
|
|
120
132
|
const NEXT_SPEC = `import type { Page } from '@playwright/test';
|
|
121
133
|
import { defineStyleMapCapture, discoverNextRoutes, type Surface } from 'styleproof';
|
|
122
134
|
|
|
@@ -124,12 +136,12 @@ ${HEADER}
|
|
|
124
136
|
|
|
125
137
|
${SETTLE}
|
|
126
138
|
|
|
127
|
-
// Routes discovered from your Next.js app (app/ + pages/) at RUN TIME
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// is your spec. Static routes each get a capture;
|
|
131
|
-
// navigated without a value, so they're listed in
|
|
132
|
-
// surface with a concrete param.
|
|
139
|
+
// Routes discovered from your Next.js app (app/ + pages/) at RUN TIME. Both SURFACES
|
|
140
|
+
// and \`expected\` below come from this one list, so a static route you add later is
|
|
141
|
+
// captured and expected together — covered automatically, with no surface list to
|
|
142
|
+
// keep in sync. Edit freely; this is your spec. Static routes each get a capture;
|
|
143
|
+
// dynamic [param] routes can't be navigated without a value, so they're listed in
|
|
144
|
+
// \`exclude\` until you add a surface with a concrete param.
|
|
133
145
|
const ROUTES = discoverNextRoutes();
|
|
134
146
|
|
|
135
147
|
const SURFACES: Surface[] = ROUTES.filter((r) => !r.dynamic).map((r) => ({
|
|
@@ -146,9 +158,10 @@ const SURFACES: Surface[] = ROUTES.filter((r) => !r.dynamic).map((r) => ({
|
|
|
146
158
|
|
|
147
159
|
defineStyleMapCapture({
|
|
148
160
|
surfaces: SURFACES,
|
|
149
|
-
// Coverage guard: every
|
|
150
|
-
// the suite fails (it runs without STYLEMAP_DIR — a static check, no browser).
|
|
151
|
-
//
|
|
161
|
+
// Coverage guard: every \`expected\` route must be a captured surface or excluded, or
|
|
162
|
+
// the suite fails (it runs without STYLEMAP_DIR — a static check, no browser). Since
|
|
163
|
+
// both sides come from ROUTES, static routes never trip it; it fires when they
|
|
164
|
+
// diverge — a dynamic route (excluded below), or a route you drop from SURFACES.
|
|
152
165
|
expected: ROUTES.map((r) => r.key),
|
|
153
166
|
exclude: Object.fromEntries(
|
|
154
167
|
ROUTES.filter((r) => r.dynamic).map((r) => [r.key, \`dynamic route (\${r.path}) — add a surface with a concrete param\`]),
|
|
@@ -480,9 +493,14 @@ const isNext = routes.length > 0;
|
|
|
480
493
|
const SPEC = isNext ? NEXT_SPEC : GENERIC_SPEC;
|
|
481
494
|
|
|
482
495
|
let wroteSomething = false;
|
|
496
|
+
// Every path init created or modified this run, so the summary can name exactly what
|
|
497
|
+
// it touched — and, by omission, what it did NOT (init never writes package.json or a
|
|
498
|
+
// lockfile; that's the package manager's `install`, not this scaffolder).
|
|
499
|
+
const touched = [];
|
|
483
500
|
|
|
484
501
|
const spec = writeFileSafe(specPath, SPEC, { force });
|
|
485
502
|
if (spec.wrote) {
|
|
503
|
+
touched.push(specPath);
|
|
486
504
|
console.log(`${spec.exists ? 'overwrote' : 'created'} ${specPath}`);
|
|
487
505
|
if (isNext) {
|
|
488
506
|
const dynamic = routes.filter((r) => r.dynamic).length;
|
|
@@ -502,6 +520,7 @@ if (spec.wrote) {
|
|
|
502
520
|
const configPath = 'playwright.styleproof.config.ts';
|
|
503
521
|
const config = writeFileSafe(configPath, CONFIG, { force });
|
|
504
522
|
if (config.wrote) {
|
|
523
|
+
touched.push(configPath);
|
|
505
524
|
console.log(`${config.exists ? 'overwrote' : 'created'} ${configPath} (dedicated StyleProof capture config)`);
|
|
506
525
|
wroteSomething = true;
|
|
507
526
|
} else {
|
|
@@ -515,6 +534,7 @@ if (fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js
|
|
|
515
534
|
|
|
516
535
|
const ignored = ['.styleproof/', 'test-results/', 'playwright-report/'].filter((line) => ensureGitignoreLine(line));
|
|
517
536
|
if (ignored.length) {
|
|
537
|
+
touched.push('.gitignore');
|
|
518
538
|
console.log(`updated .gitignore (${ignored.join(', ')})`);
|
|
519
539
|
wroteSomething = true;
|
|
520
540
|
}
|
|
@@ -522,12 +542,21 @@ if (ignored.length) {
|
|
|
522
542
|
// Cache-first CI report — never overwrite an existing workflow.
|
|
523
543
|
const ci = writeFileSafe(CI_PATH, CI_WORKFLOW);
|
|
524
544
|
if (ci.wrote) {
|
|
545
|
+
touched.push(CI_PATH);
|
|
525
546
|
console.log(`created ${CI_PATH} (cache-first StyleProof report)`);
|
|
526
547
|
wroteSomething = true;
|
|
527
548
|
} else {
|
|
528
549
|
console.log(`${CI_PATH} already exists — left untouched`);
|
|
529
550
|
}
|
|
530
551
|
|
|
552
|
+
if (touched.length) {
|
|
553
|
+
// State exactly what init wrote, and — because adopters have blamed init for the
|
|
554
|
+
// `styleproof` entry their package manager's `install` added — say plainly that it
|
|
555
|
+
// did NOT touch package.json or the lockfile. Truth over assumption.
|
|
556
|
+
console.log(`\nstyleproof-init wrote only: ${touched.join(', ')}`);
|
|
557
|
+
console.log('It did NOT modify package.json or your lockfile (that was your package manager’s install).');
|
|
558
|
+
}
|
|
559
|
+
|
|
531
560
|
console.log('\nHow the gate works — it runs on your first PR with no extra steps:');
|
|
532
561
|
console.log(' 1. Commit and open a PR. CI captures the base and head surfaces in one pinned');
|
|
533
562
|
console.log(' environment and posts the StyleProof report — no local step required.');
|
package/bin/styleproof-map.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
unknownFlagMessage,
|
|
23
23
|
} from '../dist/cli-errors.js';
|
|
24
24
|
import {
|
|
25
|
+
BROWSER_BUILD_SIDECAR,
|
|
25
26
|
DEFAULT_MAP_DIR,
|
|
26
27
|
DEFAULT_MAP_LABEL,
|
|
27
28
|
DEFAULT_MAP_STORE_BRANCH,
|
|
@@ -285,6 +286,13 @@ if (restore) {
|
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
// Clear any prior run's browser-build sidecar before Playwright runs, so ONLY this
|
|
290
|
+
// run can have written it. The default dir (.styleproof/maps/current) is reused across
|
|
291
|
+
// runs; if this run records no browser version (the capture test not reached, or the
|
|
292
|
+
// handle unavailable), a stale sidecar would otherwise be read into the manifest and
|
|
293
|
+
// stamp a WRONG browser build that the compatibility guard then trusts as a fingerprint.
|
|
294
|
+
fs.rmSync(path.join(targetDir, BROWSER_BUILD_SIDECAR), { force: true });
|
|
295
|
+
|
|
288
296
|
const command = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
|
|
289
297
|
const configArgs =
|
|
290
298
|
fs.existsSync(STYLEPROOF_PLAYWRIGHT_CONFIG) && !hasPlaywrightConfigArg(playwrightArgs)
|
|
@@ -67,9 +67,33 @@ export type AffectedSurfaces = Set<string> | 'all';
|
|
|
67
67
|
* unrecognized, is `'all'`.
|
|
68
68
|
*/
|
|
69
69
|
export declare function classifyStyleChange(file: string, readFile: (p: string) => string): 'scope' | 'all';
|
|
70
|
+
/**
|
|
71
|
+
* Canonicalize a repo-relative path so the same file spells the same regardless
|
|
72
|
+
* of source (a `surfaces` value, a `changedFiles` entry, or a graph edge). Two
|
|
73
|
+
* tools disagree on `./pages/Home.tsx` vs `pages/Home.tsx` vs `pages//Home.tsx`;
|
|
74
|
+
* without one spelling, a reverse-reachability hit can silently miss the surface
|
|
75
|
+
* whose entry key was spelled differently, dropping it from the affected set —
|
|
76
|
+
* an unsound skip. Byte-cheap and fs-free: strip a leading `./`, collapse `//`,
|
|
77
|
+
* and drop `.`/`..` segments as pure string math (no realpath, no resolution). */
|
|
78
|
+
export declare function canonicalPath(p: string): string;
|
|
70
79
|
/**
|
|
71
80
|
* Compute the set of declared surfaces a change could have altered, or `'all'`.
|
|
72
81
|
* See the module doc for the soundness contract. Any not in the returned set are
|
|
73
82
|
* provably unaffected and may reuse their committed base map.
|
|
74
83
|
*/
|
|
75
84
|
export declare function affectedSurfaces(input: AffectedSurfacesInput): AffectedSurfaces;
|
|
85
|
+
/**
|
|
86
|
+
* Render an {@link affectedSurfaces} verdict as human-readable lines a pre-push
|
|
87
|
+
* hook (or CI log) can print, so a reviewer can sanity-check the skip list before
|
|
88
|
+
* trusting it. Pure formatter — no I/O, no graph work.
|
|
89
|
+
*
|
|
90
|
+
* @param result the value {@link affectedSurfaces} returned.
|
|
91
|
+
* @param allSurfaces every declared surface key (e.g. `Object.keys(surfaces)`),
|
|
92
|
+
* so the helper can name what is *reused from base* — the ones
|
|
93
|
+
* the verdict skips — not just what re-captures.
|
|
94
|
+
* @param reason optional one-line explanation for an `'all'` verdict (e.g.
|
|
95
|
+
* the classifying file, from {@link classifyStyleChange}). The
|
|
96
|
+
* library doesn't attach a reason to the sentinel, so pass it
|
|
97
|
+
* if the caller knows why; omitted, the `'all'` line stands alone.
|
|
98
|
+
*/
|
|
99
|
+
export declare function explainAffectedSurfaces(result: AffectedSurfaces, allSurfaces: Iterable<string>, reason?: string): string;
|