stylelint-plugin-rhythmguard 1.9.0 → 2.0.1

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 CHANGED
@@ -6,6 +6,33 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.0.1] - 2026-06-17
10
+
11
+ ### Added
12
+
13
+ - Added TypeScript declaration files for the public plugin, config, rule, preset, ESLint companion, and Audit 2.0 API exports.
14
+ - Added dependency-free Audit 2.0 dashboard and Figma-friendly export examples under `examples/`.
15
+ - Added CI adoption and motion-default evidence docs for safer baseline-based rollout.
16
+
17
+ ### Fixed
18
+
19
+ - Fixed `rhythmguard init` scripted input handling so multiple prompts work reliably in piped/CI contexts.
20
+
21
+ ## [2.0.0] - 2026-05-23
22
+
23
+ ### Changed
24
+
25
+ - `rhythmguard audit --format json` now emits the stable audit contract with `schemaVersion: "2.0"`.
26
+ - Moved the pre-2.0 audit JSON shape to `--format json-v1` for migration compatibility.
27
+ - Updated audit defaults to use the explicit spacing token pattern `^--(space|spacing)-`.
28
+
29
+ ### Added
30
+
31
+ - Added `stylelint-plugin-rhythmguard/audit` with `createAuditReport`, `loadAuditConfig`, `parseTokenSources`, and `toAuditContractReport`.
32
+ - Added `rhythmguard audit --format html`, `--output <file>`, and `--schema`.
33
+ - Added `rhythmguard doctor` checks for `.rhythmguardrc.json`, configured token sources, motion audit config, and baseline files.
34
+ - Added `docs/MIGRATING_TO_2.md`.
35
+
9
36
  ## [1.9.0] - 2026-05-23
10
37
 
11
38
  ### Added
package/README.md CHANGED
@@ -97,9 +97,11 @@ npx rhythmguard audit ./src --staged --max-findings 0
97
97
  npx rhythmguard audit ./src --token-source ./tokens.json
98
98
  npx rhythmguard audit ./src --token-source ./theme.css --token-source-format css
99
99
  npx rhythmguard audit ./src --include-motion
100
+ npx rhythmguard audit ./src --format html --output rhythmguard-report.html
101
+ npx rhythmguard audit --schema
100
102
  ```
101
103
 
102
- The report covers authored CSS declarations, Tailwind arbitrary spacing values in common template/source files, and token-contract drift such as missing spacing tokens, unused spacing tokens, repeated raw values that deserve token review, raw values that match known tokens, and conflicting token values. Scan paths are scoped to the directory argument. Use `--ignore`, `.rhythmguardignore`, or `--ignore-path` for generated or legacy subtrees, then add baselines and CI thresholds when you are ready to gate new drift. Markdown output is PR-ready for UX developers, UX designers, and design-system owners:
104
+ The report covers authored CSS declarations, Tailwind arbitrary spacing values in common template/source files, and token-contract drift such as missing spacing tokens, unused spacing tokens, repeated raw values that deserve token review, raw values that match known tokens, conflicting token values, and opt-in motion rhythm drift. Scan paths are scoped to the directory argument. Use `--ignore`, `.rhythmguardignore`, or `--ignore-path` for generated or legacy subtrees, then add baselines and CI thresholds when you are ready to gate new drift. Markdown output is PR-ready for UX developers, UX designers, and design-system owners:
103
105
 
104
106
  ```md
105
107
  # Rhythmguard Design-System Audit
@@ -136,6 +138,44 @@ For large codebases, put shared audit settings in `.rhythmguardrc.json`:
136
138
 
137
139
  `rhythmguard audit` loads `.rhythmguardrc.json` automatically when present. Use `--config <file>` for another config, `--no-config` to skip config discovery, and `--token-source <file>` for extra canonical token files. Token source paths in config files resolve from the config file directory; CLI token source paths resolve from the current working directory. Supported source formats are CSS custom properties and Tailwind v4 `@theme`, flat JSON maps, Style Dictionary JSON, and DTCG JSON.
