Skip to content

rules: compare by meaning, not declaration order or media range notation - #157

Merged
tmashara merged 2 commits into
mainfrom
claude/issue-129
Nov 5, 2026
Merged

tmashara merged 2 commits into
mainfrom
claude/issue-129

Conversation

@tmashara

@tmashara tmashara commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Refs #129

The rules: block listed a rule as changed on both sides when a formatter only reordered its declarations or rewrote a breakpoint. ruleKey now ignores both:

  • Declaration order: cssText's declarations are sorted before comparing.
  • Media ranges: (min-width: x) reads as (width >= x) and (max-width: x) as (width <= x), in @media only. In @supports the same text is a property test.

Three cases still count as changes:

  • Shorthand and longhand swapping order, var() included. Chrome's cssText holds one value per longhand, so the two orders serialize differently.
  • Pairs whose order decides. margin-left and margin-inline-start, or -webkit-border-image and border-image, set the same thing and the later wins, so sorting would hide a real change. A rule with a logical or -webkit- property keeps its order, which means reordering one still shows as a change even where it does nothing.
  • Values with ; inside. The split skips strings and brackets, so a data URI's ; ends no declaration.

Not read as equal: the reversed (60em <= width) and the two-sided (60em <= width <= 80em). They still show as changes, as they do today.

Known gap, unchanged by this PR: margin: var(--x); margin-top: 4px and margin: var(--y); margin-top: 4px serialize identically in Chrome, so that change isn't seen.

Testing

The /rules fixture proves this end to end through the CLI, in tests/visual/guardrails.test.ts. A reorder, a data URI reorder and both range rewrites give no entry. A changed value and the swap, var() swap, logical swap and ;-in-string rules all do. That test fails if the split is naive, if sorting ignores logical properties, or if either normalisation is removed.

The fixture also gives .toc a breakpoint whose value really changes, so the "matched on some shots" output is still covered.

🤖 Generated with Claude Code

tmashara and others added 2 commits November 5, 2026 17:30
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 <noreply@anthropic.com>
-webkit-border-image and border-image set the same longhands with
different defaults, so their swap changes the computed value yet sorted
to one key, and corner-top-left-shape and corner-start-start-shape are
another logical pair. A rule with either keeps its order, as logical
properties do.

splitTop takes the separator and replaces declarations. The gradient
path now respects quotes, which only changes a bracket inside a quoted
url, which it miscounted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmashara
tmashara merged commit c953047 into main Nov 5, 2026
3 checks passed
@tmashara tmashara mentioned this pull request Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant