@eslint-config-snapshot/api 0.9.0 → 0.14.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/dist/index.js CHANGED
@@ -128,9 +128,11 @@ function lowestCommonAncestor(paths) {
128
128
  }
129
129
 
130
130
  // src/sampling.ts
131
+ import createDebug from "debug";
131
132
  import fg from "fast-glob";
132
- import picomatch2 from "picomatch";
133
+ var debugSampling = createDebug("eslint-config-snapshot:sampling");
133
134
  async function sampleWorkspaceFiles(workspaceAbs, config) {
135
+ const startedAt = Date.now();
134
136
  const all = await fg(config.includeGlobs, {
135
137
  cwd: workspaceAbs,
136
138
  ignore: config.excludeGlobs,
@@ -139,45 +141,46 @@ async function sampleWorkspaceFiles(workspaceAbs, config) {
139
141
  unique: true
140
142
  });
141
143
  const normalized = sortUnique(all.map((entry) => normalizePath(entry)));
144
+ debugSampling("workspace=%s candidates=%d", workspaceAbs, normalized.length);
142
145
  if (normalized.length === 0) {
143
146
  return [];
144
147
  }
145
148
  if (normalized.length <= config.maxFilesPerWorkspace) {
149
+ debugSampling("workspace=%s using all files=%d elapsedMs=%d", workspaceAbs, normalized.length, Date.now() - startedAt);
146
150
  return normalized;
147
151
  }
148
- if (config.hintGlobs.length === 0) {
149
- return selectDistributed(normalized, config.maxFilesPerWorkspace);
150
- }
151
- const hinted = normalized.filter((entry) => config.hintGlobs.some((pattern) => picomatch2(pattern, { dot: true })(entry)));
152
- const notHinted = normalized.filter((entry) => !hinted.includes(entry));
153
- return selectDistributed([...hinted, ...notHinted], config.maxFilesPerWorkspace);
154
- }
155
- function selectDistributed(files, count) {
152
+ const selected = selectDistributed(normalized, config.maxFilesPerWorkspace, config.tokenHints);
153
+ debugSampling(
154
+ "workspace=%s selected=%d mode=token-distributed elapsedMs=%d files=%o",
155
+ workspaceAbs,
156
+ selected.length,
157
+ Date.now() - startedAt,
158
+ selected
159
+ );
160
+ return selected;
161
+ }
162
+ function selectDistributed(files, count, tokenHints) {
156
163
  if (files.length <= count) {
157
164
  return files;
158
165
  }
166
+ const tokenPriorityMap = createTokenPriorityMap(tokenHints);
159
167
  const selected = [];
160
168
  const selectedSet = /* @__PURE__ */ new Set();
161
- const tokenSeen = /* @__PURE__ */ new Set();
162
- for (const file of files) {
163
- if (selected.length >= count) {
164
- break;
165
- }
166
- const token = getPrimaryToken(file);
167
- if (!token || tokenSeen.has(token)) {
168
- continue;
169
- }
170
- tokenSeen.add(token);
171
- selected.push(file);
172
- selectedSet.add(file);
173
- }
169
+ const preferred = files.filter((file) => isPreferredForLintSampling(file));
170
+ const nonPreferred = files.filter((file) => !isPreferredForLintSampling(file));
171
+ appendTokenRepresentatives(preferred, tokenPriorityMap, selected, selectedSet, count);
172
+ appendTokenRepresentatives(nonPreferred, tokenPriorityMap, selected, selectedSet, count);
174
173
  if (selected.length >= count) {
175
174
  return sortUnique(selected).slice(0, count);
176
175
  }
177
176
  const remaining = files.filter((file) => !selectedSet.has(file));
178
177
  const needed = count - selected.length;
179
- const spaced = pickUniformly(remaining, needed);
180
- return sortUnique([...selected, ...spaced]).slice(0, count);
178
+ const preferredRemaining = remaining.filter((file) => isPreferredForLintSampling(file));
179
+ const nonPreferredRemaining = remaining.filter((file) => !isPreferredForLintSampling(file));
180
+ const preferredPicked = pickUniformly(preferredRemaining, needed);
181
+ const afterPreferredNeed = needed - preferredPicked.length;
182
+ const fallbackPicked = afterPreferredNeed > 0 ? pickUniformly(nonPreferredRemaining, afterPreferredNeed) : [];
183
+ return sortUnique([...selected, ...preferredPicked, ...fallbackPicked]).slice(0, count);
181
184
  }
182
185
  function pickUniformly(files, count) {
183
186
  if (count <= 0 || files.length === 0) {
@@ -191,14 +194,60 @@ function pickUniformly(files, count) {
191
194
  }
192
195
  const picked = [];
193
196
  const usedIndices = /* @__PURE__ */ new Set();
194
- for (let index = 0; index < count; index += 1) {
195
- const raw = Math.round(index * (files.length - 1) / (count - 1));
196
- const safeIndex = nextFreeIndex(raw, usedIndices, files.length);
197
+ if (count >= 3) {
198
+ const anchorIndices = [0, Math.floor((files.length - 1) / 2), files.length - 1];
199
+ for (const anchorIndex of anchorIndices) {
200
+ if (picked.length >= count || usedIndices.has(anchorIndex)) {
201
+ continue;
202
+ }
203
+ usedIndices.add(anchorIndex);
204
+ const anchored = files[anchorIndex];
205
+ if (anchored !== void 0) {
206
+ picked.push(anchored);
207
+ }
208
+ }
209
+ }
210
+ for (const candidate of buildDistributedCandidates(files.length, count)) {
211
+ if (picked.length >= count) {
212
+ break;
213
+ }
214
+ const safeIndex = nextFreeIndex(candidate, usedIndices, files.length);
215
+ if (usedIndices.has(safeIndex)) {
216
+ continue;
217
+ }
197
218
  usedIndices.add(safeIndex);
198
- picked.push(files[safeIndex]);
219
+ const selected = files[safeIndex];
220
+ if (selected !== void 0) {
221
+ picked.push(selected);
222
+ }
223
+ }
224
+ if (picked.length < count) {
225
+ for (let index = 0; index < files.length && picked.length < count; index += 1) {
226
+ if (usedIndices.has(index)) {
227
+ continue;
228
+ }
229
+ usedIndices.add(index);
230
+ const fallback = files[index];
231
+ if (fallback !== void 0) {
232
+ picked.push(fallback);
233
+ }
234
+ }
199
235
  }
200
236
  return picked;
201
237
  }
238
+ function buildDistributedCandidates(length, count) {
239
+ if (length <= 0 || count <= 0) {
240
+ return [];
241
+ }
242
+ if (count === 1) {
243
+ return [0];
244
+ }
245
+ const candidates = [];
246
+ for (let index = 0; index < count; index += 1) {
247
+ candidates.push(Math.round(index * (length - 1) / (count - 1)));
248
+ }
249
+ return candidates;
250
+ }
202
251
  function nextFreeIndex(candidate, used, max) {
203
252
  if (!used.has(candidate)) {
204
253
  return candidate;
@@ -215,25 +264,257 @@ function nextFreeIndex(candidate, used, max) {
215
264
  }
216
265
  return candidate;
217
266
  }
218
- function getPrimaryToken(file) {
219
- const parts = file.split("/");
220
- const basename = parts.slice(-1)[0];
221
- if (!basename) {
267
+ function getPrimaryToken(file, tokenPriorityMap) {
268
+ const parts = file.split("/").filter((entry) => entry.length > 0);
269
+ if (parts.length === 0) {
270
+ return null;
271
+ }
272
+ const basename = parts[parts.length - 1];
273
+ if (basename === void 0) {
222
274
  return null;
223
275
  }
224
- const nameOnly = basename.replace(/\.[^.]+$/u, "");
225
- const expanded = nameOnly.replaceAll(/([a-z])([A-Z])/gu, "$1 $2").replaceAll(/[_\-.]+/gu, " ").toLowerCase();
226
- const token = expanded.split(/\s+/u).find((entry) => entry.length > 1 && !GENERIC_TOKENS.has(entry));
227
- return token ?? null;
276
+ const basenameTokens = tokenizePathPart(basename, true);
277
+ const directoryTokensForward = parts.slice(0, -1).flatMap((entry) => tokenizePathPart(entry, false));
278
+ const directoryTokens = [];
279
+ for (let index = directoryTokensForward.length - 1; index >= 0; index -= 1) {
280
+ const token = directoryTokensForward[index];
281
+ if (token !== void 0) {
282
+ directoryTokens.push(token);
283
+ }
284
+ }
285
+ const allTokens = [...basenameTokens, ...directoryTokens].filter((entry) => entry.length > 1);
286
+ const bestKnownToken = pickBestKnownToken(allTokens, tokenPriorityMap);
287
+ if (bestKnownToken !== null) {
288
+ return bestKnownToken;
289
+ }
290
+ const fallback = allTokens.find((entry) => !GENERIC_TOKENS.has(entry));
291
+ return fallback ?? null;
292
+ }
293
+ function tokenizePathPart(part, stripExtension) {
294
+ const normalized = stripExtension ? part.replace(/\.[^.]+$/u, "") : part;
295
+ const expanded = normalized.replaceAll(/([a-z])([A-Z])/gu, "$1 $2").replaceAll(/[_\-.]+/gu, " ").toLowerCase();
296
+ return expanded.split(/\s+/u).filter((entry) => entry.length > 0);
297
+ }
298
+ function pickBestKnownToken(tokens, tokenPriorityMap) {
299
+ let bestToken = null;
300
+ let bestGroupPriority = Number.POSITIVE_INFINITY;
301
+ for (const token of tokens) {
302
+ const normalizedToken = normalizeToken(token);
303
+ const groupPriority = tokenPriorityMap.get(normalizedToken);
304
+ if (groupPriority === void 0) {
305
+ continue;
306
+ }
307
+ if (groupPriority < bestGroupPriority) {
308
+ bestGroupPriority = groupPriority;
309
+ bestToken = normalizedToken;
310
+ }
311
+ }
312
+ return bestToken;
313
+ }
314
+ function normalizeToken(token) {
315
+ if (token.endsWith("ies") && token.length > 3) {
316
+ return `${token.slice(0, -3)}y`;
317
+ }
318
+ if (token.endsWith("s") && token.length > 3) {
319
+ return token.slice(0, -1);
320
+ }
321
+ return token;
322
+ }
323
+ function isPreferredForLintSampling(file) {
324
+ return CODE_PREFERRED_EXTENSIONS.has(getExtension(file));
325
+ }
326
+ function getExtension(file) {
327
+ const lastDot = file.lastIndexOf(".");
328
+ if (lastDot === -1 || lastDot === file.length - 1) {
329
+ return "";
330
+ }
331
+ return file.slice(lastDot + 1).toLowerCase();
228
332
  }
229
333
  var GENERIC_TOKENS = /* @__PURE__ */ new Set(["src", "index", "main", "test", "spec", "package", "packages", "lib", "dist"]);
334
+ var CODE_PREFERRED_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "cjs", "mjs"]);
335
+ var DEFAULT_TOKEN_HINT_GROUPS = [
336
+ [
337
+ "chunk",
338
+ "conf",
339
+ "config",
340
+ "container",
341
+ "controller",
342
+ "helpers",
343
+ "mock",
344
+ "mocks",
345
+ "presentation",
346
+ "repository",
347
+ "route",
348
+ "routes",
349
+ "schema",
350
+ "setup",
351
+ "spec",
352
+ "stories",
353
+ "style",
354
+ "styles",
355
+ "test",
356
+ "type",
357
+ "types",
358
+ "utils",
359
+ "view",
360
+ "views"
361
+ ],
362
+ [
363
+ "adapter",
364
+ "api",
365
+ "apis",
366
+ "builder",
367
+ "client",
368
+ "component",
369
+ "components",
370
+ "constants",
371
+ "context",
372
+ "core",
373
+ "dto",
374
+ "entity",
375
+ "entry",
376
+ "env",
377
+ "factory",
378
+ "fetcher",
379
+ "handler",
380
+ "hook",
381
+ "hooks",
382
+ "init",
383
+ "integration",
384
+ "interceptor",
385
+ "interface",
386
+ "layout",
387
+ "layouts",
388
+ "listener",
389
+ "logger",
390
+ "manager",
391
+ "mapper",
392
+ "meta",
393
+ "middleware",
394
+ "model",
395
+ "module",
396
+ "normalizer",
397
+ "options",
398
+ "page",
399
+ "pages",
400
+ "parser",
401
+ "plugin",
402
+ "provider",
403
+ "registry",
404
+ "resolver",
405
+ "router",
406
+ "runtime",
407
+ "serializer",
408
+ "server",
409
+ "service",
410
+ "settings",
411
+ "shared",
412
+ "slice",
413
+ "state",
414
+ "store",
415
+ "subscriber",
416
+ "theme",
417
+ "tracker",
418
+ "transform",
419
+ "unit",
420
+ "validator"
421
+ ],
422
+ [
423
+ "base",
424
+ "bundle",
425
+ "common",
426
+ "compiler",
427
+ "contract",
428
+ "definition",
429
+ "definitions",
430
+ "deserializer",
431
+ "event",
432
+ "events",
433
+ "fixture",
434
+ "fixtures",
435
+ "guard",
436
+ "internal",
437
+ "loader",
438
+ "publisher",
439
+ "reducer",
440
+ "stub",
441
+ "stubs",
442
+ "tests",
443
+ "util"
444
+ ]
445
+ ];
446
+ function createTokenPriorityMap(input) {
447
+ const groups = normalizeTokenHintGroups(input);
448
+ const entries = [];
449
+ for (const [index, group] of groups.entries()) {
450
+ entries.push(...toPriorityEntries(group, index + 1));
451
+ }
452
+ return new Map(entries);
453
+ }
454
+ function normalizeTokenHintGroups(input) {
455
+ if (!input || input.length === 0) {
456
+ return DEFAULT_TOKEN_HINT_GROUPS.map((group) => [...group]);
457
+ }
458
+ if (Array.isArray(input[0])) {
459
+ const nested = input;
460
+ return nested.map((group) => group.filter((token) => token.trim().length > 0));
461
+ }
462
+ const flat = input;
463
+ return [flat.filter((token) => token.trim().length > 0)];
464
+ }
465
+ function toPriorityEntries(tokens, priority) {
466
+ return tokens.map((token) => [normalizeToken(token), priority]);
467
+ }
468
+ function appendTokenRepresentatives(files, tokenPriorityMap, selected, selectedSet, count) {
469
+ if (selected.length >= count || files.length === 0) {
470
+ return;
471
+ }
472
+ const tokenToFiles = /* @__PURE__ */ new Map();
473
+ const tokenFirstIndex = /* @__PURE__ */ new Map();
474
+ for (const [index, file] of files.entries()) {
475
+ const token = getPrimaryToken(file, tokenPriorityMap);
476
+ if (!token) {
477
+ continue;
478
+ }
479
+ tokenFirstIndex.set(token, Math.min(tokenFirstIndex.get(token) ?? Number.POSITIVE_INFINITY, index));
480
+ const current = tokenToFiles.get(token) ?? [];
481
+ current.push(file);
482
+ tokenToFiles.set(token, current);
483
+ }
484
+ const orderedTokens = [...tokenToFiles.keys()].sort((left, right) => {
485
+ const leftPriority = tokenPriorityMap.get(left) ?? Number.POSITIVE_INFINITY;
486
+ const rightPriority = tokenPriorityMap.get(right) ?? Number.POSITIVE_INFINITY;
487
+ if (leftPriority !== rightPriority) {
488
+ return leftPriority - rightPriority;
489
+ }
490
+ const leftIndex = tokenFirstIndex.get(left) ?? Number.POSITIVE_INFINITY;
491
+ const rightIndex = tokenFirstIndex.get(right) ?? Number.POSITIVE_INFINITY;
492
+ if (leftIndex !== rightIndex) {
493
+ return leftIndex - rightIndex;
494
+ }
495
+ return left.localeCompare(right);
496
+ });
497
+ for (const token of orderedTokens) {
498
+ if (selected.length >= count) {
499
+ break;
500
+ }
501
+ const firstFile = tokenToFiles.get(token)?.[0];
502
+ if (!firstFile || selectedSet.has(firstFile)) {
503
+ continue;
504
+ }
505
+ selected.push(firstFile);
506
+ selectedSet.add(firstFile);
507
+ }
508
+ }
230
509
 
231
510
  // src/extract.ts
511
+ import createDebug2 from "debug";
232
512
  import { spawnSync } from "child_process";
233
513
  import { existsSync, readFileSync } from "fs";
234
514
  import { createRequire } from "module";
235
515
  import path2 from "path";
236
516
  import { pathToFileURL } from "url";
517
+ var debugExtract = createDebug2("eslint-config-snapshot:extract");
237
518
  function resolveEslintBinForWorkspace(workspaceAbs) {
238
519
  const anchor = path2.join(workspaceAbs, "__snapshot_anchor__.cjs");
239
520
  const req = createRequire(anchor);
@@ -280,11 +561,16 @@ function findPackageRoot(entryAbs) {
280
561
  }
281
562
  function extractRulesFromPrintConfig(workspaceAbs, fileAbs) {
282
563
  const eslintBin = resolveEslintBinForWorkspace(workspaceAbs);
564
+ const commandArgs = [eslintBin, "--print-config", fileAbs];
565
+ const startedAt = Date.now();
566
+ debugExtract("spawn: cwd=%s cmd=%s %o", workspaceAbs, process.execPath, commandArgs);
283
567
  const proc = spawnSync(process.execPath, [eslintBin, "--print-config", fileAbs], {
284
568
  cwd: workspaceAbs,
285
569
  encoding: "utf8"
286
570
  });
571
+ debugExtract("spawn: done status=%s elapsedMs=%d", String(proc.status), Date.now() - startedAt);
287
572
  if (proc.status !== 0) {
573
+ debugExtract("spawn: stderr=%s", proc.stderr.trim());
288
574
  throw new Error(`Failed to run eslint --print-config for ${fileAbs}`);
289
575
  }
290
576
  const stdout = proc.stdout.trim();
@@ -314,6 +600,7 @@ function resolveEslintVersionForWorkspace(workspaceAbs) {
314
600
  return "unknown";
315
601
  }
316
602
  async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
603
+ debugExtract("workspace=%s sampleCount=%d", workspaceAbs, fileAbsList.length);
317
604
  const evaluate = await createWorkspaceEvaluator(workspaceAbs);
318
605
  const results = [];
319
606
  for (const fileAbs of fileAbsList) {
@@ -322,9 +609,16 @@ async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
322
609
  results.push({ fileAbs, rules });
323
610
  } catch (error) {
324
611
  const normalizedError = error instanceof Error ? error : new Error(String(error));
612
+ debugExtract("extract failed: workspace=%s file=%s error=%s", workspaceAbs, fileAbs, normalizedError.message);
325
613
  results.push({ fileAbs, error: normalizedError });
326
614
  }
327
615
  }
616
+ debugExtract(
617
+ "workspace=%s extracted=%d failed=%d",
618
+ workspaceAbs,
619
+ results.filter((entry) => entry.rules !== void 0).length,
620
+ results.filter((entry) => entry.error !== void 0).length
621
+ );
328
622
  return results;
329
623
  }
330
624
  async function createWorkspaceEvaluator(workspaceAbs) {
@@ -335,6 +629,7 @@ async function createWorkspaceEvaluator(workspaceAbs) {
335
629
  const eslintModule = await import(pathToFileURL(eslintModuleEntry).href);
336
630
  const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint;
337
631
  if (ESLintClass) {
632
+ debugExtract("workspace=%s evaluator=eslint-api", workspaceAbs);
338
633
  const eslint = new ESLintClass({ cwd: workspaceAbs });
339
634
  return async (fileAbs) => {
340
635
  const config = await eslint.calculateConfigForFile(fileAbs);
@@ -346,6 +641,7 @@ async function createWorkspaceEvaluator(workspaceAbs) {
346
641
  }
347
642
  } catch {
348
643
  }
644
+ debugExtract("workspace=%s evaluator=spawn-print-config", workspaceAbs);
349
645
  return (fileAbs) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs));
350
646
  }
351
647
  function normalizeRules(rules) {
@@ -380,45 +676,27 @@ function aggregateRules(ruleMaps) {
380
676
  const aggregated = /* @__PURE__ */ new Map();
381
677
  for (const rules of ruleMaps) {
382
678
  for (const [ruleName, nextEntry] of rules.entries()) {
383
- const currentEntry = aggregated.get(ruleName);
384
- if (!currentEntry) {
385
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
386
- continue;
387
- }
388
- const severityCmp = compareSeverity(nextEntry[0], currentEntry[0]);
389
- if (severityCmp > 0) {
390
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
391
- continue;
392
- }
393
- if (severityCmp < 0) {
394
- continue;
395
- }
396
- const currentOptions = currentEntry.length > 1 ? canonicalizeJson(currentEntry[1]) : void 0;
397
- const nextOptions = nextEntry.length > 1 ? canonicalizeJson(nextEntry[1]) : void 0;
398
- if (currentOptions === void 0 && nextOptions !== void 0) {
399
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
400
- continue;
401
- }
402
- if (currentOptions !== void 0 && nextOptions === void 0) {
403
- continue;
404
- }
405
- if (currentOptions === void 0 && nextOptions === void 0) {
406
- continue;
407
- }
408
- const currentJson = JSON.stringify(currentOptions);
409
- const nextJson = JSON.stringify(nextOptions);
410
- if (nextJson < currentJson) {
411
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
412
- }
679
+ const normalizedEntry = canonicalizeJson(nextEntry);
680
+ const variantKey = toVariantKey(normalizedEntry);
681
+ const variants = aggregated.get(ruleName) ?? /* @__PURE__ */ new Map();
682
+ variants.set(variantKey, normalizedEntry);
683
+ aggregated.set(ruleName, variants);
413
684
  }
414
685
  }
415
- return new Map([...aggregated.entries()].sort(([a], [b]) => a.localeCompare(b)));
686
+ const entries = [...aggregated.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ruleName, variants]) => {
687
+ const sortedVariants = [...variants.values()].sort(compareVariants);
688
+ if (sortedVariants.length === 1) {
689
+ return [ruleName, sortedVariants[0]];
690
+ }
691
+ return [ruleName, sortedVariants];
692
+ });
693
+ return new Map(entries);
416
694
  }
417
695
  function buildSnapshot(groupId, workspaces, rules) {
418
696
  const sortedRules = [...rules.entries()].sort(([a], [b]) => a.localeCompare(b));
419
697
  const rulesObject = {};
420
698
  for (const [name, config] of sortedRules) {
421
- rulesObject[name] = canonicalizeJson(config);
699
+ rulesObject[name] = isSingleRuleEntry(config) ? canonicalizeJson(config) : config.map((variant) => canonicalizeJson(variant)).sort(compareVariants);
422
700
  }
423
701
  return {
424
702
  formatVersion: 1,
@@ -440,6 +718,17 @@ async function readSnapshotFile(fileAbs) {
440
718
  const raw = await readFile(fileAbs, "utf8");
441
719
  return JSON.parse(raw);
442
720
  }
721
+ function toVariantKey(entry) {
722
+ return JSON.stringify(canonicalizeJson(entry));
723
+ }
724
+ function compareVariants(a, b) {
725
+ const aJson = JSON.stringify(canonicalizeJson(a));
726
+ const bJson = JSON.stringify(canonicalizeJson(b));
727
+ return aJson.localeCompare(bJson);
728
+ }
729
+ function isSingleRuleEntry(entry) {
730
+ return !Array.isArray(entry[0]);
731
+ }
443
732
 
444
733
  // src/diff.ts
445
734
  function diffSnapshots(before, after) {
@@ -454,32 +743,51 @@ function diffSnapshots(before, after) {
454
743
  for (const name of beforeNames.filter((entry) => afterNames.includes(entry))) {
455
744
  const oldEntry = beforeRules[name];
456
745
  const newEntry = afterRules[name];
457
- if (oldEntry[0] !== newEntry[0]) {
746
+ const oldVariants = toVariants(oldEntry);
747
+ const newVariants = toVariants(newEntry);
748
+ const oldSeverity = summarizeSeveritySet(oldVariants);
749
+ const newSeverity = summarizeSeveritySet(newVariants);
750
+ if (oldSeverity !== newSeverity) {
458
751
  severityChanges.push({
459
752
  rule: name,
460
- before: oldEntry[0],
461
- after: newEntry[0]
753
+ before: oldSeverity,
754
+ after: newSeverity
462
755
  });
463
756
  }
464
- const oldOptions = oldEntry.length > 1 ? canonicalizeJson(oldEntry[1]) : void 0;
465
- const newOptions = newEntry.length > 1 ? canonicalizeJson(newEntry[1]) : void 0;
466
- if (oldEntry[0] === "off" || newEntry[0] === "off") {
467
- if (oldEntry[0] === "off" && newEntry[0] === "off") {
468
- if (oldOptions !== void 0 && newOptions === void 0) {
757
+ const oldSerialized = JSON.stringify(oldVariants);
758
+ const newSerialized = JSON.stringify(newVariants);
759
+ if (oldSerialized === newSerialized) {
760
+ continue;
761
+ }
762
+ const oldIsOnlyOff = oldVariants.every((entry) => entry[0] === "off");
763
+ const newIsOnlyOff = newVariants.every((entry) => entry[0] === "off");
764
+ if (oldIsOnlyOff || newIsOnlyOff) {
765
+ if (oldIsOnlyOff && newIsOnlyOff) {
766
+ const oldHasOptions = oldVariants.some((variant) => variant.length > 1);
767
+ const newHasOptions = newVariants.some((variant) => variant.length > 1);
768
+ if (oldHasOptions && !newHasOptions) {
469
769
  removedRules.push(name);
470
- } else if (oldOptions === void 0 && newOptions !== void 0) {
770
+ } else if (!oldHasOptions && newHasOptions) {
471
771
  introducedRules.push(name);
772
+ } else if (oldVariants.length > newVariants.length) {
773
+ removedRules.push(name);
774
+ } else if (oldVariants.length < newVariants.length) {
775
+ introducedRules.push(name);
776
+ } else {
777
+ optionChanges.push({
778
+ rule: name,
779
+ before: oldVariants,
780
+ after: newVariants
781
+ });
472
782
  }
473
783
  }
474
784
  continue;
475
785
  }
476
- if (JSON.stringify(oldOptions) !== JSON.stringify(newOptions)) {
477
- optionChanges.push({
478
- rule: name,
479
- before: oldOptions,
480
- after: newOptions
481
- });
482
- }
786
+ optionChanges.push({
787
+ rule: name,
788
+ before: oldVariants,
789
+ after: newVariants
790
+ });
483
791
  }
484
792
  const beforeWorkspaces = sortUnique(before.workspaces);
485
793
  const afterWorkspaces = sortUnique(after.workspaces);
@@ -494,6 +802,17 @@ function diffSnapshots(before, after) {
494
802
  }
495
803
  };
496
804
  }
805
+ function toVariants(entry) {
806
+ if (!Array.isArray(entry[0])) {
807
+ return [canonicalizeJson(entry)];
808
+ }
809
+ return entry.map((variant) => canonicalizeJson(variant));
810
+ }
811
+ function summarizeSeveritySet(variants) {
812
+ const severityOrder = ["error", "warn", "off"];
813
+ const severities = new Set(variants.map((variant) => variant[0]));
814
+ return severityOrder.filter((severity) => severities.has(severity)).join("|");
815
+ }
497
816
  function hasDiff(diff) {
498
817
  return diff.introducedRules.length > 0 || diff.removedRules.length > 0 || diff.severityChanges.length > 0 || diff.optionChanges.length > 0 || diff.workspaceMembershipChanges.added.length > 0 || diff.workspaceMembershipChanges.removed.length > 0;
499
818
  }
@@ -508,10 +827,9 @@ var DEFAULT_CONFIG = {
508
827
  groups: [{ name: "default", match: ["**/*"] }]
509
828
  },
510
829
  sampling: {
511
- maxFilesPerWorkspace: 8,
512
- includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
513
- excludeGlobs: ["**/node_modules/**", "**/dist/**"],
514
- hintGlobs: []
830
+ maxFilesPerWorkspace: 10,
831
+ includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs,md,mdx,json,css}"],
832
+ excludeGlobs: ["**/node_modules/**", "**/dist/**"]
515
833
  }
516
834
  };
517
835
  var SPEC_SEARCH_PLACES = [
@@ -586,10 +904,35 @@ function getConfigScaffold(preset = "minimal") {
586
904
  groups: [{ name: 'default', match: ['**/*'] }]
587
905
  },
588
906
  sampling: {
589
- maxFilesPerWorkspace: 8,
590
- includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
907
+ maxFilesPerWorkspace: 10,
908
+ includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs,md,mdx,json,css}'],
591
909
  excludeGlobs: ['**/node_modules/**', '**/dist/**'],
592
- hintGlobs: []
910
+ tokenHints: [
911
+ 'chunk',
912
+ 'conf',
913
+ 'config',
914
+ 'container',
915
+ 'controller',
916
+ 'helpers',
917
+ 'mock',
918
+ 'mocks',
919
+ 'presentation',
920
+ 'repository',
921
+ 'route',
922
+ 'routes',
923
+ 'schema',
924
+ 'setup',
925
+ 'spec',
926
+ 'stories',
927
+ 'style',
928
+ 'styles',
929
+ 'test',
930
+ 'type',
931
+ 'types',
932
+ 'utils',
933
+ 'view',
934
+ 'views'
935
+ ]
593
936
  }
594
937
  }
595
938
  `;