styleproof 3.20.0 → 3.21.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 +54 -0
- package/README.md +1 -1
- package/bin/styleproof-diff.mjs +97 -22
- package/dist/capture.d.ts +8 -0
- package/dist/capture.js +22 -0
- package/dist/change-groups.d.ts +91 -0
- package/dist/change-groups.js +471 -0
- package/dist/report.d.ts +1 -4
- package/dist/report.js +70 -352
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.21.0] - 2026-07-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Shared-chrome tier in the report and the `styleproof-diff` CLI.** When one
|
|
15
|
+
change rides the frame every view draws — a persistent nav rail, header, or
|
|
16
|
+
footer that moved on every surface that renders it — it is promoted to a single
|
|
17
|
+
"🧱 Global chrome change" callout at the top, with the detail folded beneath,
|
|
18
|
+
instead of repeating across a long surface list on several entries. The reviewer
|
|
19
|
+
reads "the nav changed everywhere" once. The threshold is **structural, not a
|
|
20
|
+
tunable knob**: an element path is chrome only when it is hosted on more than one
|
|
21
|
+
surface base and changed on _every_ base that hosts it (full coverage of its
|
|
22
|
+
hosting surfaces). A change on merely some surfaces, or a view's own content
|
|
23
|
+
change entangled with the frame change, is never promoted — the view-specific
|
|
24
|
+
detail always stays visible. Purely presentational: grouping keys, findings,
|
|
25
|
+
counts, exit codes, and `--json` are unchanged.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **`styleproof-diff` human output now reuses the report's grouping.** One real
|
|
30
|
+
change no longer prints once per surface with its derived-longhand echo: the CLI
|
|
31
|
+
groups surfaces that changed identically into one finding (with the per-surface
|
|
32
|
+
count on the header line), summarises longhands into shorthands, and folds the
|
|
33
|
+
size/position-derived longhands (`transform-origin`, `width`/`height`, cascaded
|
|
34
|
+
ancestor heights…) behind a `(+N derived longhands)` count. A one-view button
|
|
35
|
+
restyle at one width that used to fill dozens of raw lines now reads as a single
|
|
36
|
+
grouped finding. `--json` stays the complete, byte-stable machine contract (every
|
|
37
|
+
surface, every raw longhand); exit codes are unchanged.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **The report's certification block no longer contradicts the diff on inventory
|
|
42
|
+
additions.** For the same capture pair, `styleproof-diff` correctly prints a
|
|
43
|
+
navigable addition (additive, non-gating), but `styleproof-report`'s certification
|
|
44
|
+
block still read `Inventory — ✓ navigable set unchanged` — telling a reviewer the
|
|
45
|
+
navigable set didn't change when it did. The inventory line now echoes additions as
|
|
46
|
+
an informational, still-✓-class clause (`✓ N navigable affordance(s) added: <keys>
|
|
47
|
+
(additions don't gate)`), with the same truncation and key-escaping discipline as
|
|
48
|
+
the removals line. Removals still drive the ⚠/✗ gate semantics; additions are
|
|
49
|
+
appended. The diff and the report can no longer disagree about the navigable delta.
|
|
50
|
+
- **The unit suite no longer flakes on spawned-CLI tests (test-infra only, no
|
|
51
|
+
consumer-visible change).** The package-smoke test packed the live checkout with
|
|
52
|
+
`npm pack`, which runs the `prepare` lifecycle (`tsc`) even under `--ignore-scripts`.
|
|
53
|
+
`tsc` truncates each `dist/*.js` to zero bytes before rewriting it, so a CLI spawned
|
|
54
|
+
by a _different_ test running concurrently under `node --test` could read a
|
|
55
|
+
half-written `dist` module and die with a static ESM link error
|
|
56
|
+
(`does not provide an export named …`) — surfacing as an "impossible" exit code (a
|
|
57
|
+
flag-rejection test that pins exit `2` seeing exit `1`, or a spawned bin printing a
|
|
58
|
+
missing-export `SyntaxError`). The smoke test now packs a staged copy of the package
|
|
59
|
+
whose manifest carries no lifecycle scripts, so pack can never rebuild — the shared
|
|
60
|
+
`dist` is never mutated mid-suite. Same coverage (the packed tarball is still
|
|
61
|
+
installed and its API + every bin's `--help` exercised); the packed artifact is
|
|
62
|
+
identical.
|
|
63
|
+
|
|
10
64
|
## [3.20.0] - 2026-07-07
|
|
11
65
|
|
|
12
66
|
### Fixed
|
package/README.md
CHANGED
|
@@ -866,7 +866,7 @@ Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<styl
|
|
|
866
866
|
|
|
867
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`.
|
|
868
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.
|
|
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`.
|
|
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`. The human output **groups the same way the report does**: surfaces that changed identically collapse into one finding (with the per-surface count on its header), longhands fold into shorthands, and size/position-derived longhands fold behind a `(+N derived longhands)` count — so one real change reads as one entry, not dozens of raw lines. A change that rode the shared frame every view draws (a persistent nav/header/footer) is promoted to a "🧱 Global chrome change" callout up top. `--json` stays the complete, unchanged machine contract — every surface and every raw longhand — regardless of the human grouping.
|
|
870
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).
|
|
871
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.
|
|
872
872
|
|
package/bin/styleproof-diff.mjs
CHANGED
|
@@ -25,6 +25,20 @@
|
|
|
25
25
|
import fs from 'node:fs';
|
|
26
26
|
import path from 'node:path';
|
|
27
27
|
import { diffStyleMapDirs, findingLabel } from '../dist/diff.js';
|
|
28
|
+
// The shared grouping brain (leaf — no Playwright-adjacent imports) that already
|
|
29
|
+
// dedupes the report: group identical change-sets across surfaces and fold derived
|
|
30
|
+
// longhands. Used for the HUMAN output only; --json stays the raw machine contract.
|
|
31
|
+
import {
|
|
32
|
+
cleanFindings,
|
|
33
|
+
groupBySignature,
|
|
34
|
+
groupByPath,
|
|
35
|
+
groupTitle,
|
|
36
|
+
summarizeProps,
|
|
37
|
+
derivedLonghandCount,
|
|
38
|
+
formatSurfaceList,
|
|
39
|
+
classifyChrome,
|
|
40
|
+
surfaceBase,
|
|
41
|
+
} from '../dist/change-groups.js';
|
|
28
42
|
import {
|
|
29
43
|
DEFAULT_MAP_STORE_BRANCH,
|
|
30
44
|
DEFAULT_REMOTE,
|
|
@@ -39,7 +53,7 @@ import {
|
|
|
39
53
|
showHelpAndExit,
|
|
40
54
|
unknownFlagMessage,
|
|
41
55
|
} from '../dist/cli-errors.js';
|
|
42
|
-
import { readInventories } from '../dist/capture.js';
|
|
56
|
+
import { readInventories, surfaceElementPaths } from '../dist/capture.js';
|
|
43
57
|
import { auditRunInventory, readAckFile } from '../dist/inventory.js';
|
|
44
58
|
import { auditCoverage, auditDeterminism, COVERAGE_LEDGER } from '../dist/coverage.js';
|
|
45
59
|
import { isMapFile } from '../dist/map-store.js';
|
|
@@ -254,6 +268,7 @@ let result;
|
|
|
254
268
|
let inventoryAudit = null;
|
|
255
269
|
let coverageVerdict = null;
|
|
256
270
|
let determinismVerdict = null;
|
|
271
|
+
let surfacePaths = new Map();
|
|
257
272
|
try {
|
|
258
273
|
assertCompatibleMapDirs(dirA, dirB);
|
|
259
274
|
result = diffStyleMapDirs(dirA, dirB);
|
|
@@ -264,6 +279,9 @@ try {
|
|
|
264
279
|
const headLedger = readLedger(dirB);
|
|
265
280
|
coverageVerdict = auditCoverage(capturedSurfaceKeys(dirB), headLedger);
|
|
266
281
|
determinismVerdict = auditDeterminism(readLedger(dirA), headLedger);
|
|
282
|
+
// Element-path sets per surface, for the shared-chrome tier — same "read while
|
|
283
|
+
// the dirs exist" rule as the ledgers above.
|
|
284
|
+
surfacePaths = surfaceElementPaths(dirA, dirB);
|
|
267
285
|
} catch (e) {
|
|
268
286
|
console.error(e.message);
|
|
269
287
|
process.exit(2);
|
|
@@ -272,33 +290,90 @@ try {
|
|
|
272
290
|
}
|
|
273
291
|
const { surfaces, counts, compared } = result;
|
|
274
292
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
293
|
+
// ── grouped human output ─────────────────────────────────────────────────────
|
|
294
|
+
// Reuse the report's dedup so one real change doesn't print once per surface with
|
|
295
|
+
// its derived-longhand echo: group surfaces that changed identically, fold the
|
|
296
|
+
// size/position-derived longhands behind a count, and keep the per-surface tally
|
|
297
|
+
// in each group's header line. --json below is untouched (the raw machine feed).
|
|
298
|
+
|
|
299
|
+
// One finding's lines: a heading, then its summarised property deltas (the same
|
|
300
|
+
// dedupe the report shows). Returns [] for a DOM finding (handled separately) or a
|
|
301
|
+
// finding whose props all summarised away.
|
|
302
|
+
function findingLines(f) {
|
|
303
|
+
if (f.kind === 'dom') return [];
|
|
304
|
+
const rows = summarizeProps(f.props);
|
|
305
|
+
if (!rows.length) return [];
|
|
306
|
+
const head =
|
|
307
|
+
f.kind === 'state'
|
|
308
|
+
? ` [:${f.state}] ${findingLabel(f.path, f.cls)}${f.sub !== f.path ? ` ⇒ ${f.sub}` : ''}`
|
|
309
|
+
: ` ${findingLabel(f.path, f.cls)}${f.pseudo || ''}`;
|
|
310
|
+
return [head, ...rows.map((p) => ` ${p.prop}: ${p.before} → ${p.after}`)];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A DOM finding's one-line heading (added/removed/retagged).
|
|
314
|
+
function domLine(dom) {
|
|
315
|
+
return dom.change === 'retagged'
|
|
316
|
+
? ` DOM retagged: ${dom.path} ${dom.detail ?? ''}`
|
|
317
|
+
: ` DOM ${dom.change}: ${findingLabel(dom.path, dom.cls)}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// The element lines for one change group, from its representative's cleaned
|
|
321
|
+
// findings: one heading per element, then its summarised property deltas.
|
|
322
|
+
function elementLines(findings) {
|
|
281
323
|
const lines = [];
|
|
282
|
-
for (const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
? ` DOM retagged: ${f.path} ${f.detail}`
|
|
287
|
-
: ` DOM ${f.change}: ${findingLabel(f.path, f.cls)}`,
|
|
288
|
-
);
|
|
289
|
-
} else if (f.kind === 'style') {
|
|
290
|
-
lines.push(` ${findingLabel(f.path, f.cls)}${f.pseudo || ''}`);
|
|
291
|
-
for (const p of f.props) lines.push(` ${p.prop}: ${p.before} → ${p.after}`);
|
|
292
|
-
} else {
|
|
293
|
-
lines.push(` [:${f.state}] ${findingLabel(f.path, f.cls)}${f.sub !== f.path ? ` ⇒ ${f.sub}` : ''}`);
|
|
294
|
-
for (const p of f.props) lines.push(` ${p.prop}: ${p.before} → ${p.after}`);
|
|
295
|
-
}
|
|
324
|
+
for (const group of groupByPath(findings)) {
|
|
325
|
+
const dom = group.find((f) => f.kind === 'dom');
|
|
326
|
+
if (dom) lines.push(domLine(dom));
|
|
327
|
+
for (const f of group) lines.push(...findingLines(f));
|
|
296
328
|
}
|
|
297
|
-
|
|
329
|
+
return lines;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// New (one-sided) surfaces keep their own line, unchanged.
|
|
333
|
+
for (const sd of surfaces) {
|
|
334
|
+
if (!sd.missing) continue;
|
|
335
|
+
const side = sd.missing === 'before' ? 'after' : 'before';
|
|
336
|
+
console.log(`\n${sd.surface}: new surface — captured only in the ${side} set, no baseline to compare`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Group the changed surfaces the way the report does, so an identical change
|
|
340
|
+
// across N surfaces prints once (with the count), not N times.
|
|
341
|
+
const preparedForGrouping = surfaces
|
|
342
|
+
.filter((sd) => !sd.missing)
|
|
343
|
+
// Carry the RAW findings too, so we can report how many derived longhands the
|
|
344
|
+
// grouped view folded (the cleaned findings have them already removed).
|
|
345
|
+
.map((sd) => ({ surface: sd.surface, findings: cleanFindings(sd.findings), raw: sd.findings }))
|
|
346
|
+
.filter((p) => p.findings.length > 0);
|
|
347
|
+
|
|
348
|
+
function printGroup(cg) {
|
|
349
|
+
const lines = elementLines(cg.findings);
|
|
350
|
+
const derived = derivedLonghandCount(cg.rep.raw);
|
|
351
|
+
const foldNote = derived > 0 ? ` (+${derived} derived longhand${derived === 1 ? '' : 's'})` : '';
|
|
352
|
+
const others = cg.surfaces.length - 1;
|
|
353
|
+
const scope =
|
|
354
|
+
others > 0
|
|
355
|
+
? `${cg.rep.surface} (+${others} more surface${others === 1 ? '' : 's'}: ${formatSurfaceList(cg.surfaces)})`
|
|
356
|
+
: cg.rep.surface;
|
|
357
|
+
console.log(`\n${scope}: ${groupTitle(cg.findings)}${foldNote}`);
|
|
298
358
|
for (const line of lines.slice(0, MAX)) console.log(line);
|
|
299
359
|
if (lines.length > MAX) console.log(` ... and ${lines.length - MAX} more lines (re-run with --max ${lines.length})`);
|
|
300
360
|
}
|
|
301
361
|
|
|
362
|
+
// Shared-chrome tier: a change that rode the frame every view draws (nav/header)
|
|
363
|
+
// gets one banner up top, then its detail — so the reviewer reads "the nav changed
|
|
364
|
+
// everywhere" once, not once per surface entry. Presentational only.
|
|
365
|
+
const grouped = groupBySignature(preparedForGrouping);
|
|
366
|
+
const { chrome, rest } = classifyChrome(grouped, surfacePaths);
|
|
367
|
+
if (chrome.length) {
|
|
368
|
+
// Base count from the pre-cleanup surface set (dirB may be deleted by now).
|
|
369
|
+
const bases = new Set([...surfacePaths.keys()].map(surfaceBase)).size;
|
|
370
|
+
console.log(
|
|
371
|
+
`\n🧱 Global chrome change(s) — across all ${bases} surface(s): ${chrome.length} change(s) rode the shared frame every view draws (a persistent nav, header, or footer).`,
|
|
372
|
+
);
|
|
373
|
+
for (const cg of chrome) printGroup(cg);
|
|
374
|
+
}
|
|
375
|
+
for (const cg of rest) printGroup(cg);
|
|
376
|
+
|
|
302
377
|
const invRemovals = printInventoryAudit(inventoryAudit);
|
|
303
378
|
const coverageFails = printCoverageVerdict(coverageVerdict);
|
|
304
379
|
const determinismFails = printDeterminismVerdict(determinismVerdict);
|
package/dist/capture.d.ts
CHANGED
|
@@ -261,4 +261,12 @@ export declare function loadStyleMap(filePath: string): StyleMap;
|
|
|
261
261
|
export declare function readInventories(dir: string): Array<{
|
|
262
262
|
inventory?: NavigableItem[];
|
|
263
263
|
}>;
|
|
264
|
+
/**
|
|
265
|
+
* Each captured surface key → the set of element paths it renders, unioned across
|
|
266
|
+
* the given dirs (so an element present on only one side still "belongs" to the
|
|
267
|
+
* surface). Feeds the shared-chrome tier, which needs to know, per surface, which
|
|
268
|
+
* paths exist to tell a frame-wide change from one view's content. Shared by the
|
|
269
|
+
* report and the diff CLI (both already read maps this way).
|
|
270
|
+
*/
|
|
271
|
+
export declare function surfaceElementPaths(...dirs: string[]): Map<string, Set<string>>;
|
|
264
272
|
export {};
|
package/dist/capture.js
CHANGED
|
@@ -805,3 +805,25 @@ export function readInventories(dir) {
|
|
|
805
805
|
.map((f) => loadStyleMap(path.join(dir, f)))
|
|
806
806
|
.map((m) => (m.inventory ? { inventory: m.inventory } : {}));
|
|
807
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Each captured surface key → the set of element paths it renders, unioned across
|
|
810
|
+
* the given dirs (so an element present on only one side still "belongs" to the
|
|
811
|
+
* surface). Feeds the shared-chrome tier, which needs to know, per surface, which
|
|
812
|
+
* paths exist to tell a frame-wide change from one view's content. Shared by the
|
|
813
|
+
* report and the diff CLI (both already read maps this way).
|
|
814
|
+
*/
|
|
815
|
+
export function surfaceElementPaths(...dirs) {
|
|
816
|
+
const bySurface = new Map();
|
|
817
|
+
for (const dir of dirs) {
|
|
818
|
+
if (!fs.existsSync(dir))
|
|
819
|
+
continue;
|
|
820
|
+
for (const file of fs.readdirSync(dir).filter(isMapFile)) {
|
|
821
|
+
const surface = file.replace(/\.json(\.gz)?$/, '');
|
|
822
|
+
const set = bySurface.get(surface) ?? new Set();
|
|
823
|
+
for (const p of Object.keys(loadStyleMap(path.join(dir, file)).elements))
|
|
824
|
+
set.add(p);
|
|
825
|
+
bySurface.set(surface, set);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return bySurface;
|
|
829
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type Finding, type PropChange } from './diff.js';
|
|
2
|
+
export declare const isNonValue: (v: string) => boolean;
|
|
3
|
+
export declare function summarizeProps(props: PropChange[]): PropChange[];
|
|
4
|
+
/** `div.who-grid`, `a.nav-cta`, `h3` — the semantic marker class, else the tag. */
|
|
5
|
+
export declare function prettyLabel(p: string, cls: string): string;
|
|
6
|
+
export declare const safeKey: (s: string) => string;
|
|
7
|
+
export declare const surfaceBase: (s: string) => string;
|
|
8
|
+
export declare const surfaceWidth: (s: string) => number;
|
|
9
|
+
export declare function pushSurfaceWidth(byBase: Map<string, number[]>, base: string, surface: string): void;
|
|
10
|
+
export declare function renderSurfaceGroups(byBase: Map<string, number[]>): string;
|
|
11
|
+
/** "landing @ 1280, 1080, 390 · landing-nav-open @ 1080" from the surface keys. */
|
|
12
|
+
export declare function formatSurfaceList(surfaces: string[]): string;
|
|
13
|
+
/** Group findings by their element path (one group per changed element). */
|
|
14
|
+
export declare function groupByPath(findings: Finding[]): Finding[][];
|
|
15
|
+
/** Canonical signature of a surface's findings: surfaces that changed in the
|
|
16
|
+
* same way collapse into one section + one image (the rects differ per width;
|
|
17
|
+
* the change itself does not). */
|
|
18
|
+
export declare function signatureOf(findings: Finding[]): string;
|
|
19
|
+
/** A one-line heading for a change group: "1 element added", "2 elements restyled". */
|
|
20
|
+
export declare function groupTitle(findings: Finding[]): string;
|
|
21
|
+
/** How many of a surface's summarised props are derived/box longhands — the count
|
|
22
|
+
* the CLI folds behind `(+N derived longhands)`. Counts on the RAW finding props
|
|
23
|
+
* (before cleaning) so the CLI can advertise exactly what it suppressed. */
|
|
24
|
+
export declare function derivedLonghandCount(findings: Finding[]): number;
|
|
25
|
+
/**
|
|
26
|
+
* Strip the noise the visual report shouldn't carry, cross-referencing each
|
|
27
|
+
* element's layers so the forced-state layer stops echoing the base:
|
|
28
|
+
* - base/pseudo styles: drop size/position-derived longhands (reflow casualties);
|
|
29
|
+
* - forced states: drop derived + grid-track props, drop a delta the BASE
|
|
30
|
+
* already changed (a `:hover color` that just follows a recoloured base is an
|
|
31
|
+
* echo, not a dropped variant), and drop non-value↔non-value rows;
|
|
32
|
+
* - any finding left with no props is removed entirely.
|
|
33
|
+
*/
|
|
34
|
+
export declare function cleanFindings(findings: Finding[]): Finding[];
|
|
35
|
+
/** A surface's diff distilled to just what grouping needs: its key and the
|
|
36
|
+
* findings kept after noise-cleaning. */
|
|
37
|
+
export type SurfaceFindings = {
|
|
38
|
+
surface: string;
|
|
39
|
+
findings: Finding[];
|
|
40
|
+
};
|
|
41
|
+
/** Surfaces that changed the SAME way, collapsed to one group + a representative. */
|
|
42
|
+
export type SignatureGroup = {
|
|
43
|
+
surfaces: string[];
|
|
44
|
+
rep: SurfaceFindings;
|
|
45
|
+
findings: Finding[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Group surfaces that changed identically (same signature) into one group each,
|
|
49
|
+
* keeping the widest surface as the representative. The rects differ per width;
|
|
50
|
+
* the change itself does not. Callers pass the already-prepared per-surface
|
|
51
|
+
* findings (missing/one-sided surfaces excluded upstream).
|
|
52
|
+
*/
|
|
53
|
+
export declare function groupBySignature(prepared: SurfaceFindings[]): SignatureGroup[];
|
|
54
|
+
/**
|
|
55
|
+
* The set of element paths that changed as SHARED CHROME — a persistent frame
|
|
56
|
+
* element (nav rail, header, footer) that every view renders and that moved on
|
|
57
|
+
* every view that renders it.
|
|
58
|
+
*
|
|
59
|
+
* The rule is STRUCTURAL, deliberately NOT a tunable percentage. For each changed
|
|
60
|
+
* path we compare two base-key sets:
|
|
61
|
+
* - `hosting` — the surface bases whose style map contains the path at all;
|
|
62
|
+
* - `changed` — the surface bases where the path appears in a finding.
|
|
63
|
+
* The path is chrome iff it is hosted on MORE THAN ONE base and it changed on
|
|
64
|
+
* EVERY base that hosts it (`changed ⊇ hosting`). "Every surface that has this
|
|
65
|
+
* element changed it" is exactly what shared chrome means; it needs no threshold
|
|
66
|
+
* to tune or defend. A content element (hosted on one base) fails the >1 guard; a
|
|
67
|
+
* partial change (some hosting bases unchanged) fails the coverage guard.
|
|
68
|
+
*
|
|
69
|
+
* Widths of one base collapse to the base key, so a nav present at @1280 and @390
|
|
70
|
+
* counts once. `surfacePaths` maps each captured surface key → the element paths
|
|
71
|
+
* it renders (union of both sides from the caller).
|
|
72
|
+
*/
|
|
73
|
+
export declare function chromePaths(changedOnSurfaces: Array<{
|
|
74
|
+
path: string;
|
|
75
|
+
surfaces: string[];
|
|
76
|
+
}>, surfacePaths: Map<string, Set<string>>): Set<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Split signature groups into the shared-chrome tier and the rest. A group is
|
|
79
|
+
* promoted only when EVERY one of its affected element paths is a chrome path
|
|
80
|
+
* (see `chromePaths`) — a group that entangles a frame change with a view's own
|
|
81
|
+
* content change stays in `rest` and renders in place, so we never hide a
|
|
82
|
+
* content change under a chrome banner.
|
|
83
|
+
*/
|
|
84
|
+
export declare function classifyChrome<G extends {
|
|
85
|
+
surfaces: string[];
|
|
86
|
+
findings: Finding[];
|
|
87
|
+
}>(groups: G[], surfacePaths: Map<string, Set<string>>): {
|
|
88
|
+
chrome: G[];
|
|
89
|
+
rest: G[];
|
|
90
|
+
chromePaths: Set<string>;
|
|
91
|
+
};
|