138
140
 
141
+ ### Audit JSON 2.0 and API
142
+
143
+ In Rhythmguard 2.0, `--format json` emits the stable audit contract:
144
+
145
+ ```json
146
+ {
147
+ "schemaVersion": "2.0",
148
+ "command": { "directory": "./src", "scanScope": "full" },
149
+ "summary": { "totalFindings": 12, "scaleCleanliness": 94 },
150
+ "scanned": { "cssFiles": 10, "templateFiles": 20 },
151
+ "contracts": {
152
+ "scale": {},
153
+ "tokens": {},
154
+ "motion": {}
155
+ },
156
+ "findings": {
157
+ "css": [],
158
+ "tailwind": [],
159
+ "motion": []
160
+ },
161
+ "baseline": null
162
+ }
163
+ ```
164
+
165
+ Use `--format json-v1` for the pre-2.0 JSON shape during migration.
166
+
167
+ Programmatic usage:
168
+
169
+ ```js
170
+ const {
171
+ createAuditReport,
172
+ toAuditContractReport,
173
+ } = require('stylelint-plugin-rhythmguard/audit');
174
+
175
+ const report = await createAuditReport({ dir: './src', noConfig: true });
176
+ const contract = toAuditContractReport(report);
177
+ ```
178
+
139
179
  ## Installation
140
180
 
141
181
  ```bash
@@ -251,6 +291,8 @@ Framework-specific setup for Vue, Lit, Astro, and SvelteKit: [`docs/FRAMEWORKS.m
251
291
 
252
292
  - Side-by-side tool fit guide with migration snippets: [`docs/COMPARISON.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/COMPARISON.md)
253
293
  - Audit 2.0 validation and roadmap: [`docs/AUDIT_2_VALIDATION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/AUDIT_2_VALIDATION.md)
294
+ - CI baseline rollout recipe: [`docs/CI_ADOPTION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/CI_ADOPTION.md)
295
+ - Programmatic dashboard and Figma-friendly export examples: [`docs/AUDIT_API_EXAMPLES.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/AUDIT_API_EXAMPLES.md)
254
296
  - Real-world before/after excerpts from public repos: [`docs/ADOPTION_DIFFS.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/ADOPTION_DIFFS.md)
255
297
  - Distribution submissions to Stylelint discovery surfaces: [`docs/DISTRIBUTION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DISTRIBUTION.md)
