From 1d592b0ebf13d8ebda53e85ddfdf8ba61c3cd8ca Mon Sep 17 00:00:00 2001 From: Timur Mashara <62305878+tmashara@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:30:26 +0100 Subject: [PATCH 1/2] rules: compare by meaning, not declaration order or media range notation A stylelint pass reorders declarations and rewrites min-width as width >=, and each such rule showed as changed on both sides. ruleKey now sorts cssText's declarations and reads (min-width: x) as (width >= x), in @media only, since in @supports that is a property. cssText holds one value per longhand, so a shorthand and longhand that swap still differ, var() included. A logical property and its physical twin are two longhands whose order decides, so a rule with one keeps its order. The split skips strings and brackets: a data URI's `;` ends no declaration. Co-Authored-By: Claude Opus 5.5 --- docs/output.md | 18 ++++++---- skills/renderdiff/reference.md | 6 ++-- src/computed.ts | 48 +++++++++++++++++++++++++-- tests/fixtures/pages/rules/left.html | 16 ++++++--- tests/fixtures/pages/rules/right.html | 13 ++++++-- tests/visual/guardrails.test.ts | 26 ++++++++++----- 6 files changed, 100 insertions(+), 27 deletions(-) diff --git a/docs/output.md b/docs/output.md index 71b0ccd..c534579 100644 --- a/docs/output.md +++ b/docs/output.md @@ -289,15 +289,21 @@ style rule in each side's stylesheets and lists the groups of rules only one sid ``` rules: 3 changed groups, 1 never matched, 2 matched on some shots +chat.css (1 rule) matched nothing: /help@1440,375 - +docs.css @media (width >= 60em) (5 rules) matched /a@960,1440 /b@960; nothing /a@959,375 + +docs.css @media (min-width: 60em) (5 rules) matched /a@960,1440 /b@960; nothing /a@959,375 -docs.css @media (min-width: 60em) (5 rules) matched /a@960,1440 /b@960; nothing /a@959,375 ``` - A rule is its enclosing at-rules, selector (nesting resolved) and declarations, as Chrome - serializes them. So pure notation changes Chrome normalizes (`rgb(0 0 0)` and - `rgb(0, 0, 0)`) are no change, while `min-width:` against `width >=` is. Asset hashes in - `url()` and the left origin are read as on the other side. A rule moved to another file is - no change. Only style rules count: not `@font-face`, `@keyframes` or `@property`. + serializes them, compared by meaning. So notation Chrome normalizes (`rgb(0 0 0)` and + `rgb(0, 0, 0)`, hex case, `0` and `0px`, merged shorthands) is no change, and neither is + declaration order or `@media (min-width: 60em)` against `(width >= 60em)`, or `max-width:` + against `<=`. A shorthand and longhand that swap order are a change, since that changes + what the rule does. A rule with a logical property (`margin-inline-start`) keeps its order, + so reordering one is a change even where it does nothing. The reversed + `(60em <= width)` and the two-sided `(60em <= width <= 80em)` aren't read as `min-width:`. + Asset hashes in `url()` and the left origin are read as on the other side. A rule moved to + another file is no change. Only style rules count: not `@font-face`, `@keyframes` or + `@property`. - `+` rules only the left (your change) has, `-` only the right. A group is a stylesheet's file name (asset hash as `#`; a `

Rule coverage guard rail

-

The two sides write one breakpoint in different notation and render the same, so only rule coverage tells the rewritten rule apart, and whether any shot reached it.

+

The two sides write their breakpoints in different notation and render the same, so only rule coverage tells a rule that really changed from one rewritten, and whether any shot reached it.