styleproof 3.9.0 → 3.10.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 +16 -0
- package/bin/styleproof-diff.mjs +43 -20
- package/dist/coverage.d.ts +19 -0
- package/dist/coverage.js +11 -0
- package/dist/runner.js +12 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.10.0] - 2026-07-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Determinism provenance — a green needs a proven-deterministic capture.** Source-of-
|
|
15
|
+
truth step 2: a clean diff of two _nondeterministic_ captures is meaningless (they
|
|
16
|
+
might just happen to match). The capture now records its determinism basis in the
|
|
17
|
+
ledger — `self-checked` (captured twice, styles matched — a drift would have failed the
|
|
18
|
+
capture), `replayed` (rendered against a recorded HAR, deterministic by construction),
|
|
19
|
+
or `unproven` (neither) — and `styleproof-diff`:
|
|
20
|
+
- **blocks (exit 1) when either side is `unproven`**, even on an empty style diff;
|
|
21
|
+
- prints `✓ determinism proven — base X, head Y`, `✗ determinism NOT proven …`, or
|
|
22
|
+
`⚠ determinism basis unknown` (an older bundle with no field — degrades, never a
|
|
23
|
+
false red). The record-then-replay flow (base `self-checked`, head `replayed`) is
|
|
24
|
+
proven, as it should be.
|
|
25
|
+
|
|
10
26
|
## [3.9.0] - 2026-07-05
|
|
11
27
|
|
|
12
28
|
### Added
|
package/bin/styleproof-diff.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from '../dist/cli-errors.js';
|
|
42
42
|
import { loadStyleMap } from '../dist/capture.js';
|
|
43
43
|
import { auditRunInventory } from '../dist/inventory.js';
|
|
44
|
-
import { auditCoverage, COVERAGE_LEDGER } from '../dist/coverage.js';
|
|
44
|
+
import { auditCoverage, auditDeterminism, COVERAGE_LEDGER } from '../dist/coverage.js';
|
|
45
45
|
import { isMapFile } from '../dist/map-store.js';
|
|
46
46
|
|
|
47
47
|
const COMMAND = path.basename(process.argv[1] ?? 'styleproof-diff').replace(/\.mjs$/, '');
|
|
@@ -129,17 +129,14 @@ function capturedSurfaceKeys(dir) {
|
|
|
129
129
|
];
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function
|
|
133
|
-
let ledger = null;
|
|
132
|
+
function readLedger(dir) {
|
|
134
133
|
const p = path.join(dir, COVERAGE_LEDGER);
|
|
135
|
-
if (fs.existsSync(p))
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
134
|
+
if (!fs.existsSync(p)) return null;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
137
|
+
} catch {
|
|
138
|
+
return null; // a corrupt ledger reads as no registry → "not asserted", never a false green
|
|
141
139
|
}
|
|
142
|
-
return auditCoverage(capturedSurfaceKeys(dir), ledger);
|
|
143
140
|
}
|
|
144
141
|
|
|
145
142
|
// Print the completeness verdict; return true if it BLOCKS (a registered surface is missing).
|
|
@@ -165,6 +162,23 @@ function printCoverageVerdict(v) {
|
|
|
165
162
|
return true;
|
|
166
163
|
}
|
|
167
164
|
|
|
165
|
+
// Print the determinism verdict; return true if it BLOCKS (a side's capture was unproven).
|
|
166
|
+
function printDeterminismVerdict(v) {
|
|
167
|
+
if (v.status === 'proven') {
|
|
168
|
+
console.log(`\n✓ determinism proven — base ${v.base}, head ${v.head}`);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (v.status === 'unknown') {
|
|
172
|
+
console.log('\n⚠ determinism basis unknown — a capture predates the determinism ledger; recapture to certify it.');
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
console.log(
|
|
176
|
+
`\n✗ determinism NOT proven — base ${v.base}, head ${v.head}. An unproven capture can drift, so a clean\n` +
|
|
177
|
+
' diff might be two matching NONDETERMINISTIC reads. Enable selfCheck (default) or replay a recorded HAR.',
|
|
178
|
+
);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
const HELP = `${COMMAND} — certify a CSS refactor by diffing two computed-style map captures
|
|
169
183
|
|
|
170
184
|
usage: ${COMMAND} [baseRef] [options]
|
|
@@ -257,14 +271,17 @@ if (args.length <= 1) {
|
|
|
257
271
|
let result;
|
|
258
272
|
let inventoryAudit = null;
|
|
259
273
|
let coverageVerdict = null;
|
|
274
|
+
let determinismVerdict = null;
|
|
260
275
|
try {
|
|
261
276
|
assertCompatibleMapDirs(dirA, dirB);
|
|
262
277
|
result = diffStyleMapDirs(dirA, dirB);
|
|
263
|
-
// Read inventory +
|
|
264
|
-
// exist — the finally below deletes them in cached-map mode. Coverage is the
|
|
265
|
-
// bundle's completeness basis.
|
|
278
|
+
// Read inventory + the certification ledgers here, while the (possibly cached/restored)
|
|
279
|
+
// dirs still exist — the finally below deletes them in cached-map mode. Coverage is the
|
|
280
|
+
// HEAD bundle's completeness basis; determinism needs both sides.
|
|
266
281
|
inventoryAudit = readInventoryAudit(dirA, dirB);
|
|
267
|
-
|
|
282
|
+
const headLedger = readLedger(dirB);
|
|
283
|
+
coverageVerdict = auditCoverage(capturedSurfaceKeys(dirB), headLedger);
|
|
284
|
+
determinismVerdict = auditDeterminism(readLedger(dirA), headLedger);
|
|
268
285
|
} catch (e) {
|
|
269
286
|
console.error(e.message);
|
|
270
287
|
process.exit(2);
|
|
@@ -302,9 +319,13 @@ for (const sd of surfaces) {
|
|
|
302
319
|
|
|
303
320
|
const invRemovals = printInventoryAudit(inventoryAudit);
|
|
304
321
|
const coverageFails = printCoverageVerdict(coverageVerdict);
|
|
322
|
+
const determinismFails = printDeterminismVerdict(determinismVerdict);
|
|
305
323
|
|
|
306
324
|
if (jsonOut)
|
|
307
|
-
fs.writeFileSync(
|
|
325
|
+
fs.writeFileSync(
|
|
326
|
+
jsonOut,
|
|
327
|
+
JSON.stringify({ counts, surfaces, compared, coverage: coverageVerdict, determinism: determinismVerdict }, null, 2),
|
|
328
|
+
);
|
|
308
329
|
|
|
309
330
|
const total = counts.dom + counts.style + counts.state;
|
|
310
331
|
const newSurfaces = surfaces.filter((s) => s.missing).length;
|
|
@@ -313,14 +334,16 @@ const surfaceCount = surfaces.length;
|
|
|
313
334
|
const newNote = newSurfaces ? ` (+${newSurfaces} new surface(s) with no baseline)` : '';
|
|
314
335
|
const invNote = invRemovals ? ` + ${invRemovals} unacknowledged inventory removal(s)` : '';
|
|
315
336
|
const covNote = coverageFails ? ` + ${coverageVerdict.uncovered.length} uncaptured registered surface(s)` : '';
|
|
316
|
-
const
|
|
337
|
+
const detNote = determinismFails ? ' + determinism unproven' : '';
|
|
338
|
+
const clean = total === 0 && invRemovals === 0 && !coverageFails && !determinismFails;
|
|
317
339
|
console.log(
|
|
318
340
|
clean
|
|
319
341
|
? newSurfaces === 0
|
|
320
342
|
? `\n✓ 0 changed surfaces across ${compared} captured surface(s): every computed style, pseudo-element, and hover/focus/active state matches`
|
|
321
343
|
: `\nℹ ${newSurfaces} new surface(s) captured with no baseline to compare — review before baselining`
|
|
322
|
-
: `\n✗ ${counts.dom} DOM change(s), ${counts.style} computed-style difference(s), ${counts.state} state-delta difference(s) across ${surfaceCount} surfaces${newNote}${invNote}${covNote}`,
|
|
344
|
+
: `\n✗ ${counts.dom} DOM change(s), ${counts.style} computed-style difference(s), ${counts.state} state-delta difference(s) across ${surfaceCount} surfaces${newNote}${invNote}${covNote}${detNote}`,
|
|
323
345
|
);
|
|
324
|
-
// 0 = identical, 1 = reviewable differences (incl. unacknowledged inventory removals
|
|
325
|
-
//
|
|
326
|
-
|
|
346
|
+
// 0 = identical, 1 = reviewable differences (incl. unacknowledged inventory removals, an
|
|
347
|
+
// incomplete coverage registry, or an unproven-determinism capture), 3 = only new surfaces
|
|
348
|
+
// (no baseline). 2 = usage/capture error.
|
|
349
|
+
process.exit(total > 0 || invRemovals > 0 || coverageFails || determinismFails ? 1 : newSurfaces > 0 ? 3 : 0);
|
package/dist/coverage.d.ts
CHANGED
|
@@ -36,13 +36,32 @@ export type CoverageGaps = {
|
|
|
36
36
|
export declare function coverageGaps(capturedKeys: Iterable<string>, expected: Iterable<string>, exclude?: Record<string, string>): CoverageGaps;
|
|
37
37
|
/** Bundled next to the maps, so the completeness basis travels with the capture. */
|
|
38
38
|
export declare const COVERAGE_LEDGER = "styleproof-coverage.json";
|
|
39
|
+
/**
|
|
40
|
+
* How a capture's determinism was established — the second half of a trustworthy green.
|
|
41
|
+
* `self-checked`: captured twice and the computed styles matched (a drift would have
|
|
42
|
+
* failed the capture). `replayed`: rendered against a recorded HAR, so deterministic by
|
|
43
|
+
* construction. `unproven`: neither — the styles could have drifted and no one checked.
|
|
44
|
+
*/
|
|
45
|
+
export type DeterminismBasis = 'self-checked' | 'replayed' | 'unproven';
|
|
39
46
|
export type CoverageLedger = {
|
|
40
47
|
version: 1;
|
|
41
48
|
/** The declared surface registry, or null when the spec asserted none. */
|
|
42
49
|
expected: string[] | null;
|
|
43
50
|
/** Reviewed opt-outs (`key → reason`). */
|
|
44
51
|
exclude: Record<string, string>;
|
|
52
|
+
/** How this capture's determinism was established (3.10.0). Absent on older bundles. */
|
|
53
|
+
determinism?: DeterminismBasis;
|
|
54
|
+
};
|
|
55
|
+
export type DeterminismVerdict = {
|
|
56
|
+
/** `proven` — both sides self-checked or replayed; `unproven` — a side was neither, so
|
|
57
|
+
* a clean diff might just be two matching NONDETERMINISTIC captures; `unknown` — an
|
|
58
|
+
* older bundle with no determinism field (degrade, don't block). */
|
|
59
|
+
status: 'proven' | 'unproven' | 'unknown';
|
|
60
|
+
base: DeterminismBasis | 'unknown';
|
|
61
|
+
head: DeterminismBasis | 'unknown';
|
|
45
62
|
};
|
|
63
|
+
/** The gate's determinism call: a green needs BOTH sides proven (self-checked or replayed). */
|
|
64
|
+
export declare function auditDeterminism(base: CoverageLedger | null, head: CoverageLedger | null): DeterminismVerdict;
|
|
46
65
|
export type CoverageVerdict = {
|
|
47
66
|
/** `complete` — every registered surface captured; `incomplete` — a registered
|
|
48
67
|
* surface is missing (gates); `unasserted` — no registry, so a green can only
|
package/dist/coverage.js
CHANGED
|
@@ -44,6 +44,17 @@ export function coverageGaps(capturedKeys, expected, exclude = {}) {
|
|
|
44
44
|
// the suite guard — which checks the declared list — cannot see it).
|
|
45
45
|
/** Bundled next to the maps, so the completeness basis travels with the capture. */
|
|
46
46
|
export const COVERAGE_LEDGER = 'styleproof-coverage.json';
|
|
47
|
+
/** The gate's determinism call: a green needs BOTH sides proven (self-checked or replayed). */
|
|
48
|
+
export function auditDeterminism(base, head) {
|
|
49
|
+
const b = base?.determinism ?? 'unknown';
|
|
50
|
+
const h = head?.determinism ?? 'unknown';
|
|
51
|
+
const proven = (d) => d === 'self-checked' || d === 'replayed';
|
|
52
|
+
if (b === 'unproven' || h === 'unproven')
|
|
53
|
+
return { status: 'unproven', base: b, head: h };
|
|
54
|
+
if (proven(b) && proven(h))
|
|
55
|
+
return { status: 'proven', base: b, head: h };
|
|
56
|
+
return { status: 'unknown', base: b, head: h };
|
|
57
|
+
}
|
|
47
58
|
/** The gate's completeness call: audit what was actually captured against the ledger. */
|
|
48
59
|
export function auditCoverage(capturedKeys, ledger) {
|
|
49
60
|
if (!ledger || ledger.expected == null) {
|
package/dist/runner.js
CHANGED
|
@@ -458,11 +458,18 @@ function resolveSettings(c) {
|
|
|
458
458
|
* `expected: null` records that the spec declared no registry, so a green can only
|
|
459
459
|
* certify the captured surfaces. Runs on a capture run (dir set) only.
|
|
460
460
|
*/
|
|
461
|
-
function writeCoverageLedgerTest(
|
|
461
|
+
function writeCoverageLedgerTest(settings, dir, expected, exclude) {
|
|
462
462
|
test('styleproof coverage ledger', () => {
|
|
463
|
-
const outDir = path.join(baseDir, dir);
|
|
463
|
+
const outDir = path.join(settings.baseDir, dir);
|
|
464
464
|
fs.mkdirSync(outDir, { recursive: true });
|
|
465
|
-
|
|
465
|
+
// Determinism basis: self-check ON proves it (a drift would have failed the capture);
|
|
466
|
+
// else a replay run is deterministic by construction; else it's unproven.
|
|
467
|
+
const determinism = settings.selfCheck
|
|
468
|
+
? 'self-checked'
|
|
469
|
+
: settings.replayFrom
|
|
470
|
+
? 'replayed'
|
|
471
|
+
: 'unproven';
|
|
472
|
+
const ledger = { version: 1, expected, exclude, determinism };
|
|
466
473
|
fs.writeFileSync(path.join(outDir, COVERAGE_LEDGER), JSON.stringify(ledger, null, 2));
|
|
467
474
|
});
|
|
468
475
|
}
|
|
@@ -490,7 +497,7 @@ export function defineStyleMapCapture(options) {
|
|
|
490
497
|
test.describe('styleproof capture', () => {
|
|
491
498
|
test.skip(!dir, 'set STYLEMAP_DIR=<label> to capture computed-style maps');
|
|
492
499
|
if (dir)
|
|
493
|
-
writeCoverageLedgerTest(settings
|
|
500
|
+
writeCoverageLedgerTest(settings, dir, expected ?? null, exclude);
|
|
494
501
|
for (const surface of captureSurfaces) {
|
|
495
502
|
if (surface.widths && surface.widths.length > 0) {
|
|
496
503
|
// Explicit widths: one parallelizable test per surface × width.
|
|
@@ -545,7 +552,7 @@ export function defineCrawlCapture(options) {
|
|
|
545
552
|
// so it records `expected: null` (completeness honestly "not asserted": the crawl
|
|
546
553
|
// captures what the nav links to, and can't prove that's every route in the app).
|
|
547
554
|
if (dir)
|
|
548
|
-
writeCoverageLedgerTest(settings
|
|
555
|
+
writeCoverageLedgerTest(settings, dir, null, {});
|
|
549
556
|
test('discover surfaces by crawling links, then capture each', async ({ page }) => {
|
|
550
557
|
// 1. Load the root and wait for its nav links to hydrate — an SPA renders them
|
|
551
558
|
// client-side, so they aren't in the initial HTML.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "styleproof",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Catch every CSS change before it ships — review PRs and certify refactors by the browser's computed styles, not pixels. Works with any styling system.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|