256
298
 
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createAuditReport, toAuditContractReport } from 'stylelint-plugin-rhythmguard/audit';
6
+
7
+ function readOption(argv, name, fallback) {
8
+ const index = argv.indexOf(name);
9
+ if (index === -1 || !argv[index + 1] || argv[index + 1].startsWith('--')) {
10
+ return fallback;
11
+ }
12
+
13
+ return argv[index + 1];
14
+ }
15
+
16
+ function escapeHtml(value) {
17
+ return String(value)
18
+ .replace(/&/g, '&amp;')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;');
22
+ }
23
+
24
+ function collectFindings(findings) {
25
+ return [
26
+ ...(findings.css || []),
27
+ ...(findings.tailwind || []),
28
+ ...(findings.motion || []),
29
+ ];
30
+ }
31
+
32
+ function renderMetric(label, value) {
33
+ return `<article class="metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></article>`;
34
+ }
35
+
36
+ function renderDashboard(contract) {
37
+ const findings = collectFindings(contract.findings);
38
+ const topFindings = findings.slice(0, 12);
39
+ const cleanliness = contract.contracts.scale.cleanliness;
40
+ const score = typeof cleanliness === 'number' ? cleanliness : contract.summary.cleanliness;
41
+
42
+ return `<!doctype html>
43
+ <html lang="en">
44
+ <head>
45
+ <meta charset="utf-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1">
47
+ <title>Rhythmguard Audit Dashboard</title>
48
+ <style>
49
+ body{font-family:Inter,ui-sans-serif,system-ui,sans-serif;margin:0;background:#f7f8fa;color:#111827;}
50
+ main{max-width:1120px;margin:0 auto;padding:32px 20px;}
51
+ h1{font-size:28px;margin:0 0 8px;}
52
+ p{color:#4b5563;margin:0 0 24px;}
53
+ .metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:24px 0;}
54
+ .metric{border:1px solid #d8dde5;background:#fff;border-radius:8px;padding:16px;}
55
+ .metric span{display:block;color:#6b7280;font-size:12px;text-transform:uppercase;}
56
+ .metric strong{display:block;font-size:28px;margin-top:8px;}
57
+ table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #d8dde5;border-radius:8px;overflow:hidden;}
58
+ th,td{text-align:left;border-bottom:1px solid #e5e7eb;padding:10px 12px;font-size:14px;}
59
+ th{background:#f1f5f9;color:#374151;font-size:12px;text-transform:uppercase;}
60
+ tr:last-child td{border-bottom:0;}
61
+ code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;}
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <main>
66
+ <h1>Rhythmguard Audit Dashboard</h1>
67
+ <p>${escapeHtml(contract.command.directory)} · ${escapeHtml(contract.command.scanScope)} · schema ${escapeHtml(contract.schemaVersion)}</p>
68
+ <section class="metrics">
69
+ ${renderMetric('Scale cleanliness', `${score ?? 100}%`)}
70
+ ${renderMetric('CSS files', contract.scanned.cssFiles)}
71
+ ${renderMetric('Template files', contract.scanned.templateFiles)}
72
+ ${renderMetric('Findings', findings.length)}
73
+ </section>
74
+ <table>
75
+ <thead><tr><th>Type</th><th>File</th><th>Value</th><th>Message</th></tr></thead>
76
+ <tbody>
77
+ ${topFindings.map((finding) => `<tr><td>${escapeHtml(finding.type)}</td><td><code>${escapeHtml(finding.file)}</code></td><td><code>${escapeHtml(finding.value || finding.rawValue || '')}</code></td><td>${escapeHtml(finding.message || '')}</td></tr>`).join('\n')}
78
+ </tbody>
79
+ </table>
80
+ </main>
81
+ </body>
82
+ </html>`;
83
+ }
84
+
85
+ const argv = process.argv.slice(2);
86
+ const dir = readOption(argv, '--dir', argv.find((arg) => !arg.startsWith('--')) || 'src');
87
+ const output = readOption(argv, '--output', 'rhythmguard-dashboard.html');
88
+ const includeMotion = argv.includes('--include-motion');
89
+
90
+ const report = await createAuditReport({ dir, includeMotion });
91
+ const contract = toAuditContractReport(report);
92
+ const outputPath = path.resolve(process.cwd(), output);
93
+
94
+ fs.writeFileSync(outputPath, renderDashboard(contract));
95
+ process.stdout.write(`Wrote ${outputPath}\n`);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createAuditReport, toAuditContractReport } from 'stylelint-plugin-rhythmguard/audit';
6
+
7
+ function readOption(argv, name, fallback) {
8
+ const index = argv.indexOf(name);
9
+ if (index === -1 || !argv[index + 1] || argv[index + 1].startsWith('--')) {
10
+ return fallback;
11
+ }
12
+
13
+ return argv[index + 1];
14
+ }
15
+
16
+ function collectFindings(findings) {
17
+ return [
18
+ ...(findings.css || []),
19
+ ...(findings.tailwind || []),
20
+ ...(findings.motion || []),
21
+ ];
22
+ }
23
+
24
+ function countBy(items, getKey) {
25
+ const counts = new Map();
26
+ for (const item of items) {
27
+ const key = getKey(item) || 'unknown';
28
+ counts.set(key, (counts.get(key) || 0) + 1);
29
+ }
30
+
31
+ return [...counts.entries()]
32
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
33
+ .map(([label, value]) => ({ label, value }));
34
+ }
35
+
36
+ function buildFigmaPayload(contract) {
37
+ const findings = collectFindings(contract.findings);
38
+ const cleanliness = contract.contracts.scale.cleanliness;
39
+
40
+ return {
41
+ schema: 'rhythmguard.figma-export.v1',
42
+ generatedAt: new Date().toISOString(),
43
+ source: {
44
+ directory: contract.command.directory,
45
+ scanScope: contract.command.scanScope,
46
+ schemaVersion: contract.schemaVersion,
47
+ },
48
+ summaryCards: [
49
+ {
50
+ label: 'Scale cleanliness',
51
+ value: `${typeof cleanliness === 'number' ? cleanliness : contract.summary.cleanliness ?? 100}%`,
52
+ },
53
+ { label: 'CSS files', value: contract.scanned.cssFiles },
54
+ { label: 'Template files', value: contract.scanned.templateFiles },
55
+ { label: 'Findings', value: findings.length },
56
+ ],
57
+ charts: {
58
+ findingsByType: countBy(findings, (finding) => finding.type),
59
+ findingsByFile: countBy(findings, (finding) => finding.file).slice(0, 10),
60
+ },
61
+ findings: findings.slice(0, 25).map((finding) => ({
62
+ file: finding.file,
63
+ line: finding.line || null,
64
+ message: finding.message || '',
65
+ type: finding.type,
66
+ value: finding.value || finding.rawValue || null,
67
+ })),
68
+ };
69
+ }
70
+
71
+ const argv = process.argv.slice(2);
72
+ const dir = readOption(argv, '--dir', argv.find((arg) => !arg.startsWith('--')) || 'src');
73
+ const output = readOption(argv, '--output', 'rhythmguard-figma-export.json');
74
+ const includeMotion = argv.includes('--include-motion');
75
+
76
+ const report = await createAuditReport({ dir, includeMotion });
77
+ const contract = toAuditContractReport(report);
78
+ const outputPath = path.resolve(process.cwd(), output);
79
+
80
+ fs.writeFileSync(outputPath, `${JSON.stringify(buildFigmaPayload(contract), null, 2)}\n`);
81
+ process.stdout.write(`Wrote ${outputPath}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "1.9.0",
3
+ "version": "2.0.1",
4
4
  "description": "Token governance for CSS and Tailwind — enforce spacing scales, require design tokens, catch arbitrary values",
5
5
  "bin": {
6
6
  "rhythmguard": "src/cli/index.js"
@@ -20,72 +20,95 @@
20
20
  ],
21
21
  "type": "commonjs",
22
22
  "main": "src/index.js",
23
+ "types": "./types/index.d.ts",
23
24
  "exports": {
24
25
  ".": {
26
+ "types": "./types/index.d.ts",
25
27
  "require": "./src/index.js",
26
28
  "import": "./src/index.mjs"
27
29
  },
28
30
  "./configs/recommended": {
31
+ "types": "./types/config.d.ts",
29
32
  "require": "./src/configs/recommended.js",
30
33
  "import": "./src/configs/recommended.mjs"
31
34
  },
32
35
  "./configs/strict": {
36
+ "types": "./types/config.d.ts",
33
37
  "require": "./src/configs/strict.js",
34
38
  "import": "./src/configs/strict.mjs"
35
39
  },
36
40
  "./configs/tailwind": {
41
+ "types": "./types/config.d.ts",
37
42
  "require": "./src/configs/tailwind.js",
38
43
  "import": "./src/configs/tailwind.mjs"
39
44
  },
40
45
  "./configs/expanded": {
46
+ "types": "./types/config.d.ts",
41
47
  "require": "./src/configs/expanded.js",
42
48
  "import": "./src/configs/expanded.mjs"
43
49
  },
44
50
  "./configs/logical": {
51
+ "types": "./types/config.d.ts",
45
52
  "require": "./src/configs/logical.js",
46
53
  "import": "./src/configs/logical.mjs"
47
54
  },
48
55
  "./configs/migration": {
56
+ "types": "./types/config.d.ts",
49
57
  "require": "./src/configs/migration.js",
50
58
  "import": "./src/configs/migration.mjs"
51
59
  },
52
60
  "./configs/motion": {
61
+ "types": "./types/config.d.ts",
53
62
  "require": "./src/configs/motion.js",
54
63
  "import": "./src/configs/motion.mjs"
55
64
  },
56
65
  "./configs/react-tailwind": {
66
+ "types": "./types/config.d.ts",
57
67
  "require": "./src/configs/react-tailwind.js",
58
68
  "import": "./src/configs/react-tailwind.mjs"
59
69
  },
60
70
  "./presets": {
71
+ "types": "./types/presets.d.ts",
61
72
  "require": "./src/presets/index.js",
62
73
  "import": "./src/presets/index.mjs"
63
74
  },
75
+ "./audit": {
76
+ "types": "./types/audit.d.ts",
77
+ "require": "./src/audit/index.js",
78
+ "import": "./src/audit/index.mjs"
79
+ },
64
80
  "./rules/use-scale": {
81
+ "types": "./types/rule.d.ts",
65
82
  "require": "./src/rules/use-scale/index.js",
66
83
  "import": "./src/rules/use-scale/index.mjs"
67
84
  },
68
85
  "./rules/prefer-token": {
86
+ "types": "./types/rule.d.ts",
69
87
  "require": "./src/rules/prefer-token/index.js",
70
88
  "import": "./src/rules/prefer-token/index.mjs"
71
89
  },
72
90
  "./rules/no-offscale-transform": {
91
+ "types": "./types/rule.d.ts",
73
92
  "require": "./src/rules/no-offscale-transform/index.js",
74
93
  "import": "./src/rules/no-offscale-transform/index.mjs"
75
94
  },
76
95
  "./rules/use-motion-scale": {
96
+ "types": "./types/rule.d.ts",
77
97
  "require": "./src/rules/use-motion-scale/index.js",
78
98
  "import": "./src/rules/use-motion-scale/index.mjs"
79
99
  },
80
100
  "./eslint": {
101
+ "types": "./types/eslint.d.ts",
81
102
  "require": "./src/eslint/index.js",
82
103
  "import": "./src/eslint/index.mjs"
83
104
  }
84
105
  },
85
106
  "files": [
107
+ "examples",
86
108
  "scales",
87
109
  "schemas",
88
110
  "src",
111
+ "types",
89
112
  "README.md",
90
113
  "CHANGELOG.md",
91
114
  "CONTRIBUTING.md",
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ AUDIT_JSON_SCHEMA,
5
+ createAuditReport,
6
+ loadAuditConfig,
7
+ toAuditContractReport,
8
+ } = require('../cli/audit');
9
+ const { parseTokenSources } = require('../utils/token-sources');
10
+
11
+ module.exports = {
12
+ AUDIT_JSON_SCHEMA,
13
+ createAuditReport,
14
+ loadAuditConfig,
15
+ parseTokenSources,
16
+ toAuditContractReport,
17
+ };
@@ -0,0 +1,11 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const audit = require('./index.js');
5
+
6
+ export default audit;
7
+ export const AUDIT_JSON_SCHEMA = audit.AUDIT_JSON_SCHEMA;
8
+ export const createAuditReport = audit.createAuditReport;
9
+ export const loadAuditConfig = audit.loadAuditConfig;
10
+ export const parseTokenSources = audit.parseTokenSources;
11
+ export const toAuditContractReport = audit.toAuditContractReport;