stylelint-plugin-rhythmguard 3.11.0 → 3.13.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 +96 -0
- package/README.md +5 -1
- package/package.json +23 -1
- package/src/audit/args.js +23 -4
- package/src/audit/baseline.js +198 -28
- package/src/audit/codemod.js +76 -11
- package/src/audit/config.js +10 -30
- package/src/audit/contract.js +99 -10
- package/src/audit/decisions.js +45 -16
- package/src/audit/render-github.js +4 -0
- package/src/audit/render-html.js +7 -0
- package/src/audit/render-markdown.js +48 -20
- package/src/audit/render-text.js +24 -1
- package/src/audit/render-utils.js +28 -1
- package/src/audit/report.js +72 -13
- package/src/audit/scan/files.js +265 -44
- package/src/audit/scan/stylesheets.js +67 -34
- package/src/audit/scan/templates.js +22 -7
- package/src/audit/token-chains.js +22 -8
- package/src/audit/token-ladders.js +23 -2
- package/src/cli/doctor.js +76 -10
- package/src/cli/fix.js +20 -22
- package/src/cli/index.js +2 -2
- package/src/cli/init.js +176 -27
- package/src/cli/quickstart.js +79 -15
- package/src/configs/embed.js +2 -0
- package/src/configs/motion.js +1 -0
- package/src/configs/recommended.js +1 -0
- package/src/configs/strict.js +1 -0
- package/src/configs/tailwind.js +1 -0
- package/src/core/decisions.js +2 -1
- package/src/core/fs-cache.js +6 -0
- package/src/core/json.js +25 -0
- package/src/core/length.js +11 -0
- package/src/core/options.js +46 -21
- package/src/core/scale-inference.js +171 -85
- package/src/core/tailwind-class-analysis.js +25 -14
- package/src/core/tailwind-motion-analysis.js +12 -3
- package/src/core/token-index.js +5 -2
- package/src/core/token-map.js +2 -1
- package/src/core/token-source-entries.js +64 -0
- package/src/core/token-sources.js +363 -70
- package/src/core/value-nodes.js +280 -54
- package/src/eslint/index.js +14 -8
- package/src/eslint/literal-spans.js +94 -9
- package/src/eslint/rules/tailwind-class-use-motion-scale.js +1 -0
- package/src/eslint/rules/tailwind-class-use-scale.js +40 -11
- package/src/rules/no-offscale-transform/index.js +69 -44
- package/src/rules/prefer-token/index.js +61 -55
- package/src/rules/use-motion-scale/index.js +82 -6
- package/src/rules/use-scale/index.js +99 -112
- package/src/rules/validate.js +13 -4
- package/types/__checks__/consumer.ts +20 -1
- package/types/__checks__/eslint-config.ts +45 -0
- package/types/__checks__/shipped-configs.ts +96 -0
- package/types/audit.d.ts +108 -15
- package/types/index.d.ts +15 -0
- package/types/shared.d.ts +147 -23
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,102 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.13.0] - 2026-10-18
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `checkMathBounds` checks bounds inside translations. `use-scale`, `prefer-token` and `no-offscale-transform` skipped it for `transform`, so `transform: translateX(clamp(13px, 2vw, 17px))` gave no finding while `padding: clamp(13px, 2vw, 17px)` reported both bounds; `no-offscale-transform` accepted the option but ignored it everywhere, the `translate` properties included. The `clamp()` minimum and maximum and the length arguments of `min()` and `max()` are now checked, named and fixed in translate functions and in `translate`, `translate-x`, `translate-y` and `translate-z` as anywhere else. Only configs that set `checkMathBounds: true` see new reports. ([#272](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/272))
|
|
14
|
+
- Option shapes that turned a rule into a silent no-op are invalid options now. `scale: ["auto"]`, an empty `scale` or `customScale`, an empty list or an unknown preset in `scales` or `propertyScales` (`{ spacing: [] }`, `{ padding: { preset: "nope" } }`), and `use-motion-scale`'s `durationScale: 100`, `durationScale: []` or `durationUnits: "s"` were accepted and then checked nothing or fell back to the defaults; each is now an invalid option warning, which fails the Stylelint run as a misspelled option does. `"auto"` stays valid as the whole `scale`. A preset name as a `propertyScales` value (`{ padding: "rhythmic-8" }`), typed `scaleOrPreset` in the docs, was rejected and is accepted now. `audit.scales` in `.rhythmguardrc.json` uses the same check, so an empty list or unknown preset there is refused with `scales must map ...`. ([#273](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/273))
|
|
15
|
+
- `use-scale` and `no-offscale-transform` say why a negative value or a percentage is reported. With `allowNegative: false` or `allowPercentages: false` the message read `Use scale values (nearest: n/a).`, although `-8px` is on the scale. It now reads `Negative values are disabled for this rule.` or `Percentages are disabled for this rule.`, as `tailwind-class-use-scale` already does; see Messages. What is reported is unchanged. ([#274](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/274))
|
|
16
|
+
- `prefer-token` finds a px `tokenMap` key for a converted length. The key was built from the raw conversion, so `padding: 1.1rem` at `baseFontSize: 14` looked up `15.400000000000002px`, missed `"15.4px": "var(--space-x)"` and read `No known token holds this value`. The key is now formatted like every other length, so the value reads `Use var(--space-x).` and is fixed to it. This changes the message and adds a fix for such values. ([#275](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/275))
|
|
17
|
+
- The `use-motion-scale` docs print the rule's messages. They showed `Use motion scale values` and `Use an easing token.`, while the rule reports `Use duration scale values (nearest: 150ms or 200ms).` and `Use motion tokens for easing decisions.`; the docs now match, and a test lints the documented example against them. The messages are unchanged. ([#276](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/276))
|
|
18
|
+
- A file saved with a UTF-8 byte order mark is read like one without. A JSON token source with a BOM failed with `Unable to parse token source ...: Unexpected token`, a `.rhythmguardrc.json` with one was ignored by the lint (`.rhythmguardrc.json could not be read`) and stopped the audit with `Invalid Rhythmguard config`, and a baseline with one stopped `--since-baseline`. A leading BOM is now stripped before parsing token sources (JSON and CSS), `.rhythmguardrc.json` in the lint, the audit and `rhythmguard doctor`, `tokenMapFile`, and baselines. ([#291](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/291))
|
|
19
|
+
- A bare `--space` is a spacing base only when the other spacing tokens do not form a scale on their own. `:root { --space: 1rem; --space-s: .5rem; --space-m: 1rem; --space-l: 2rem; --space-xl: 3rem }` with `scale: "auto"` gave multiples of 16px up to 1536px, so 40px and 56px passed while 12px was reported against 16px steps; that ladder is now the scale and `--space` one step on it. A project that declares `--space: 0.25rem` and writes each step as `calc(var(--space) * N)`, as Discourse does, keeps the multiples, since its steps are not plain lengths. `--spacing` (Tailwind v4) is always a base. ([#293](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/293))
|
|
20
|
+
- Audit token ladders compare a ladder of negative steps by magnitude. `--space-negative-1: -4px` to `--space-negative-3: -16px` was reported as two `inverted` problems; a ladder whose every step is negative is now compared by magnitude, and a ladder that crosses zero keeps its signed order. ([#294](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/294))
|
|
21
|
+
- Audit token chains no longer count shadow tokens as spacing tokens. `inset` in Tailwind v4's `--inset-shadow-*` made them spacing-named, so a Tailwind `theme.css` added three `non-length` chains and read "1 of 4 spacing tokens resolve". Names containing `shadow` are now left out of the chains. ([#295](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/295))
|
|
22
|
+
- An explicit `tokenPattern` now selects the tokens `scale: "auto"` reads from token files, as it already did for `fixWith: "token"`. With `{ scale: "auto", tokenPattern: "^--size-", scaleSources: ["sizes.css"] }` and `--size-1` to `--size-4` in that file, inference found nothing and fell back to `rhythmic-4`, while fixes wrote the `--size-*` tokens. The pattern is now applied to `scaleSources`, `.rhythmguardrc.json` token sources and installed token packages on top of the spacing names, unless a source sets its own `tokenPattern`, so inference and fixes read the same tokens. This changes the inferred scale for configs that set both `scale: "auto"` and `tokenPattern` when a token file has tokens the pattern matches but the spacing names do not. ([#296](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/296))
|
|
23
|
+
- The docs list the `scale: "auto"` sources in the order the code reads them: `tailwindConfigPath` comes before installed token packages. `docs/rules/use-scale.md` and `docs/FOR_CONFIG_AUTHORS.md` had them the other way round. The order itself is unchanged. ([#297](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/297))
|
|
24
|
+
- `tailwind-class-use-scale` keeps the sign of a double negative. In `-m-[-13px]` the two signs cancel and Tailwind writes `margin: 13px`, but the rule treated either sign as making the value negative, so the fix wrote `-m-3` (-12px) and the message named `-m-3` or `-m-4`. The value is now negative only when exactly one of the two signs is there, so `-m-[-13px]` becomes `m-3`; with `spacingUnit: false` it still becomes `-m-[-12px]`, and with `allowNegative: false` a double negative is checked like any positive value instead of being reported as negative. The audit's class-string findings change the same way. ([#277](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/277))
|
|
25
|
+
- `eslint --fix-type` applies the Rhythmguard fixes. Neither ESLint rule declared `meta.type`, and ESLint skips the fixes of a rule without one whenever `--fix-type` is given, so `--fix-type problem,suggestion,layout` fixed nothing. Both rules are now `type: "suggestion"`. ([#278](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/278))
|
|
26
|
+
- The ESLint `configs.recommended` works as a flat config of its own. It had only `rules`, so `export default [rhythmguard.configs.recommended]` failed with `could not find plugin "rhythmguard-tailwind"`. It now also has `plugins: { "rhythmguard-tailwind": plugin }`; spreading `configs.recommended.rules` into a block that registers the plugin works as before. ([#279](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/279))
|
|
27
|
+
- The Stylelint option types accept the package's own configs. `RhythmguardRuleOptions` lacked most options, so typing the shipped `embed` config failed on `severity` and `motion` on `durationScale`. The types now have every option each rule accepts, as `RhythmguardUseScaleOptions`, `RhythmguardPreferTokenOptions`, `RhythmguardTransformRuleOptions` and `RhythmguardMotionRuleOptions` (with `RhythmguardRuleOptions` covering all three length rules), Stylelint's own `severity`, `message`, `disableFix`, `reportDisables` and `url`, and the `[true]` form. `rules` in `RhythmguardStylelintConfig` types each Rhythmguard rule with its own options. `includeMathFunctions`, which no rule accepts, is gone from the types; use `enforceInsideMathFunctions`. Every shipped config now declares its type and is checked by `npm run typecheck`. ([#280](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/280))
|
|
28
|
+
- Smaller type and playground mismatches. `getScalePreset` and `getCommunityScaleMetadata` are typed as returning `null` for an unknown name, which is what they return. The subpath types (`stylelint-plugin-rhythmguard/eslint`, `/audit`, `/presets`, `/configs/*`, `/rules/*`, `/cli`) resolve under `moduleResolution: "node"` through `typesVersions`, and `eslint-plugin-rhythmguard`'s types import the package root, so they no longer fail with `TS2307`. The playground's `tailwind` config includes the `strict` config it extends, so it runs `use-scale` and `no-offscale-transform` as Stylelint does, not only `prefer-token`. ([#281](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/281))
|
|
29
|
+
- The audit API types match the report. `AuditSummary` declared `cleanliness`, `filesWithFindings` and `findingCount`, `AuditFinding` declared `message`, `AuditBaselineComparison` declared `resolvedFindings`, and `config` was typed as a string; the report has `scaleCleanliness`, `filesWithIssues`, `totalFindings`, `text`, `resolvedFindingsCount` and `config: { file }`. The types now describe the report as `createAuditReport` returns it, `AuditOptions` marks `format`, `outputPath`, `maxFindings`, `minCleanliness`, `failOnNewDrift` and `badgeMetric` as CLI only (`createAuditReport` never read them), and `loadAuditConfig` has its real return type. `createAuditReport({ scale: undefined })` used the undefined over the default scale and inferred one from the scanned CSS; a key set to `undefined` now counts as omitted, so the default or the config file value applies. ([#284](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/284))
|
|
30
|
+
- Numeric audit flags refuse empty, whitespace and non-decimal values. `--min-cleanliness=` (an unset `$THRESHOLD` in CI) gated at 0% and passed, `--max-findings=` became 0, and `--max-findings 0x10` became 16, because each value went through `Number()`. `--max-findings`, `--min-cleanliness`, `--token-candidate-min-count` and `--base-font-size` (also on `rhythmguard fix`) now take plain decimal digits only, with a fraction where one makes sense, and exit 1 with their existing message otherwise. In `.rhythmguardrc.json` a JSON number or a decimal string is accepted as before; `null` and `true`, which read as 0 and 1, are refused. ([#282](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/282))
|
|
31
|
+
- `audit.scales` reaches `--plan` and the reported scale. With `{"scales":{"spacing":[0,5,10],"typography":[12,14,16]}}`, `--plan` proposed `margin: 8px` with `"nearest": ["8px","8px"]` and `font-size: 15px` with `["12px","16px"]`, taken from the project scale, and `contracts.scale.values` still read `0, 4, 8, ..., 32`. A `spacing` list or preset is now the audit's spacing scale (`source: "explicit"`), as it already was for `use-scale`; `--plan` takes each value's nearest steps from the scale it was checked against, gives a value found in groups with different scales one entry per scale, and leaves `nearest` empty for a group set to `"auto"`. Token opportunities, Tailwind class strings and token chains now read the `spacing` entry too, so this changes findings for configs that set `audit.scales.spacing`. ([#283](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/283))
|
|
32
|
+
- Markdown reports keep their tables and code spans intact. A scale source file named `a|b.css` split the `Scale source` row, and a file named ``we`ird.css`` broke out of its code span, since a backslash escapes nothing inside a code span. The scale source row is escaped now, and code spans use a fence one backtick longer than any run in the value; token names, chain terminals, ladder steps and the directory line get the same treatment. ([#285](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/285))
|
|
33
|
+
- `init --help`, `doctor --help` and `quickstart --help` print their usage. `echo y | rhythmguard init --help` wrote `.stylelintrc.json`, doctor ran its checks, quickstart audited, and a typo such as `init --agnets claude` fell through to the interactive init. Every command now handles `--help` and `-h` and refuses an option it does not know before doing anything; `init --agents` with no value still means `all`. ([#286](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/286))
|
|
34
|
+
- The audit commands quickstart suggests reproduce its result. Quickstart audits with `--scale auto` and the token files it finds, but suggested `npx rhythmguard audit . --format markdown`, which used the default scale: with a `tokens.json` scale of `0, 4, 8, 12, 20`, quickstart reported `16px` and the suggested audit and baseline reported `20px`. The suggested commands now carry the same flags, for example `npx rhythmguard audit . --scale auto --token-source ./tokens.json --format markdown`. ([#287](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/287))
|
|
35
|
+
- `rhythmguard fix` checks its target and reports where each literal is. `fix ./nope` and `fix ./a.css` ended in a raw `binding.readdir` stack trace; they now exit 1 with the audit's `Directory not found: ./nope` and `Not a directory: ./a.css`. A literal in a value that spans lines was listed at the declaration's line with a column past its end (`2:24` for a `10px` on line 4); positions are now counted through the value. ([#288](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/288))
|
|
36
|
+
- The audit follows symbolic links. A linked directory or stylesheet was skipped without a word (`cssFiles: 1` and no findings for `src/linked -> ../real` and `src/l.css -> ../real/r.css`), in a full run and with `--staged`. Links that resolve inside the audited directory are now followed, a link back up the tree is not walked twice, and a file reached by more than one path is scanned once, under its own path when the walk reaches it directly. Security: a link that resolves outside the directory (`../../other-project`, `/etc`) is never followed, in a full run, with `--staged` and with `--since`, and is counted as `Links not followed` in text and Markdown output (`scanned.linksOutsideRoot` in the report); `rhythmguard fix` walks the same way and never writes through a symbolic link, so `fix --write` cannot rewrite a file outside the directory it was given. This adds findings for projects that link stylesheets into the audited directory. ([#289](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/289))
|
|
37
|
+
- A corrupt baseline names its file. `--since-baseline` with an invalid `.rhythmguard-baseline.json` failed with `Expected property name or '}' in JSON at position 1` alone; the error now reads `Invalid baseline file .rhythmguard-baseline.json: <the JSON error>`, also when a `--staged` or `--since` run merges into it. ([#290](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/290))
|
|
38
|
+
|
|
39
|
+
### Messages
|
|
40
|
+
|
|
41
|
+
- `use-scale`: with `allowNegative: false` a negative value reads `Unexpected value "-8px". Negative values are disabled for this rule.`, and with `allowPercentages: false` a percentage reads `Unexpected value "5%". Percentages are disabled for this rule.`, where both read `Unexpected off-scale value "...". Use scale values (nearest: n/a).` A `clamp()`, `min()` or `max()` bound names its position: `Unexpected clamp() minimum "-8px". Negative values are disabled for this rule.` ([#274](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/274))
|
|
42
|
+
- `no-offscale-transform`: the same cases read `Unexpected transform translation value "-8px". Negative values are disabled for this rule.` and `Unexpected transform translation value "13%". Percentages are disabled for this rule.` instead of `Use scale values (nearest: n/a).` A bound checked by `checkMathBounds` names its position, as in `use-scale`: `Unexpected transform translation clamp() minimum "13px". Use scale values (nearest: 12px or 16px).`, with the px value after a rem or em bound. ([#274](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/274), [#272](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/272))
|
|
43
|
+
- Audit: a baseline that is not valid JSON reads `Invalid baseline file <path>: <the JSON error>`. ([#290](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/290))
|
|
44
|
+
- `rhythmguard init`, `doctor` and `quickstart`: `--help` prints `Usage: rhythmguard init [options]`, `Usage: rhythmguard doctor` or `Usage: rhythmguard [quickstart]` with the options; an unknown option reads `Unknown option: <option>` followed by the usage, and `init --agents=` reads `Missing value for --agents.` ([#286](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/286))
|
|
45
|
+
- `rhythmguard fix`: a missing target reads `Directory not found: <path>` and a file `Not a directory: <path>`, as in the audit. ([#288](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/288))
|
|
46
|
+
- Audit: text output adds `Links not followed <n> (they point outside the scanned directory)` and Markdown `| Symbolic links not followed (they point outside the scanned directory) | <n> |` when a symbolic link resolves outside the scanned directory. `rhythmguard fix` lists `skipped <file>: symbolic link to a path outside the directory; not followed` and `skipped <file>: reached through a symbolic link; rewrite the file it points to instead`. ([#289](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/289))
|
|
47
|
+
- Quickstart: the `Next` section puts each note on its own line above its command (`PR-ready report:` then `npx rhythmguard audit . --scale auto ... --format markdown`), and the baseline note reads `Then gate only new drift in CI, with the same flags:`. ([#287](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/287))
|
|
48
|
+
|
|
49
|
+
## [3.12.0] - 2026-10-18
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- `use-motion-scale` no longer snaps times inside `calc()`, `clamp()`, `min()` and `max()`. They are terms of an expression, so `transition-delay: calc(var(--i) * 30ms)` was reported and fixed to `calc(var(--i) * 0ms)`, removing the stagger, and `calc(var(--index) * 50ms + 100ms)` became `* 75ms`. They are now skipped, as `use-scale` skips them by default; times outside the math function in the same value are still checked. This removes reports for times inside math functions. ([#232](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/232))
|
|
54
|
+
- Negative delays are no longer reported as invalid. `use-motion-scale` rejected every negative time with `Unexpected negative motion duration "-100ms".`, including `transition-delay: -100ms` and the `-0.5s` delay in `animation: spin 1s -0.5s infinite`, which are valid CSS that start the motion partway through; `tailwind-class-use-motion-scale` did the same for `delay-[-100ms]`. A negative is now rejected only for a duration: `*-duration`, `duration-[...]`, and the first time of each layer in a `transition` or `animation` shorthand (not when a function such as `var()` comes before it, since that may be the duration). A negative delay is checked against the scale by its size and a fix keeps the sign, so `-130ms` is reported and becomes `-150ms`, while `-100ms` passes. ([#236](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/236))
|
|
55
|
+
- `tailwind-class-use-motion-scale` no longer flags token easings. Every `ease-[...]` was reported, including `ease-[var(--ease-out)]` and `ease-[--my-ease]`, the tokens its message asks for. Only raw curves (`cubic-bezier(`, `linear(`, `steps(`) are reported now, as in `use-motion-scale`; the same applies to `rhythmguard audit --include-motion`. This removes reports for `ease-[...]` values that are not raw curves. ([#245](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/245))
|
|
56
|
+
- `no-offscale-transform` accepts its documented `tokenPattern` and `tokenFunctions` options. Both were missing from its option schema, so `{ tokenPattern: "^--gap-" }` failed with `Invalid option name "tokenPattern"` and turned the rule off, and `fixWith: "token"` could only write `--space-*` tokens. Both are accepted now; `tokenPattern` picks the tokens `fixWith: "token"` writes, an invalid pattern is reported on the root instead of throwing, and a translation tokenized through one of `tokenFunctions` is skipped as in `use-scale`. With the defaults this skips the fallback of `var(--space-*, ...)`, so `translateX(var(--space-2, 13px))` is no longer reported. ([#237](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/237))
|
|
57
|
+
- `no-offscale-transform` reports what `allowNegative: false` and `allowPercentages: false` forbid. Both branches returned without a report, so `allowNegative: false` made `translateX(-8px)` pass instead of failing, and `translateX(13%)` passed with `allowPercentages: false`. They are now reported without a fix, as in `use-scale`: `Unexpected transform translation value "-8px". Use scale values (nearest: n/a).` Only configs that set either option to `false` see new reports. ([#238](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/238))
|
|
58
|
+
- The audit finds Tailwind classes in string literals nested inside another literal. After a literal closed, the scanner continued after it, so ``className={`flex ${on ? 'p-[13px]' : 'm-[15px]'}`}``, Vue `:class="{ 'p-[13px]': active }"` and `:class="[active ? 'px-[13px]' : '']"` gave no findings and Svelte `class="m-[15px] {on ? 'gap-[13px]' : ''}"` found only `m-[15px]`, while the ESLint rule reported them. Each literal's value is now scanned again for literals in the other quote kinds, which still reads every character at most three times. This changes reports: projects with such classes get new findings, and a baseline written by 3.11.0 counts them as new drift. ([#254](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/254))
|
|
59
|
+
- `rhythmguard doctor` agrees with the audit on `.rhythmguardrc.json`. It failed a decisions-only file with `audit config missing`, which the audit reads fine, and passed files the audit rejects, such as `{"audit":{"minCleanliness":150}}` or a decision with value `abc`. It now validates the `audit` section with the audit's own loader (which makes the section optional and checks every value, token source and the ignore file) and the `decisions` section with the same code the audit and the rules use, and reports each section's error as a failure. ([#255](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/255))
|
|
60
|
+
- `rhythmguard fix --decided` applies every snap decision once, to the original values. Each decision ran over the files the previous one had rewritten, so decisions `10px -> 14px` and `14px -> 16px` turned `padding: 10px` into `16px` on `--write`, while the dry run, reading the files unchanged, showed `14px`. All snaps now run in one pass: a value one snap writes is never snapped again, and the dry run lists exactly what `--write` writes. ([#256](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/256))
|
|
61
|
+
- `rhythmguard init` writes the config Stylelint loads. It always wrote `.stylelintrc.json`, which Stylelint never reads when `package.json` has a `stylelint` key or a `.stylelintrc` exists, so init reported `Wrote .stylelintrc.json` and nothing changed. Init now finds the config with Stylelint's search order (cosmiconfig's: `package.json`, `.stylelintrc`, `.stylelintrc.{json,yaml,yml,js,ts,cjs,mjs}`, `.config/stylelintrc*`, `stylelint.config.{js,ts,cjs,mjs}`). A JSON `.stylelintrc` or `.stylelintrc.json` is rewritten in place after the same prompts; for a `package.json` key, a YAML file or a JS or TS config, init writes nothing and prints the lines to add, in that file's syntax. ([#257](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/257))
|
|
62
|
+
- The token source cache no longer grows with every save. Parsed token sources were cached under a key that included each file's mtime and never evicted, so an editor or watch mode that re-linted after each edit kept every old parse: a 20k-token file edited 60 times took the heap from 4MB to 1152MB. The cache is now keyed by the source paths, keeps the mtimes in the entry and replaces it when a file changes; the same 60 edits stay at about 23MB. ([#264](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/264))
|
|
63
|
+
- The `scale: "auto"` fallback note names the token source that failed. A missing `scaleSources` file or one that does not parse read `No spacing tokens were found for scale "auto"; using preset "rhythmic-4".`, the same as a project without tokens. The note now adds the reason, for example `Token source not found: does-not-exist.css.` This changes the note for configs whose token sources fail; see Messages. ([#270](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/270))
|
|
64
|
+
- An unknown `format` in a token source no longer aborts the Stylelint run. In `scaleSources` it is now an invalid option warning; in `.rhythmguardrc.json` (`{"audit":{"tokenSources":[{"path":"tokens.css","format":"scss"}]}}`) the entry is skipped and the `scale: "auto"` fallback note names it, as it does for an rc file that is not valid JSON. The audit still stops on such an entry with `Invalid token source format "scss". ...`. An rc entry the audit rejects (no `path`, an empty `tokenPattern`) is now skipped by the lint too, where it used to be read with the pattern ignored. ([#265](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/265))
|
|
65
|
+
- Lint and audit read the same files from `.rhythmguardrc.json`. The lint honoured `baseDir` and `file` in `audit.tokenSources` entries; the audit ignored `baseDir` and refused `file` with `token source objects must include a path.` Both now read entries through one normaliser: `file` is another name for `path`, and `baseDir` resolves from the config file's directory. This changes the audit's scale for configs that set `baseDir`. ([#271](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/271))
|
|
66
|
+
- `scales: { spacing: "auto" }` next to an explicit `scale` uses `scale` when no spacing tokens are found, as the docs say. It used the `rhythmic-4` fallback, so with `scale: [0, 5, 10, 15, 20]` a `padding: 5px` was reported as `Use spacing scale values (nearest: 4px or 8px)`. This changes findings for configs without spacing tokens that set it. ([#261](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/261))
|
|
67
|
+
- Typography `"auto"` no longer reads Tailwind's sub-tokens such as `--text-sm--line-height: 20px` as font sizes, which put line heights on the type scale. Names with a second `--` after the prefix are left out. This changes findings for Tailwind v4 projects that set `scales: { typography: "auto" }`. ([#262](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/262))
|
|
68
|
+
- Audit token chains resolve through installed token packages and through tokens of any kind in token sources. Chains were resolved against token sources filtered to the spacing kind, without packages, so `--button-padding: var(--space-2)` with `@radix-ui/themes` installed, or `--card-gap: var(--size-8)` with `--size-8` in a token source, read `unresolved`. Those tokens are now looked up; they are not counted as chains of their own, so totals change only where a chain used to be unresolved. ([#259](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/259))
|
|
69
|
+
- `--token-kind` no longer changes the audit's spacing scale. With `--scale auto --token-kind radius`, spacing was checked against the radius tokens (`[0, 3, 6, 10, 14]`); token sources are now read once more for spacing tokens to build the scale, and `--token-kind` shapes the token contract only. ([#260](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/260))
|
|
70
|
+
- `tailwind-class-use-scale` with `allowNegative: false` reports the negative class form. Only a negative value inside the brackets counted, so `-m-[13px]` was reported as off-scale and fixed to `-m-3`, and `-m-[16px]` was not reported at all. `-m-[16px]` and `m-[-16px]` now both read `Negative values are disabled for this rule.`, with no fix. With `allowNegative: false`, negative classes that are on the scale are now reported; the default (`true`) reports nothing new. The audit's class-string scan uses the same analysis. ([#241](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/241))
|
|
71
|
+
- `tailwind-class-use-scale` names and writes utility classes for the configured root size. `spacingUnit` was 4px whatever `baseFontSize` said, so at `baseFontSize: 10` `p-[1.3rem]` was fixed to `p-3`, which is 7.5px at a 10px root. When `spacingUnit` is not set it is now `baseFontSize / 4` (Tailwind's `0.25rem` step, still 4px at the default 16), so that value is fixed to `p-[1.2rem]` and a scale of 2.5px multiples gets classes. `em` values keep the arbitrary form and their unit (`p-[1.1em]` becomes `p-[1em]`, not `p-4`), because a utility class is root-relative and an `em` value follows the element's font size. Configs with the default 16px root and px or rem values get the same fixes as before; an explicit `spacingUnit` is used as given. ([#247](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/247))
|
|
72
|
+
- `tailwind-class-use-scale` checks `class` attributes in Vue and Svelte templates. It visited only `Literal` and `TemplateElement`, so with vue-eslint-parser the `<template>` (a separate tree) was never seen, and with svelte-eslint-parser the static text of `class="..."` (a `SvelteLiteral`) was skipped, while the audit counted both. The rule now walks the Vue template through `parserServices.defineTemplateBodyVisitor` when the parser provides it, checking static `class` attributes and the strings in bound classes, and visits `SvelteLiteral` in `class` attributes. Both are fixed in place, with HTML entities kept as written. No new dependency: the code uses the parsers' node shapes only when they are present. ([#244](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/244))
|
|
73
|
+
- The ESLint plugin's TypeScript declarations are assignable to ESLint's `ESLint.Plugin` and `Linter.Config`, so a typed `eslint.config.ts` with `defineConfig` from `eslint/config` compiles. `configs.recommended.rules` allowed any `number` and rules returned `Record<string, unknown>`; severities are now `0 | 1 | 2 | "off" | "warn" | "error"` and `create` returns listener functions, with a typed `meta`. A typecheck fixture registers both `eslint-plugin-rhythmguard` and `stylelint-plugin-rhythmguard/eslint` through `defineConfig`. ([#242](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/242))
|
|
74
|
+
- The ESLint config samples lint JSX. The samples in the READMEs, `FRAMEWORKS.md`, `TAILWIND.md`, `COMPARISON.md`, the rule docs and the snippet quickstart prints had no `files` (so `npx eslint .` skipped `.jsx` and exited 0) or no JSX parsing (`Parsing error: Unexpected token <`). Each now sets `files: ['**/*.{js,jsx,mjs,cjs}']` and `languageOptions.parserOptions.ecmaFeatures.jsx`, and the package README has an "Other file types" note for TypeScript, Vue, Svelte and Astro parsers. A contract test runs every sample as an `eslint.config.js` against a `.jsx` file. ([#243](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/243))
|
|
75
|
+
- Quickstart and the docs no longer call the audit's fallback scale `rhythmic-4`. The audit and the ESLint rule fall back to `0, 4, 8, 12, 16, 24, 32`, while `rhythmic-4` goes on to 40, 48 and 64, so quickstart reported `margin: 40px` as off-scale and then suggested `scale: "auto"`, which falls back to `rhythmic-4` in Stylelint and passes it. On a fallback, quickstart now suggests the explicit scale it used, so the pasted config reports what quickstart reported, and the note reads `the audit's default scale is being used`. `AUDIT.md`, the ESLint rule docs and the benchmark write-ups name the actual default. The default scales themselves are unchanged; one default for the audit, the ESLint rule and the Stylelint rules changes reports and is planned for 4.0. ([#246](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/246))
|
|
76
|
+
- A CSS token file keeps the scope of each custom property. `scaleSources`, `--token-source` and token-package files dropped it, so `.chip { --chip-spacing: 3px }` in `tokens.css` joined the `:root` scale (`[0,3,4,8,16,24]`) and `padding: 3px` was not reported, although the same CSS in the linted stylesheet left it out. Component-local variables in a token file are now left out whenever the root tokens form a scale, as in the stylesheet. This can change the inferred scale, and so the findings, for a token file that declares component variables. ([#258](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/258))
|
|
77
|
+
- A CSS token file with an unquoted `url(https://...)`, such as `@import url(https://fonts.example.com/x.css);` before `:root`, yielded no tokens and no warning, because `//` was read as a line comment in every file and the rest of the line was skipped. `//` now starts a comment only outside plain `.css` files, and never right after `:` or `(`, so `https://` and `url(//cdn...)` stay values in SCSS too. Applies to token sources, the audit's scan of stylesheets and token chains. ([#263](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/263))
|
|
78
|
+
- A Sass variable declared inside a block no longer defines the scale. `$spacer: 8px` followed by `.compact { $spacer: 2px; }` made `$spacers` evaluate against 2px, so the scale became `[0,1,2,4,6]` and `padding: 16px` was reported. Scale inference now reads only top-level declarations and `!global` ones, in token files and in the linted SCSS file; declarations in rules, mixins, `@media` and `@if` blocks, mixin parameters and `@use ... with (...)` settings are left out. Several top-level declarations on one line are now all read; before, only the first was. Values in the linted file are still evaluated as before. This can change the inferred scale for Sass sources with block-local spacing variables. ([#266](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/266)) On the quiet benchmark this uncovers three findings that a block-local variable had hidden: `$link-underline-thickness * -1` (-3px) in govuk-frontend, and `$_header-item-padding` (12px) and `$_header-item-padding - 1px` (11px) in nhsuk-frontend, whose own scale has no 12px step.
|
|
79
|
+
- DTCG and Style Dictionary references resolve. A semantic token such as `"spacing.sm": { "$value": "{dimension.200}" }` or `"value": "{size.base.value}"` kept the reference text, so a DTCG file of aliases over primitives fell back to the default scale and a Style Dictionary alias was dropped from it. References are now followed within the file, through tokens of any name, before the spacing filter; a reference to a missing token or a cycle keeps its text and still counts as no length. ([#267](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/267))
|
|
80
|
+
- DTCG 2025.10 dimension objects are read. `"$value": { "value": 4, "unit": "px" }` was skipped, so a token file in the stable format yielded no tokens and the scale fell back without saying why. px and rem dimensions are now read (`4px`, `0.75rem`), references resolve through them, and a dimension in another unit is skipped with a warning that names the token. ([#163](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/163))
|
|
81
|
+
- A non-zero token value without a unit is no longer read as px. `--space-ratio: 1.5` and `--spacing-scale: 3` put 1.5px and 3px on an inferred scale, and token chains reported them as `off-scale` with terminal `1.5px`; a DTCG `"$value": 1.5` or a Style Dictionary `"value": 4` did the same. They are now ratios, not lengths: left out of the scale and `non-length` in token chains. A unitless `0` is still a length, and flat JSON numbers (`{ "space-1": 4 }`) are still read as px. ([#268](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/268))
|
|
82
|
+
- A comment or a trailing `!important` in a custom property value no longer drops the token. `--space-1: 4px !important` and `--space-1: 4px /* 0.25rem */` were stored as `4px !important` and `4px /* 0.25rem */`, which do not parse, so the token was lost and the scale could fall back. The value is now read without comments and without the trailing `!important`, and a `;` inside a string or a comment no longer ends it. ([#269](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/269))
|
|
83
|
+
- A comment inside a value is kept by the fix, and the warning points at the literal. PostCSS strips comments from `decl.value`, so `padding: 8px /* keep */ 13px` was reported at the start of the value and fixed to `padding: 8px 12px` with the comment gone; the same happened in `clamp()` and in Sass values, where a `// note` between terms was lost too. The rules now read the value as written, comments included, and splice each fix into it, so the result is `padding: 8px /* keep */ 12px` and lengths inside a Sass `//` comment are never read as values. Applies to `use-scale`, `prefer-token`, `no-offscale-transform` and `use-motion-scale`. ([#233](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/233))
|
|
84
|
+
- A literal operand of a Sass expression that does not resolve is reported but no longer rewritten. With `$gap` unknown, `margin: $gap * 13px` was fixed to `$gap * 12px`, which changes what the expression computes. A literal next to `*`, `/`, `+`, `-` or `%`, or inside a Sass module function such as `math.div(13px, $n)`, in a value containing `$` is now reported without a fix, by `use-scale` and `prefer-token`. A literal that is a term of its own (`13px $gutter`) is still fixed. ([#234](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/234))
|
|
85
|
+
- Typography tokens are no longer offered for spacing properties. `--text-*`, `--fs-*`, `--letter-spacing-*` and `--word-spacing-*` (also prefixed, as `--mantine-letter-spacing-md`) declared no kind, so `padding: 13px` with `fixWith: "token"` was fixed to `var(--text-sm)` and `prefer-token` offered `var(--letter-spacing-wide)` for a margin. They are now typography tokens and serve only `font-size`, `line-height`, `letter-spacing` and `word-spacing`. A Tailwind sub-key such as `--text-sm--line-height` is a typography token too. ([#239](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/239))
|
|
86
|
+
- A raw length in the fallback of a token `var()` is checked. A `var()` whose name matched `tokenPattern` was skipped whole, so `padding: var(--space-1, 7px)` passed while `var(--nope, 13px)` was reported. Only the name is skipped now; the fallback is walked like any value, also when nested (`var(--a, var(--b, 7px))`). This adds reports: `use-scale` and `no-offscale-transform` report an off-scale fallback (`translateX(var(--space-2, 13px))`), and `prefer-token` asks for a token in a fallback (`var(--brand-gap, 8px)` becomes `var(--brand-gap, var(--space-2))`). A fallback the enclosing token already holds (`var(--space-2, 8px)` with `--space-2: 8px`) passes `prefer-token`, and no fix ever writes the token a fallback falls back from. ([#240](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/240))
|
|
87
|
+
- `--staged` audits the staged content. It took the file list from the index but read each file from disk, so a commit carrying `padding: 13px` passed `--max-findings 0` when the working copy had moved on to `color: blue`, and drift that existed only in the working copy failed a clean commit. The staged blobs are now read from the index in one `git cat-file --batch` call and linted as they will be committed, for stylesheets and template files alike, including a staged file that is no longer in the working tree. `--since` still reads the working tree. ([#248](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/248))
|
|
88
|
+
- Git-scoped runs no longer corrupt baselines. With `--staged` or `--since`, `--since-baseline` counted every baseline finding outside the changed files as resolved (`2 resolved, 0 new` for findings in a file that was never scanned), and `--write-baseline` replaced the baseline with the changed files' findings alone, so the next full run failed `--fail-on-new-drift` with `0 resolved, 2 new`. A scoped comparison now counts resolved findings only in the files it scanned, and a scoped write replaces the entries for those files and keeps the rest. A scoped write into a `formatVersion: 1` baseline stops with an error asking for one full `--write-baseline` first, since those entries cannot be merged reliably. ([#249](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/249))
|
|
89
|
+
- Stylesheets the audit did not lint no longer count as clean scanned files. `.scss` files skipped because postcss-scss is not installed and files excluded by the project's `.stylelintignore` were in `CSS files scanned` and in the cleanliness denominator, so a project whose only drift sat in skipped files reported 100%. They are now left out of both, and a baseline finding in such a file is neither resolved nor new; the quickstart prints them as `Skipped 2 not audited (2 SCSS without postcss-scss)`. This changes `scanned.cssFiles`, `scanned.totalFiles` and `scaleCleanliness` for projects with skipped or ignored stylesheets. ([#250](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/250))
|
|
90
|
+
- `--min-cleanliness 100` fails when any file has a finding. Cleanliness was rounded to the nearest percent, so 1 file with drift among 201 reported 100%, passed the gate and showed a 0% drift badge. The gate now compares the exact value and the reported value is rounded down: that run reads 99%, exits 1, and the badge shows 1%. Reported cleanliness can be one point lower than before. ([#251](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/251))
|
|
91
|
+
- Baseline keys no longer depend on the working directory. They named each file relative to the directory the command ran from, so a baseline written by `audit web/src` from the repository root reported `2 resolved, 2 new` and failed `--fail-on-new-drift` when read by `audit ./src` from `web`, with no code change. Baselines now carry `formatVersion: 3` and name files relative to the git top level, or to the scanned directory outside a repository. Baselines written by 3.11.0 still compare: from the directory they were written in as before, and from another one when their recorded `directory` ends the scanned path. They are upgraded on the next `--write-baseline`. ([#252](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/252))
|
|
92
|
+
- A stylesheet with a syntax error is reported as a parse error instead of an off-scale finding. Stylelint runs no rule on a file it cannot parse, and its `CssSyntaxError` warning was read as `{"type":"off-scale","value":null}`, counted in total findings, cleanliness and the badge, with nothing saying the file was not audited. It is now a `parse-error` entry in `findings.parseErrors` with a `summary.parseErrors` count, left out of findings, off-scale counts and cleanliness, listed in the text, Markdown, HTML and quickstart output and annotated with `::error` in `--format github`. The `--schema` output and `types/audit.d.ts` describe the new fields. A file that fails to parse no longer adds to `totalFindings`, so `--max-findings` can pass where it failed before. ([#253](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/253))
|
|
93
|
+
|
|
94
|
+
### Messages
|
|
95
|
+
|
|
96
|
+
- `rhythmguard doctor`: `✗ .rhythmguardrc.json audit config missing` is gone. An invalid `audit` section reads `✗ .rhythmguardrc.json audit config invalid: <the audit's error>`; the decisions section reads `✓ .rhythmguardrc.json decisions valid (2 decisions)`, `✗ .rhythmguardrc.json decisions invalid: <the error>` or `- decisions check skipped (not configured)`. ([#255](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/255))
|
|
97
|
+
- `rhythmguard init`: the write prompt names the file it writes (`Write .stylelintrc? (y/n)` for a JSON `.stylelintrc`; unchanged for `.stylelintrc.json`). When Stylelint loads a config init cannot rewrite, init prints `⚠ Stylelint loads <file> before .stylelintrc.json, so it would never read a .stylelintrc.json.` (or `⚠ Stylelint loads <file>; a new .stylelintrc.json would replace it and leave it unused.`), the lines to add, and `Nothing was written.` ([#257](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/257))
|
|
98
|
+
- `use-scale` with `scale: "auto"`: when a token source fails, the fallback note goes on to say why, `No spacing tokens were found for scale "auto"; using preset "rhythmic-4". Token source not found: does-not-exist.css.`, with `Unable to parse token source tokens.json: ...`, `.rhythmguardrc.json: Invalid token source format "scss". ...` or `.rhythmguardrc.json could not be read: ...` for the other causes. The rejected-scale note (`The spacing tokens found do not form a spacing scale ...`) gets the same addition. The note is unchanged when every source reads. ([#270](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/270), [#265](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/265))
|
|
99
|
+
- `tailwind-class-use-scale`: an `em` value, or a value at a `baseFontSize` other than 16 whose nearest steps are not quarter steps of `baseFontSize / 4`, now gets the px form of the message, `Use scale values (nearest: 16px or 24px).`, where it named classes such as `Use "p-4" (16px) or "p-6" (24px).` With `allowNegative: false`, `-m-[13px]` reads `Negative values are disabled for this rule.` ([#241](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/241), [#247](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/247))
|
|
100
|
+
- Quickstart: `the rhythmic-4 preset is being used.` now reads `the audit's default scale is being used.`, both for no tokens and for rejected tokens. ([#246](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/246))
|
|
101
|
+
- New token source warning for a DTCG dimension in a unit other than px or rem: `Token source tokens.json: skipped --spacing-print (dimension unit "pt" is not px or rem).` ([#163](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/163))
|
|
102
|
+
- Audit text output: when nothing was found but some stylesheets failed to parse, the closing line reads `! No findings in the stylesheets that parsed; 1 could not be parsed and were not audited.` instead of `✓ No issues found. Your design-system rhythm is clean.` New lines: `Parse errors 1 (not audited)` in the summary and a `PARSE ERRORS` section; in Markdown a `Stylesheets that failed to parse (not audited)` row, a `## Parse Errors` table and the next action `Fix the parse errors above: those stylesheets were not audited.`; in the quickstart `Skipped` and `Parse errors` lines. ([#250](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/250), [#253](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/253))
|
|
103
|
+
- A scoped `--write-baseline` into a `formatVersion: 1` baseline fails with `Cannot update <file> from a --staged or --since run: it was written with formatVersion 1, keyed by line. Run --write-baseline once without --staged or --since to upgrade it.` ([#249](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/249))
|
|
104
|
+
|
|
9
105
|
## [3.11.0] - 2026-10-18
|
|
10
106
|
|
|
11
107
|
### Fixed
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
Nobody chose 13px. Rhythmguard catches off-scale spacing in CSS and Tailwind class strings, tells you the nearest steps on your scale, and snaps to them or to your tokens when you ask.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
|
|
10
|
-
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
11
11
|
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
|
|
@@ -59,12 +59,16 @@ import rhythmguard from 'eslint-plugin-rhythmguard';
|
|
|
59
59
|
|
|
60
60
|
export default [
|
|
61
61
|
{
|
|
62
|
+
files: ['**/*.{js,jsx,mjs,cjs}'],
|
|
63
|
+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
|
|
62
64
|
plugins: { 'rhythmguard-tailwind': rhythmguard },
|
|
63
65
|
rules: { 'rhythmguard-tailwind/tailwind-class-use-scale': ['error', { scale: [0, 4, 8, 12, 16, 24, 32] }] },
|
|
64
66
|
},
|
|
65
67
|
];
|
|
66
68
|
```
|
|
67
69
|
|
|
70
|
+
`files` and `ecmaFeatures.jsx` are what make ESLint read `.jsx`; without them it lints no JSX at all. TypeScript, Vue, Svelte and Astro files need their parser (`typescript-eslint`, `vue-eslint-parser`, `svelte-eslint-parser`, `astro-eslint-parser`) in a block of their own: see [Other file types](packages/eslint-plugin-rhythmguard#other-file-types).
|
|
71
|
+
|
|
68
72
|
## Rules
|
|
69
73
|
|
|
70
74
|
| Rule | What it reports | Fix |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Nobody chose 13px. Catches off-scale spacing in CSS and Tailwind class strings and snaps it to your scale or tokens. Stylelint rules, an ESLint companion, and an audit CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rhythmguard": "src/cli/index.js"
|
|
@@ -21,6 +21,28 @@
|
|
|
21
21
|
"type": "commonjs",
|
|
22
22
|
"main": "src/index.js",
|
|
23
23
|
"types": "./types/index.d.ts",
|
|
24
|
+
"typesVersions": {
|
|
25
|
+
"*": {
|
|
26
|
+
"audit": [
|
|
27
|
+
"./types/audit.d.ts"
|
|
28
|
+
],
|
|
29
|
+
"cli": [
|
|
30
|
+
"./types/cli.d.ts"
|
|
31
|
+
],
|
|
32
|
+
"configs/*": [
|
|
33
|
+
"./types/config.d.ts"
|
|
34
|
+
],
|
|
35
|
+
"eslint": [
|
|
36
|
+
"./types/eslint.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"presets": [
|
|
39
|
+
"./types/presets.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"rules/*": [
|
|
42
|
+
"./types/rule.d.ts"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
24
46
|
"exports": {
|
|
25
47
|
".": {
|
|
26
48
|
"types": "./types/index.d.ts",
|
package/src/audit/args.js
CHANGED
|
@@ -164,8 +164,27 @@ function parsePathOption(raw, optionName) {
|
|
|
164
164
|
return String(raw);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* A number typed on the command line or written in .rhythmguardrc.json. A
|
|
169
|
+
* JSON number is taken as is; a string must be plain decimal digits, with a
|
|
170
|
+
* fraction only where one is allowed. `Number()` alone would read an empty
|
|
171
|
+
* value (an unset `$THRESHOLD` in CI) as 0 and `0x10` as 16 (#282).
|
|
172
|
+
*/
|
|
173
|
+
const DECIMAL_INTEGER = /^\d+$/;
|
|
174
|
+
const DECIMAL_NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
175
|
+
|
|
176
|
+
function parseDecimal(raw, pattern) {
|
|
177
|
+
if (typeof raw === 'number') {
|
|
178
|
+
return raw;
|
|
179
|
+
}
|
|
180
|
+
if (typeof raw === 'string' && pattern.test(raw)) {
|
|
181
|
+
return Number(raw);
|
|
182
|
+
}
|
|
183
|
+
return Number.NaN;
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
function parseNonNegativeInteger(raw, optionName) {
|
|
168
|
-
const value =
|
|
187
|
+
const value = parseDecimal(raw, DECIMAL_INTEGER);
|
|
169
188
|
if (!Number.isInteger(value) || value < 0) {
|
|
170
189
|
throw new Error(`${optionName} must be a non-negative integer.`);
|
|
171
190
|
}
|
|
@@ -174,7 +193,7 @@ function parseNonNegativeInteger(raw, optionName) {
|
|
|
174
193
|
}
|
|
175
194
|
|
|
176
195
|
function parsePositiveInteger(raw, optionName) {
|
|
177
|
-
const value =
|
|
196
|
+
const value = parseDecimal(raw, DECIMAL_INTEGER);
|
|
178
197
|
if (!Number.isInteger(value) || value <= 0) {
|
|
179
198
|
throw new Error(`${optionName} must be a positive integer.`);
|
|
180
199
|
}
|
|
@@ -183,7 +202,7 @@ function parsePositiveInteger(raw, optionName) {
|
|
|
183
202
|
}
|
|
184
203
|
|
|
185
204
|
function parsePercentage(raw, optionName) {
|
|
186
|
-
const value =
|
|
205
|
+
const value = parseDecimal(raw, DECIMAL_NUMBER);
|
|
187
206
|
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
188
207
|
throw new Error(`${optionName} must be a number between 0 and 100.`);
|
|
189
208
|
}
|
|
@@ -267,7 +286,7 @@ function validateScale(scale, optionName = '--scale', raw = scale) {
|
|
|
267
286
|
}
|
|
268
287
|
|
|
269
288
|
function parseBaseFontSize(raw) {
|
|
270
|
-
const value =
|
|
289
|
+
const value = parseDecimal(raw, DECIMAL_NUMBER);
|
|
271
290
|
if (!Number.isFinite(value) || value <= 0) {
|
|
272
291
|
throw new Error('--base-font-size must be a positive number.');
|
|
273
292
|
}
|
package/src/audit/baseline.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
+
const { readJsonFile } = require('../core/json');
|
|
6
|
+
const { exactScaleCleanliness } = require('./contract');
|
|
7
|
+
const { findGitTopLevel } = require('./scan/files');
|
|
5
8
|
const { formatPath } = require('./shared');
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -9,28 +12,96 @@ const { formatPath } = require('./shared');
|
|
|
9
12
|
* file, property (or Tailwind class), value, and the occurrence index among
|
|
10
13
|
* identical findings in that file. Inserting a comment above a declaration no
|
|
11
14
|
* longer turns it into one "resolved" and one "new"; adding a second identical
|
|
12
|
-
* off-scale declaration still counts as new.
|
|
13
|
-
*
|
|
15
|
+
* off-scale declaration still counts as new.
|
|
16
|
+
*
|
|
17
|
+
* Version 3 names the file relative to a root that does not move with the
|
|
18
|
+
* working directory: the git top level when the scanned directory is inside a
|
|
19
|
+
* repository, otherwise the scanned directory itself. Version 2 baselines
|
|
20
|
+
* (3.x up to 3.11.0) named it relative to the directory the command ran from;
|
|
21
|
+
* they are read by translating each file to the version 3 form. Version 1
|
|
22
|
+
* baselines keyed by line and column are still compared with the key they
|
|
23
|
+
* were written with.
|
|
14
24
|
*/
|
|
15
|
-
const BASELINE_FORMAT_VERSION =
|
|
25
|
+
const BASELINE_FORMAT_VERSION = 3;
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
const KEY_SEPARATOR = '\u001f';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What the run looked at, so a baseline is only judged on files the audit
|
|
31
|
+
* actually read. `scoped` is true for --staged and --since runs.
|
|
32
|
+
*/
|
|
33
|
+
function createBaselineScope(report, scope = {}) {
|
|
34
|
+
const scanRoot = path.resolve(scope.scanRoot || report.directory || '.');
|
|
35
|
+
let realScanRoot = scanRoot;
|
|
36
|
+
try {
|
|
37
|
+
realScanRoot = fs.realpathSync(scanRoot);
|
|
38
|
+
} catch {
|
|
39
|
+
// The directory was audited, so it exists; keep the given form if not.
|
|
40
|
+
}
|
|
41
|
+
const keyRoot = findGitTopLevel(scanRoot) || realScanRoot;
|
|
42
|
+
|
|
43
|
+
// Name a file relative to the key root. Paths under the scanned directory
|
|
44
|
+
// go through its real path, the form git reports the top level in.
|
|
45
|
+
const toKeyFile = (absolutePath) => {
|
|
46
|
+
const insideScan = path.relative(scanRoot, absolutePath);
|
|
47
|
+
const realPath = insideScan.startsWith('..') || path.isAbsolute(insideScan)
|
|
48
|
+
? absolutePath
|
|
49
|
+
: path.join(realScanRoot, insideScan);
|
|
50
|
+
return path.relative(keyRoot, realPath).split(path.sep).join('/');
|
|
51
|
+
};
|
|
52
|
+
const findingKeyFile = (finding) => toKeyFile(path.resolve(process.cwd(), String(finding.file || '')));
|
|
53
|
+
const toKeyFiles = (files) => new Set((files || []).map((file) => toKeyFile(path.resolve(file))));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
auditedFiles: toKeyFiles(scope.auditedFiles),
|
|
57
|
+
findingKeyFile,
|
|
58
|
+
scanRoot,
|
|
59
|
+
scoped: scope.scoped === true,
|
|
60
|
+
toKeyFile,
|
|
61
|
+
unauditedFiles: toKeyFiles(scope.unauditedFiles),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A baseline finding that is no longer reported counts as resolved only when
|
|
67
|
+
* the run audited its file. A --staged or --since run reads the changed files
|
|
68
|
+
* alone, and no run reads a stylesheet that was skipped, ignored or failed to
|
|
69
|
+
* parse, so findings there are neither resolved nor new.
|
|
70
|
+
*/
|
|
71
|
+
function countsAsResolved(scope, keyFile) {
|
|
72
|
+
return scope.scoped
|
|
73
|
+
? scope.auditedFiles.has(keyFile)
|
|
74
|
+
: !scope.unauditedFiles.has(keyFile);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function applyBaselineComparison(report, baselinePath, scopeInput) {
|
|
18
78
|
const resolvedPath = path.resolve(process.cwd(), baselinePath);
|
|
19
79
|
if (!fs.existsSync(resolvedPath)) {
|
|
20
80
|
throw new Error(`Baseline file not found: ${baselinePath}`);
|
|
21
81
|
}
|
|
22
82
|
|
|
23
|
-
const baseline =
|
|
83
|
+
const baseline = readBaselineFile(resolvedPath, baselinePath);
|
|
24
84
|
const baselineFindings = Array.isArray(baseline.findings) ? baseline.findings : [];
|
|
25
|
-
const
|
|
85
|
+
const scope = createBaselineScope(report, scopeInput);
|
|
26
86
|
const currentFindings = getAllFindings(report);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
87
|
+
|
|
88
|
+
let currentKeys;
|
|
89
|
+
let entries;
|
|
90
|
+
if (isContentKeyed(baseline)) {
|
|
91
|
+
currentKeys = assignFindingKeys(currentFindings, scope.findingKeyFile);
|
|
92
|
+
entries = normalizeBaselineEntries(baseline, scope);
|
|
93
|
+
} else {
|
|
94
|
+
currentKeys = new Map(currentFindings.map((finding) => [finding, createLegacyFindingKey(finding)]));
|
|
95
|
+
entries = baselineFindings.map((finding) => ({
|
|
96
|
+
file: scope.findingKeyFile(finding),
|
|
97
|
+
key: finding.key || createLegacyFindingKey(finding),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const baselineKeys = new Set(entries.map((entry) => entry.key));
|
|
31
102
|
const currentKeySet = new Set(currentKeys.values());
|
|
32
103
|
const newFindings = currentFindings.filter((finding) => !baselineKeys.has(currentKeys.get(finding)));
|
|
33
|
-
const resolvedFindings =
|
|
104
|
+
const resolvedFindings = entries.filter((entry) => !currentKeySet.has(entry.key) && countsAsResolved(scope, entry.file));
|
|
34
105
|
|
|
35
106
|
report.baseline = {
|
|
36
107
|
baselineFindings: baselineFindings.length,
|
|
@@ -43,31 +114,122 @@ function applyBaselineComparison(report, baselinePath) {
|
|
|
43
114
|
report.summary.resolvedFindings = resolvedFindings.length;
|
|
44
115
|
}
|
|
45
116
|
|
|
46
|
-
|
|
117
|
+
/**
|
|
118
|
+
* A baseline file as JSON. A parse error names the file, so a corrupt
|
|
119
|
+
* baseline is not reported as a bare `JSON.parse` message (#290).
|
|
120
|
+
*/
|
|
121
|
+
function readBaselineFile(resolvedPath, baselinePath) {
|
|
122
|
+
try {
|
|
123
|
+
return readJsonFile(resolvedPath);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw new Error(`Invalid baseline file ${baselinePath}: ${err.message}`, { cause: err });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeBaseline(report, baselinePath, scopeInput) {
|
|
47
130
|
const resolvedPath = path.resolve(process.cwd(), baselinePath);
|
|
131
|
+
const scope = createBaselineScope(report, scopeInput);
|
|
48
132
|
const findings = getAllFindings(report);
|
|
49
|
-
const keys = assignFindingKeys(findings);
|
|
133
|
+
const keys = assignFindingKeys(findings, scope.findingKeyFile);
|
|
134
|
+
let records = findings.map((finding) => toBaselineFinding(finding, keys.get(finding), scope.findingKeyFile(finding)));
|
|
135
|
+
let scaleCleanliness = report.scaleCleanliness;
|
|
136
|
+
|
|
137
|
+
// A --staged or --since run saw only the changed files. It replaces the
|
|
138
|
+
// entries for the files it audited and keeps every other entry, so a scoped
|
|
139
|
+
// run never shrinks the baseline.
|
|
140
|
+
if (scope.scoped && fs.existsSync(resolvedPath)) {
|
|
141
|
+
const existing = readBaselineFile(resolvedPath, baselinePath);
|
|
142
|
+
if (!isContentKeyed(existing)) {
|
|
143
|
+
throw new Error(`Cannot update ${baselinePath} from a --staged or --since run: it was written with formatVersion 1, keyed by line. Run --write-baseline once without --staged or --since to upgrade it.`);
|
|
144
|
+
}
|
|
145
|
+
const kept = normalizeBaselineEntries(existing, scope)
|
|
146
|
+
.filter((entry) => !scope.auditedFiles.has(entry.file))
|
|
147
|
+
.map((entry) => ({ ...entry.record, file: entry.file, key: entry.key }));
|
|
148
|
+
records = [...kept, ...records].sort((a, b) =>
|
|
149
|
+
String(a.file).localeCompare(String(b.file)) || (a.line || 0) - (b.line || 0) || (a.column || 0) - (b.column || 0));
|
|
150
|
+
if (existing.summary && Number.isFinite(existing.summary.scaleCleanliness)) {
|
|
151
|
+
scaleCleanliness = existing.summary.scaleCleanliness;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
50
155
|
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
51
156
|
fs.writeFileSync(
|
|
52
157
|
resolvedPath,
|
|
53
158
|
`${JSON.stringify({
|
|
54
159
|
createdAt: new Date().toISOString(),
|
|
55
160
|
directory: report.directory,
|
|
56
|
-
findings:
|
|
161
|
+
findings: records,
|
|
57
162
|
formatVersion: BASELINE_FORMAT_VERSION,
|
|
58
163
|
summary: {
|
|
59
|
-
scaleCleanliness
|
|
60
|
-
totalFindings:
|
|
164
|
+
scaleCleanliness,
|
|
165
|
+
totalFindings: records.length,
|
|
61
166
|
},
|
|
62
167
|
}, null, 2)}\n`,
|
|
63
168
|
);
|
|
64
169
|
|
|
65
170
|
report.baselineWritten = {
|
|
66
171
|
file: formatPath(resolvedPath),
|
|
67
|
-
findings:
|
|
172
|
+
findings: records.length,
|
|
68
173
|
};
|
|
69
174
|
}
|
|
70
175
|
|
|
176
|
+
/** Version 2 and later key findings by content; version 1 (or none) by line. */
|
|
177
|
+
function isContentKeyed(baseline) {
|
|
178
|
+
return Number.isInteger(baseline.formatVersion) && baseline.formatVersion >= 2;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Baseline entries with their file and key in the current form. Version 3
|
|
183
|
+
* entries are already in it. A version 2 file and key are relative to the
|
|
184
|
+
* directory the baseline was written from, which `legacyWriterDirectory` works
|
|
185
|
+
* out; the key keeps its rule, type, property, value and occurrence index and
|
|
186
|
+
* only the file part is rewritten.
|
|
187
|
+
*/
|
|
188
|
+
function normalizeBaselineEntries(baseline, scope) {
|
|
189
|
+
const findings = Array.isArray(baseline.findings) ? baseline.findings : [];
|
|
190
|
+
if (baseline.formatVersion >= BASELINE_FORMAT_VERSION) {
|
|
191
|
+
return findings.map((record) => ({ file: String(record.file || ''), key: record.key, record }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const writerDirectory = legacyWriterDirectory(baseline.directory, scope.scanRoot);
|
|
195
|
+
return findings.map((record) => {
|
|
196
|
+
const file = scope.toKeyFile(path.resolve(writerDirectory, String(record.file || '')));
|
|
197
|
+
const parts = typeof record.key === 'string' ? record.key.split(KEY_SEPARATOR) : [];
|
|
198
|
+
const key = parts.length === 6
|
|
199
|
+
? [...parts.slice(0, 2), file, ...parts.slice(3)].join(KEY_SEPARATOR)
|
|
200
|
+
: `${findingIdentity(record, file)}${KEY_SEPARATOR}0`;
|
|
201
|
+
return { file, key, record };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The directory a version 2 baseline was written from. Its files are relative
|
|
207
|
+
* to that directory, and `directory` records the audited directory as it was
|
|
208
|
+
* typed there. When that directory, read from here, is the one being audited,
|
|
209
|
+
* the baseline was written from here (the only case 3.11.0 matched). When it is
|
|
210
|
+
* not, but the audited directory ends with it (`web/src` written from the
|
|
211
|
+
* parent, audited as `./src` from `web`), the baseline was written from the
|
|
212
|
+
* directory that many levels up. Anything else is read as written from here.
|
|
213
|
+
*/
|
|
214
|
+
function legacyWriterDirectory(directory, scanRoot) {
|
|
215
|
+
const cwd = process.cwd();
|
|
216
|
+
if (typeof directory !== 'string' || directory === '' || path.isAbsolute(directory)) {
|
|
217
|
+
return cwd;
|
|
218
|
+
}
|
|
219
|
+
if (path.resolve(cwd, directory) === scanRoot) {
|
|
220
|
+
return cwd;
|
|
221
|
+
}
|
|
222
|
+
const segments = directory.split(/[\\/]+/).filter((segment) => segment !== '' && segment !== '.');
|
|
223
|
+
if (segments.length === 0 || segments.includes('..')) {
|
|
224
|
+
return cwd;
|
|
225
|
+
}
|
|
226
|
+
const scanSegments = scanRoot.split(path.sep);
|
|
227
|
+
if (scanSegments.slice(-segments.length).join('/') !== segments.join('/')) {
|
|
228
|
+
return cwd;
|
|
229
|
+
}
|
|
230
|
+
return path.resolve(scanRoot, ...segments.map(() => '..'));
|
|
231
|
+
}
|
|
232
|
+
|
|
71
233
|
function getAllFindings(report) {
|
|
72
234
|
return [
|
|
73
235
|
...report.findings.css,
|
|
@@ -76,10 +238,10 @@ function getAllFindings(report) {
|
|
|
76
238
|
];
|
|
77
239
|
}
|
|
78
240
|
|
|
79
|
-
function toBaselineFinding(finding, key) {
|
|
241
|
+
function toBaselineFinding(finding, key, file = finding.file) {
|
|
80
242
|
return {
|
|
81
243
|
column: finding.column,
|
|
82
|
-
file
|
|
244
|
+
file,
|
|
83
245
|
key,
|
|
84
246
|
line: finding.line,
|
|
85
247
|
property: finding.property || undefined,
|
|
@@ -91,27 +253,31 @@ function toBaselineFinding(finding, key) {
|
|
|
91
253
|
};
|
|
92
254
|
}
|
|
93
255
|
|
|
94
|
-
function findingIdentity(finding) {
|
|
256
|
+
function findingIdentity(finding, file = finding.file) {
|
|
95
257
|
return [
|
|
96
258
|
finding.rule || '',
|
|
97
259
|
finding.type || '',
|
|
98
|
-
|
|
260
|
+
file || '',
|
|
99
261
|
finding.property || finding.token || '',
|
|
100
262
|
finding.value || finding.rawValue || '',
|
|
101
|
-
].join(
|
|
263
|
+
].join(KEY_SEPARATOR);
|
|
102
264
|
}
|
|
103
265
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Content keys for a set of findings: identity plus the occurrence index in
|
|
268
|
+
* source order. `fileOf` names each finding's file in the key (the version 3
|
|
269
|
+
* root-relative form when called from the audit).
|
|
270
|
+
*/
|
|
271
|
+
function assignFindingKeys(findings, fileOf = (finding) => finding.file) {
|
|
106
272
|
const ordered = [...findings].sort((a, b) =>
|
|
107
273
|
String(a.file).localeCompare(String(b.file)) || (a.line || 0) - (b.line || 0) || (a.column || 0) - (b.column || 0));
|
|
108
274
|
const seen = new Map();
|
|
109
275
|
const keys = new Map();
|
|
110
276
|
for (const finding of ordered) {
|
|
111
|
-
const identity = findingIdentity(finding);
|
|
277
|
+
const identity = findingIdentity(finding, fileOf(finding));
|
|
112
278
|
const occurrence = seen.get(identity) || 0;
|
|
113
279
|
seen.set(identity, occurrence + 1);
|
|
114
|
-
keys.set(finding, `${identity}
|
|
280
|
+
keys.set(finding, `${identity}${KEY_SEPARATOR}${occurrence}`);
|
|
115
281
|
}
|
|
116
282
|
return keys;
|
|
117
283
|
}
|
|
@@ -126,7 +292,7 @@ function createLegacyFindingKey(finding) {
|
|
|
126
292
|
finding.column || '',
|
|
127
293
|
finding.value || finding.rawValue || finding.token || '',
|
|
128
294
|
finding.text || '',
|
|
129
|
-
].join(
|
|
295
|
+
].join(KEY_SEPARATOR);
|
|
130
296
|
}
|
|
131
297
|
|
|
132
298
|
function getAuditFailures(report, parsed) {
|
|
@@ -136,7 +302,9 @@ function getAuditFailures(report, parsed) {
|
|
|
136
302
|
failures.push(`total findings ${report.totalWarnings} exceeds --max-findings ${parsed.maxFindings}`);
|
|
137
303
|
}
|
|
138
304
|
|
|
139
|
-
|
|
305
|
+
// The gate reads the unrounded value: 199 clean files of 200 is 99.5%, which
|
|
306
|
+
// must not pass --min-cleanliness 100.
|
|
307
|
+
if (parsed.minCleanliness !== null && exactScaleCleanliness(report) < parsed.minCleanliness) {
|
|
140
308
|
failures.push(`scale cleanliness ${report.scaleCleanliness}% is below --min-cleanliness ${parsed.minCleanliness}%`);
|
|
141
309
|
}
|
|
142
310
|
|
|
@@ -151,8 +319,10 @@ module.exports = {
|
|
|
151
319
|
BASELINE_FORMAT_VERSION,
|
|
152
320
|
applyBaselineComparison,
|
|
153
321
|
assignFindingKeys,
|
|
322
|
+
createBaselineScope,
|
|
154
323
|
createLegacyFindingKey,
|
|
155
324
|
getAllFindings,
|
|
156
325
|
getAuditFailures,
|
|
326
|
+
legacyWriterDirectory,
|
|
157
327
|
writeBaseline,
|
|
158
328
|
};
|