stylelint-plugin-rhythmguard 2.0.1 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
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"
@@ -32,6 +32,11 @@
32
32
  "require": "./src/configs/recommended.js",
33
33
  "import": "./src/configs/recommended.mjs"
34
34
  },
35
+ "./configs/embed": {
36
+ "types": "./types/config.d.ts",
37
+ "require": "./src/configs/embed.js",
38
+ "import": "./src/configs/embed.mjs"
39
+ },
35
40
  "./configs/strict": {
36
41
  "types": "./types/config.d.ts",
37
42
  "require": "./src/configs/strict.js",
@@ -118,19 +123,21 @@
118
123
  "scripts": {
119
124
  "bench:perf": "node scripts/bench/compare.mjs",
120
125
  "bench:perf:fix": "node scripts/bench/compare.mjs --fix",
126
+ "bench:quiet": "node scripts/bench/quiet.mjs",
121
127
  "lint": "eslint .",
122
128
  "scales:add": "node scripts/scales/add-scale.mjs",
123
129
  "scales:validate": "node scripts/scales/validate-community-scales.mjs",
124
- "test:pack-smoke": "node scripts/ci/pack-smoke.mjs",
125
- "test:npm-smoke": "node scripts/ci/npm-registry-smoke.mjs --package stylelint-plugin-rhythmguard --version latest",
126
130
  "test": "node --test test/*.test.js",
127
131
  "test:compat-floor": "node --test test/floor-compat.test.js",
132
+ "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js",
133
+ "test:npm-smoke": "node scripts/ci/npm-registry-smoke.mjs --package stylelint-plugin-rhythmguard --version latest",
134
+ "test:pack-smoke": "node scripts/ci/pack-smoke.mjs",
128
135
  "test:watch": "node --test --watch test/*.test.js",
129
- "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js"
136
+ "typecheck": "tsc -p tsconfig.typecheck.json"
130
137
  },
131
138
  "repository": {
132
139
  "type": "git",
133
- "url": "https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard"
140
+ "url": "git+https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard.git"
134
141
  },
135
142
  "homepage": "https://github.com/petrilahdelma/stylelint-plugin-rhythmguard#readme",
136
143
  "bugs": {
@@ -147,13 +154,15 @@
147
154
  },
148
155
  "devDependencies": {
149
156
  "@eslint/js": "^9.22.0",
157
+ "@types/node": "^22.20.1",
150
158
  "c8": "^10.1.3",
151
159
  "eslint": "^9.22.0",
152
160
  "geist": "^1.7.0",
153
161
  "postcss-value-parser": "^4.2.0",
154
162
  "stylelint": "^16.15.0",
163
+ "stylelint-plugin-logical-css": "^2.0.2",
155
164
  "stylelint-scales": "^5.0.0",
156
- "stylelint-plugin-logical-css": "^2.0.2"
165
+ "typescript": "^5.9.3"
157
166
  },
158
167
  "engines": {
159
168
  "node": ">=18.18.0"
@@ -0,0 +1,449 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ normalizeTokenKind,
5
+ normalizeTokenSourceFormat,
6
+ } = require('../utils/token-sources');
7
+ const {
8
+ VALID_FORMATS,
9
+ createDefaultAuditOptions,
10
+ normalizeIgnorePattern,
11
+ } = require('./shared');
12
+
13
+ const HELP = `Usage: rhythmguard audit <dir> [options]
14
+
15
+ Options:
16
+ --format <text|json|json-v1|markdown|html|github> Output format (default: text)
17
+ github = GitHub Actions workflow-command annotations
18
+ --json Alias for --format json
19
+ --markdown Alias for --format markdown
20
+ --schema Print the audit JSON schema and exit
21
+ --output <file> Write json, markdown, or html output to a file
22
+ --config <file> Load audit config (default: .rhythmguardrc.json when present)
23
+ --no-config Ignore .rhythmguardrc.json discovery
24
+ --ignore <pattern> Exclude root-relative path/glob (repeatable, comma-separated)
25
+ --ignore-path <file> Load ignore patterns from file (default: .rhythmguardignore when present)
26
+ --baseline <file> Baseline file path (default: .rhythmguard-baseline.json)
27
+ --write-baseline [file] Write current findings as a baseline
28
+ --since-baseline [file] Compare current findings against a baseline
29
+ --fail-on-new-drift Exit 1 when --since-baseline finds new drift
30
+ --max-findings <number> Exit 1 when total findings exceed this count
31
+ --min-cleanliness <percent> Exit 1 when scale cleanliness is lower than this percent
32
+ --since <git-ref> Scan only changed files since a git ref
33
+ --staged Scan only staged files
34
+ --include-motion Include opt-in motion duration/easing drift
35
+ --token-source <file> External token source (repeatable, comma-separated)
36
+ --token-source-format <format> Token source format: auto, css, flat-json, style-dictionary, dtcg (default: auto)
37
+ --token-kind <kind> Token kind: spacing, radius, typography, size, motion, all (default: spacing)
38
+ --token-candidate-min-count <n> Minimum repeated raw value count for token candidates (default: 2)
39
+ --scale <values|auto> Comma-separated scale values (default: 0,4,8,12,16,24,32);
40
+ auto infers the scale from token sources, then scanned CSS
41
+ --base-font-size <number> px base for rem/em conversion (default: 16)
42
+ `;
43
+
44
+ function parseArgs(argv) {
45
+ const parsed = createDefaultAuditOptions();
46
+
47
+ for (let index = 0; index < argv.length; index++) {
48
+ const arg = argv[index];
49
+
50
+ if (arg === '--help' || arg === '-h') {
51
+ parsed.help = true;
52
+ continue;
53
+ }
54
+
55
+ if (arg === '--schema') {
56
+ parsed.schema = true;
57
+ parsed.format = 'json';
58
+ continue;
59
+ }
60
+
61
+ if (arg === '--json') {
62
+ parsed.format = 'json';
63
+ continue;
64
+ }
65
+
66
+ if (arg === '--markdown') {
67
+ parsed.format = 'markdown';
68
+ continue;
69
+ }
70
+
71
+ if (arg === '--output') {
72
+ parsed.outputPath = parsePathOption(argv[++index], '--output');
73
+ parsed.cliOptions.add('outputPath');
74
+ continue;
75
+ }
76
+
77
+ if (arg.startsWith('--output=')) {
78
+ parsed.outputPath = parsePathOption(arg.slice('--output='.length), '--output');
79
+ parsed.cliOptions.add('outputPath');
80
+ continue;
81
+ }
82
+
83
+ if (arg === '--ignore') {
84
+ parsed.ignorePatterns.push(...parseIgnorePatterns(argv[++index]));
85
+ parsed.cliOptions.add('ignore');
86
+ continue;
87
+ }
88
+
89
+ if (arg.startsWith('--ignore=')) {
90
+ parsed.ignorePatterns.push(...parseIgnorePatterns(arg.slice('--ignore='.length)));
91
+ parsed.cliOptions.add('ignore');
92
+ continue;
93
+ }
94
+
95
+ if (arg === '--config') {
96
+ parsed.configPath = parsePathOption(argv[++index], '--config');
97
+ parsed.configExplicit = true;
98
+ continue;
99
+ }
100
+
101
+ if (arg.startsWith('--config=')) {
102
+ parsed.configPath = parsePathOption(arg.slice('--config='.length), '--config');
103
+ parsed.configExplicit = true;
104
+ continue;
105
+ }
106
+
107
+ if (arg === '--no-config') {
108
+ parsed.noConfig = true;
109
+ continue;
110
+ }
111
+
112
+ if (arg === '--ignore-path') {
113
+ parsed.ignorePath = parsePathOption(argv[++index], '--ignore-path');
114
+ parsed.cliOptions.add('ignorePath');
115
+ continue;
116
+ }
117
+
118
+ if (arg.startsWith('--ignore-path=')) {
119
+ parsed.ignorePath = parsePathOption(arg.slice('--ignore-path='.length), '--ignore-path');
120
+ parsed.cliOptions.add('ignorePath');
121
+ continue;
122
+ }
123
+
124
+ if (arg === '--baseline') {
125
+ parsed.baselinePath = parsePathOption(argv[++index], '--baseline');
126
+ parsed.cliOptions.add('baselinePath');
127
+ continue;
128
+ }
129
+
130
+ if (arg.startsWith('--baseline=')) {
131
+ parsed.baselinePath = parsePathOption(arg.slice('--baseline='.length), '--baseline');
132
+ parsed.cliOptions.add('baselinePath');
133
+ continue;
134
+ }
135
+
136
+ if (arg === '--write-baseline') {
137
+ parsed.writeBaseline = true;
138
+ parsed.cliOptions.add('writeBaseline');
139
+ if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
140
+ parsed.baselinePath = parsePathOption(argv[++index], '--write-baseline');
141
+ parsed.cliOptions.add('baselinePath');
142
+ }
143
+ continue;
144
+ }
145
+
146
+ if (arg.startsWith('--write-baseline=')) {
147
+ parsed.writeBaseline = true;
148
+ parsed.baselinePath = parsePathOption(arg.slice('--write-baseline='.length), '--write-baseline');
149
+ parsed.cliOptions.add('writeBaseline');
150
+ parsed.cliOptions.add('baselinePath');
151
+ continue;
152
+ }
153
+
154
+ if (arg === '--since-baseline') {
155
+ parsed.sinceBaseline = true;
156
+ parsed.cliOptions.add('sinceBaseline');
157
+ if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
158
+ parsed.baselinePath = parsePathOption(argv[++index], '--since-baseline');
159
+ parsed.cliOptions.add('baselinePath');
160
+ }
161
+ continue;
162
+ }
163
+
164
+ if (arg.startsWith('--since-baseline=')) {
165
+ parsed.sinceBaseline = true;
166
+ parsed.baselinePath = parsePathOption(arg.slice('--since-baseline='.length), '--since-baseline');
167
+ parsed.cliOptions.add('sinceBaseline');
168
+ parsed.cliOptions.add('baselinePath');
169
+ continue;
170
+ }
171
+
172
+ if (arg === '--fail-on-new-drift') {
173
+ parsed.failOnNewDrift = true;
174
+ parsed.cliOptions.add('failOnNewDrift');
175
+ continue;
176
+ }
177
+
178
+ if (arg === '--max-findings') {
179
+ parsed.maxFindings = parseNonNegativeInteger(argv[++index], '--max-findings');
180
+ parsed.cliOptions.add('maxFindings');
181
+ continue;
182
+ }
183
+
184
+ if (arg.startsWith('--max-findings=')) {
185
+ parsed.maxFindings = parseNonNegativeInteger(arg.slice('--max-findings='.length), '--max-findings');
186
+ parsed.cliOptions.add('maxFindings');
187
+ continue;
188
+ }
189
+
190
+ if (arg === '--min-cleanliness') {
191
+ parsed.minCleanliness = parsePercentage(argv[++index], '--min-cleanliness');
192
+ parsed.cliOptions.add('minCleanliness');
193
+ continue;
194
+ }
195
+
196
+ if (arg.startsWith('--min-cleanliness=')) {
197
+ parsed.minCleanliness = parsePercentage(
198
+ arg.slice('--min-cleanliness='.length),
199
+ '--min-cleanliness',
200
+ );
201
+ parsed.cliOptions.add('minCleanliness');
202
+ continue;
203
+ }
204
+
205
+ if (arg === '--since') {
206
+ parsed.since = parsePathOption(argv[++index], '--since');
207
+ parsed.cliOptions.add('since');
208
+ continue;
209
+ }
210
+
211
+ if (arg.startsWith('--since=')) {
212
+ parsed.since = parsePathOption(arg.slice('--since='.length), '--since');
213
+ parsed.cliOptions.add('since');
214
+ continue;
215
+ }
216
+
217
+ if (arg === '--staged') {
218
+ parsed.staged = true;
219
+ parsed.cliOptions.add('staged');
220
+ continue;
221
+ }
222
+
223
+ if (arg === '--include-motion') {
224
+ parsed.includeMotion = true;
225
+ parsed.cliOptions.add('includeMotion');
226
+ continue;
227
+ }
228
+
229
+ if (arg === '--token-source') {
230
+ parsed.tokenSources.push(...parseTokenSourcePaths(argv[++index]));
231
+ parsed.cliOptions.add('tokenSources');
232
+ continue;
233
+ }
234
+
235
+ if (arg.startsWith('--token-source=')) {
236
+ parsed.tokenSources.push(...parseTokenSourcePaths(arg.slice('--token-source='.length)));
237
+ parsed.cliOptions.add('tokenSources');
238
+ continue;
239
+ }
240
+
241
+ if (arg === '--token-source-format') {
242
+ parsed.tokenSourceFormat = normalizeTokenSourceFormat(argv[++index]);
243
+ parsed.cliOptions.add('tokenSourceFormat');
244
+ continue;
245
+ }
246
+
247
+ if (arg.startsWith('--token-source-format=')) {
248
+ parsed.tokenSourceFormat = normalizeTokenSourceFormat(arg.slice('--token-source-format='.length));
249
+ parsed.cliOptions.add('tokenSourceFormat');
250
+ continue;
251
+ }
252
+
253
+ if (arg === '--token-kind') {
254
+ parsed.tokenKind = normalizeTokenKind(argv[++index]);
255
+ parsed.cliOptions.add('tokenKind');
256
+ continue;
257
+ }
258
+
259
+ if (arg.startsWith('--token-kind=')) {
260
+ parsed.tokenKind = normalizeTokenKind(arg.slice('--token-kind='.length));
261
+ parsed.cliOptions.add('tokenKind');
262
+ continue;
263
+ }
264
+
265
+ if (arg === '--token-candidate-min-count') {
266
+ parsed.tokenCandidateMinCount = parsePositiveInteger(argv[++index], '--token-candidate-min-count');
267
+ parsed.cliOptions.add('tokenCandidateMinCount');
268
+ continue;
269
+ }
270
+
271
+ if (arg.startsWith('--token-candidate-min-count=')) {
272
+ parsed.tokenCandidateMinCount = parsePositiveInteger(
273
+ arg.slice('--token-candidate-min-count='.length),
274
+ '--token-candidate-min-count',
275
+ );
276
+ parsed.cliOptions.add('tokenCandidateMinCount');
277
+ continue;
278
+ }
279
+
280
+ if (arg === '--format') {
281
+ parsed.format = String(argv[++index] || '').toLowerCase();
282
+ continue;
283
+ }
284
+
285
+ if (arg.startsWith('--format=')) {
286
+ parsed.format = arg.slice('--format='.length).toLowerCase();
287
+ continue;
288
+ }
289
+
290
+ if (arg === '--scale') {
291
+ parsed.scale = parseScale(argv[++index]);
292
+ parsed.cliOptions.add('scale');
293
+ continue;
294
+ }
295
+
296
+ if (arg.startsWith('--scale=')) {
297
+ parsed.scale = parseScale(arg.slice('--scale='.length));
298
+ parsed.cliOptions.add('scale');
299
+ continue;
300
+ }
301
+
302
+ if (arg === '--base-font-size') {
303
+ parsed.baseFontSize = parseBaseFontSize(argv[++index]);
304
+ parsed.cliOptions.add('baseFontSize');
305
+ continue;
306
+ }
307
+
308
+ if (arg.startsWith('--base-font-size=')) {
309
+ parsed.baseFontSize = parseBaseFontSize(arg.slice('--base-font-size='.length));
310
+ parsed.cliOptions.add('baseFontSize');
311
+ continue;
312
+ }
313
+
314
+ if (!arg.startsWith('-') && !parsed.dir) {
315
+ parsed.dir = arg;
316
+ continue;
317
+ }
318
+
319
+ throw new Error(`Unknown option: ${arg}`);
320
+ }
321
+
322
+ if (!VALID_FORMATS.has(parsed.format)) {
323
+ throw new Error(`Invalid format "${parsed.format}". Expected text, json, json-v1, markdown, html, or github.`);
324
+ }
325
+
326
+ if (parsed.since && parsed.staged) {
327
+ throw new Error('Use either --since or --staged, not both.');
328
+ }
329
+
330
+ if (parsed.failOnNewDrift && !parsed.sinceBaseline) {
331
+ throw new Error('--fail-on-new-drift requires --since-baseline.');
332
+ }
333
+
334
+ return parsed;
335
+ }
336
+
337
+ function parsePathOption(raw, optionName) {
338
+ if (!raw) {
339
+ throw new Error(`Missing value for ${optionName}.`);
340
+ }
341
+
342
+ return String(raw);
343
+ }
344
+
345
+ function parseNonNegativeInteger(raw, optionName) {
346
+ const value = Number(raw);
347
+ if (!Number.isInteger(value) || value < 0) {
348
+ throw new Error(`${optionName} must be a non-negative integer.`);
349
+ }
350
+
351
+ return value;
352
+ }
353
+
354
+ function parsePositiveInteger(raw, optionName) {
355
+ const value = Number(raw);
356
+ if (!Number.isInteger(value) || value <= 0) {
357
+ throw new Error(`${optionName} must be a positive integer.`);
358
+ }
359
+
360
+ return value;
361
+ }
362
+
363
+ function parsePercentage(raw, optionName) {
364
+ const value = Number(raw);
365
+ if (!Number.isFinite(value) || value < 0 || value > 100) {
366
+ throw new Error(`${optionName} must be a number between 0 and 100.`);
367
+ }
368
+
369
+ return value;
370
+ }
371
+
372
+ function parseIgnorePatterns(raw) {
373
+ if (!raw) {
374
+ throw new Error('Missing value for --ignore.');
375
+ }
376
+
377
+ const patterns = String(raw).split(',')
378
+ .map((pattern) => normalizeIgnorePattern(pattern))
379
+ .filter(Boolean);
380
+
381
+ if (patterns.length === 0) {
382
+ throw new Error('--ignore must include at least one pattern.');
383
+ }
384
+
385
+ return patterns;
386
+ }
387
+
388
+ function parseTokenSourcePaths(raw) {
389
+ if (!raw) {
390
+ throw new Error('Missing value for --token-source.');
391
+ }
392
+
393
+ const sources = String(raw).split(',')
394
+ .map((sourcePath) => sourcePath.trim())
395
+ .filter(Boolean);
396
+
397
+ if (sources.length === 0) {
398
+ throw new Error('--token-source must include at least one file.');
399
+ }
400
+
401
+ return sources;
402
+ }
403
+
404
+
405
+ function parseScale(raw) {
406
+ if (!raw) {
407
+ throw new Error('Missing value for --scale.');
408
+ }
409
+
410
+ if (raw.trim().toLowerCase() === 'auto') {
411
+ return 'auto';
412
+ }
413
+
414
+ const scale = raw.split(',')
415
+ .map((part) => part.trim())
416
+ .filter(Boolean)
417
+ .map((part) => {
418
+ const numeric = Number(part);
419
+ return Number.isFinite(numeric) && !/[a-z%]/i.test(part) ? numeric : part;
420
+ });
421
+
422
+ if (scale.length === 0) {
423
+ throw new Error('Scale must include at least one value.');
424
+ }
425
+
426
+ return scale;
427
+ }
428
+
429
+ function parseBaseFontSize(raw) {
430
+ const value = Number(raw);
431
+ if (!Number.isFinite(value) || value <= 0) {
432
+ throw new Error('--base-font-size must be a positive number.');
433
+ }
434
+
435
+ return value;
436
+ }
437
+
438
+ module.exports = {
439
+ HELP,
440
+ parseArgs,
441
+ parseBaseFontSize,
442
+ parseIgnorePatterns,
443
+ parseNonNegativeInteger,
444
+ parsePathOption,
445
+ parsePercentage,
446
+ parsePositiveInteger,
447
+ parseScale,
448
+ parseTokenSourcePaths,
449
+ };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { formatPath } = require('./shared');
6
+
7
+ function applyBaselineComparison(report, baselinePath) {
8
+ const resolvedPath = path.resolve(process.cwd(), baselinePath);
9
+ if (!fs.existsSync(resolvedPath)) {
10
+ throw new Error(`Baseline file not found: ${baselinePath}`);
11
+ }
12
+
13
+ const baseline = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
14
+ const baselineFindings = Array.isArray(baseline.findings) ? baseline.findings : [];
15
+ const baselineKeys = new Set(baselineFindings.map((finding) => finding.key || createFindingKey(finding)));
16
+ const currentFindings = getAllFindings(report);
17
+ const currentKeys = new Set(currentFindings.map(createFindingKey));
18
+ const newFindings = currentFindings.filter((finding) => !baselineKeys.has(createFindingKey(finding)));
19
+ const resolvedFindings = baselineFindings.filter((finding) => !currentKeys.has(finding.key || createFindingKey(finding)));
20
+
21
+ report.baseline = {
22
+ baselineFindings: baselineFindings.length,
23
+ file: formatPath(resolvedPath),
24
+ newFindings,
25
+ newFindingsCount: newFindings.length,
26
+ resolvedFindingsCount: resolvedFindings.length,
27
+ };
28
+ report.summary.newFindings = newFindings.length;
29
+ report.summary.resolvedFindings = resolvedFindings.length;
30
+ }
31
+
32
+ function writeBaseline(report, baselinePath) {
33
+ const resolvedPath = path.resolve(process.cwd(), baselinePath);
34
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
35
+ fs.writeFileSync(
36
+ resolvedPath,
37
+ `${JSON.stringify({
38
+ createdAt: new Date().toISOString(),
39
+ directory: report.directory,
40
+ findings: getAllFindings(report).map(toBaselineFinding),
41
+ formatVersion: 1,
42
+ summary: {
43
+ scaleCleanliness: report.scaleCleanliness,
44
+ totalFindings: report.totalWarnings,
45
+ },
46
+ }, null, 2)}\n`,
47
+ );
48
+
49
+ report.baselineWritten = {
50
+ file: formatPath(resolvedPath),
51
+ findings: report.totalWarnings,
52
+ };
53
+ }
54
+
55
+ function getAllFindings(report) {
56
+ return [
57
+ ...report.findings.css,
58
+ ...report.findings.motion,
59
+ ...report.findings.tailwind,
60
+ ];
61
+ }
62
+
63
+ function toBaselineFinding(finding) {
64
+ return {
65
+ column: finding.column,
66
+ file: finding.file,
67
+ key: createFindingKey(finding),
68
+ line: finding.line,
69
+ rule: finding.rule,
70
+ text: finding.text,
71
+ token: finding.token,
72
+ type: finding.type,
73
+ value: finding.value || finding.rawValue,
74
+ };
75
+ }
76
+
77
+ function createFindingKey(finding) {
78
+ return [
79
+ finding.rule || '',
80
+ finding.type || '',
81
+ finding.file || '',
82
+ finding.line || '',
83
+ finding.column || '',
84
+ finding.value || finding.rawValue || finding.token || '',
85
+ finding.text || '',
86
+ ].join('\u001f');
87
+ }
88
+
89
+ function getAuditFailures(report, parsed) {
90
+ const failures = [];
91
+
92
+ if (parsed.maxFindings !== null && report.totalWarnings > parsed.maxFindings) {
93
+ failures.push(`total findings ${report.totalWarnings} exceeds --max-findings ${parsed.maxFindings}`);
94
+ }
95
+
96
+ if (parsed.minCleanliness !== null && report.scaleCleanliness < parsed.minCleanliness) {
97
+ failures.push(`scale cleanliness ${report.scaleCleanliness}% is below --min-cleanliness ${parsed.minCleanliness}%`);
98
+ }
99
+
100
+ if (parsed.failOnNewDrift && report.baseline && report.baseline.newFindingsCount > 0) {
101
+ failures.push(`new drift found: ${report.baseline.newFindingsCount} finding(s) not present in baseline`);
102
+ }
103
+
104
+ return failures;
105
+ }
106
+
107
+ module.exports = {
108
+ applyBaselineComparison,
109
+ createFindingKey,
110
+ getAllFindings,
111
+ getAuditFailures,
112
+ toBaselineFinding,
113
+ writeBaseline,
114
+ };