@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.cjs CHANGED
@@ -185,9 +185,11 @@ function lowestCommonAncestor(paths) {
185
185
  }
186
186
 
187
187
  // src/sampling.ts
188
+ var import_debug = __toESM(require("debug"), 1);
188
189
  var import_fast_glob = __toESM(require("fast-glob"), 1);
189
- var import_picomatch2 = __toESM(require("picomatch"), 1);
190
+ var debugSampling = (0, import_debug.default)("eslint-config-snapshot:sampling");
190
191
  async function sampleWorkspaceFiles(workspaceAbs, config) {
192
+ const startedAt = Date.now();
191
193
  const all = await (0, import_fast_glob.default)(config.includeGlobs, {
192
194
  cwd: workspaceAbs,
193
195
  ignore: config.excludeGlobs,
@@ -196,45 +198,46 @@ async function sampleWorkspaceFiles(workspaceAbs, config) {
196
198
  unique: true
197
199
  });
198
200
  const normalized = sortUnique(all.map((entry) => normalizePath(entry)));
201
+ debugSampling("workspace=%s candidates=%d", workspaceAbs, normalized.length);
199
202
  if (normalized.length === 0) {
200
203
  return [];
201
204
  }
202
205
  if (normalized.length <= config.maxFilesPerWorkspace) {
206
+ debugSampling("workspace=%s using all files=%d elapsedMs=%d", workspaceAbs, normalized.length, Date.now() - startedAt);
203
207
  return normalized;
204
208
  }
205
- if (config.hintGlobs.length === 0) {
206
- return selectDistributed(normalized, config.maxFilesPerWorkspace);
207
- }
208
- const hinted = normalized.filter((entry) => config.hintGlobs.some((pattern) => (0, import_picomatch2.default)(pattern, { dot: true })(entry)));
209
- const notHinted = normalized.filter((entry) => !hinted.includes(entry));
210
- return selectDistributed([...hinted, ...notHinted], config.maxFilesPerWorkspace);
211
- }
212
- function selectDistributed(files, count) {
209
+ const selected = selectDistributed(normalized, config.maxFilesPerWorkspace, config.tokenHints);
210
+ debugSampling(
211
+ "workspace=%s selected=%d mode=token-distributed elapsedMs=%d files=%o",
212
+ workspaceAbs,
213
+ selected.length,
214
+ Date.now() - startedAt,
215
+ selected
216
+ );
217
+ return selected;
218
+ }
219
+ function selectDistributed(files, count, tokenHints) {
213
220
  if (files.length <= count) {
214
221
  return files;
215
222
  }
223
+ const tokenPriorityMap = createTokenPriorityMap(tokenHints);
216
224
  const selected = [];
217
225
  const selectedSet = /* @__PURE__ */ new Set();
218
- const tokenSeen = /* @__PURE__ */ new Set();
219
- for (const file of files) {
220
- if (selected.length >= count) {
221
- break;
222
- }
223
- const token = getPrimaryToken(file);
224
- if (!token || tokenSeen.has(token)) {
225
- continue;
226
- }
227
- tokenSeen.add(token);
228
- selected.push(file);
229
- selectedSet.add(file);
230
- }
226
+ const preferred = files.filter((file) => isPreferredForLintSampling(file));
227
+ const nonPreferred = files.filter((file) => !isPreferredForLintSampling(file));
228
+ appendTokenRepresentatives(preferred, tokenPriorityMap, selected, selectedSet, count);
229
+ appendTokenRepresentatives(nonPreferred, tokenPriorityMap, selected, selectedSet, count);
231
230
  if (selected.length >= count) {
232
231
  return sortUnique(selected).slice(0, count);
233
232
  }
234
233
  const remaining = files.filter((file) => !selectedSet.has(file));
235
234
  const needed = count - selected.length;
236
- const spaced = pickUniformly(remaining, needed);
237
- return sortUnique([...selected, ...spaced]).slice(0, count);
235
+ const preferredRemaining = remaining.filter((file) => isPreferredForLintSampling(file));
236
+ const nonPreferredRemaining = remaining.filter((file) => !isPreferredForLintSampling(file));
237
+ const preferredPicked = pickUniformly(preferredRemaining, needed);
238
+ const afterPreferredNeed = needed - preferredPicked.length;
239
+ const fallbackPicked = afterPreferredNeed > 0 ? pickUniformly(nonPreferredRemaining, afterPreferredNeed) : [];
240
+ return sortUnique([...selected, ...preferredPicked, ...fallbackPicked]).slice(0, count);
238
241
  }
239
242
  function pickUniformly(files, count) {
240
243
  if (count <= 0 || files.length === 0) {
@@ -248,14 +251,60 @@ function pickUniformly(files, count) {
248
251
  }
249
252
  const picked = [];
250
253
  const usedIndices = /* @__PURE__ */ new Set();
251
- for (let index = 0; index < count; index += 1) {
252
- const raw = Math.round(index * (files.length - 1) / (count - 1));
253
- const safeIndex = nextFreeIndex(raw, usedIndices, files.length);
254
+ if (count >= 3) {
255
+ const anchorIndices = [0, Math.floor((files.length - 1) / 2), files.length - 1];
256
+ for (const anchorIndex of anchorIndices) {
257
+ if (picked.length >= count || usedIndices.has(anchorIndex)) {
258
+ continue;
259
+ }
260
+ usedIndices.add(anchorIndex);
261
+ const anchored = files[anchorIndex];
262
+ if (anchored !== void 0) {
263
+ picked.push(anchored);
264
+ }
265
+ }
266
+ }
267
+ for (const candidate of buildDistributedCandidates(files.length, count)) {
268
+ if (picked.length >= count) {
269
+ break;
270
+ }
271
+ const safeIndex = nextFreeIndex(candidate, usedIndices, files.length);
272
+ if (usedIndices.has(safeIndex)) {
273
+ continue;
274
+ }
254
275
  usedIndices.add(safeIndex);
255
- picked.push(files[safeIndex]);
276
+ const selected = files[safeIndex];
277
+ if (selected !== void 0) {
278
+ picked.push(selected);
279
+ }
280
+ }
281
+ if (picked.length < count) {
282
+ for (let index = 0; index < files.length && picked.length < count; index += 1) {
283
+ if (usedIndices.has(index)) {
284
+ continue;
285
+ }
286
+ usedIndices.add(index);
287
+ const fallback = files[index];
288
+ if (fallback !== void 0) {
289
+ picked.push(fallback);
290
+ }
291
+ }
256
292
  }
257
293
  return picked;
258
294
  }
295
+ function buildDistributedCandidates(length, count) {
296
+ if (length <= 0 || count <= 0) {
297
+ return [];
298
+ }
299
+ if (count === 1) {
300
+ return [0];
301
+ }
302
+ const candidates = [];
303
+ for (let index = 0; index < count; index += 1) {
304
+ candidates.push(Math.round(index * (length - 1) / (count - 1)));
305
+ }
306
+ return candidates;
307
+ }
259
308
  function nextFreeIndex(candidate, used, max) {
260
309
  if (!used.has(candidate)) {
261
310
  return candidate;
@@ -272,25 +321,257 @@ function nextFreeIndex(candidate, used, max) {
272
321
  }
273
322
  return candidate;
274
323
  }
275
- function getPrimaryToken(file) {
276
- const parts = file.split("/");
277
- const basename = parts.slice(-1)[0];
278
- if (!basename) {
324
+ function getPrimaryToken(file, tokenPriorityMap) {
325
+ const parts = file.split("/").filter((entry) => entry.length > 0);
326
+ if (parts.length === 0) {
327
+ return null;
328
+ }
329
+ const basename = parts[parts.length - 1];
330
+ if (basename === void 0) {
279
331
  return null;
280
332
  }
281
- const nameOnly = basename.replace(/\.[^.]+$/u, "");
282
- const expanded = nameOnly.replaceAll(/([a-z])([A-Z])/gu, "$1 $2").replaceAll(/[_\-.]+/gu, " ").toLowerCase();
283
- const token = expanded.split(/\s+/u).find((entry) => entry.length > 1 && !GENERIC_TOKENS.has(entry));
284
- return token ?? null;
333
+ const basenameTokens = tokenizePathPart(basename, true);
334
+ const directoryTokensForward = parts.slice(0, -1).flatMap((entry) => tokenizePathPart(entry, false));
335
+ const directoryTokens = [];
336
+ for (let index = directoryTokensForward.length - 1; index >= 0; index -= 1) {
337
+ const token = directoryTokensForward[index];
338
+ if (token !== void 0) {
339
+ directoryTokens.push(token);
340
+ }
341
+ }
342
+ const allTokens = [...basenameTokens, ...directoryTokens].filter((entry) => entry.length > 1);
343
+ const bestKnownToken = pickBestKnownToken(allTokens, tokenPriorityMap);
344
+ if (bestKnownToken !== null) {
345
+ return bestKnownToken;
346
+ }
347
+ const fallback = allTokens.find((entry) => !GENERIC_TOKENS.has(entry));
348
+ return fallback ?? null;
349
+ }
350
+ function tokenizePathPart(part, stripExtension) {
351
+ const normalized = stripExtension ? part.replace(/\.[^.]+$/u, "") : part;
352
+ const expanded = normalized.replaceAll(/([a-z])([A-Z])/gu, "$1 $2").replaceAll(/[_\-.]+/gu, " ").toLowerCase();
353
+ return expanded.split(/\s+/u).filter((entry) => entry.length > 0);
354
+ }
355
+ function pickBestKnownToken(tokens, tokenPriorityMap) {
356
+ let bestToken = null;
357
+ let bestGroupPriority = Number.POSITIVE_INFINITY;
358
+ for (const token of tokens) {
359
+ const normalizedToken = normalizeToken(token);
360
+ const groupPriority = tokenPriorityMap.get(normalizedToken);
361
+ if (groupPriority === void 0) {
362
+ continue;
363
+ }
364
+ if (groupPriority < bestGroupPriority) {
365
+ bestGroupPriority = groupPriority;
366
+ bestToken = normalizedToken;
367
+ }
368
+ }
369
+ return bestToken;
370
+ }
371
+ function normalizeToken(token) {
372
+ if (token.endsWith("ies") && token.length > 3) {
373
+ return `${token.slice(0, -3)}y`;
374
+ }
375
+ if (token.endsWith("s") && token.length > 3) {
376
+ return token.slice(0, -1);
377
+ }
378
+ return token;
379
+ }
380
+ function isPreferredForLintSampling(file) {
381
+ return CODE_PREFERRED_EXTENSIONS.has(getExtension(file));
382
+ }
383
+ function getExtension(file) {
384
+ const lastDot = file.lastIndexOf(".");
385
+ if (lastDot === -1 || lastDot === file.length - 1) {
386
+ return "";
387
+ }
388
+ return file.slice(lastDot + 1).toLowerCase();
285
389
  }
286
390
  var GENERIC_TOKENS = /* @__PURE__ */ new Set(["src", "index", "main", "test", "spec", "package", "packages", "lib", "dist"]);
391
+ var CODE_PREFERRED_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "cjs", "mjs"]);
392
+ var DEFAULT_TOKEN_HINT_GROUPS = [
393
+ [
394
+ "chunk",
395
+ "conf",
396
+ "config",
397
+ "container",
398
+ "controller",
399
+ "helpers",
400
+ "mock",
401
+ "mocks",
402
+ "presentation",
403
+ "repository",
404
+ "route",
405
+ "routes",
406
+ "schema",
407
+ "setup",
408
+ "spec",
409
+ "stories",
410
+ "style",
411
+ "styles",
412
+ "test",
413
+ "type",
414
+ "types",
415
+ "utils",
416
+ "view",
417
+ "views"
418
+ ],
419
+ [
420
+ "adapter",
421
+ "api",
422
+ "apis",
423
+ "builder",
424
+ "client",
425
+ "component",
426
+ "components",
427
+ "constants",
428
+ "context",
429
+ "core",
430
+ "dto",
431
+ "entity",
432
+ "entry",
433
+ "env",
434
+ "factory",
435
+ "fetcher",
436
+ "handler",
437
+ "hook",
438
+ "hooks",
439
+ "init",
440
+ "integration",
441
+ "interceptor",
442
+ "interface",
443
+ "layout",
444
+ "layouts",
445
+ "listener",
446
+ "logger",
447
+ "manager",
448
+ "mapper",
449
+ "meta",
450
+ "middleware",
451
+ "model",
452
+ "module",
453
+ "normalizer",
454
+ "options",
455
+ "page",
456
+ "pages",
457
+ "parser",
458
+ "plugin",
459
+ "provider",
460
+ "registry",
461
+ "resolver",
462
+ "router",
463
+ "runtime",
464
+ "serializer",
465
+ "server",
466
+ "service",
467
+ "settings",
468
+ "shared",
469
+ "slice",
470
+ "state",
471
+ "store",
472
+ "subscriber",
473
+ "theme",
474
+ "tracker",
475
+ "transform",
476
+ "unit",
477
+ "validator"
478
+ ],
479
+ [
480
+ "base",
481
+ "bundle",
482
+ "common",
483
+ "compiler",
484
+ "contract",
485
+ "definition",
486
+ "definitions",
487
+ "deserializer",
488
+ "event",
489
+ "events",
490
+ "fixture",
491
+ "fixtures",
492
+ "guard",
493
+ "internal",
494
+ "loader",
495
+ "publisher",
496
+ "reducer",
497
+ "stub",
498
+ "stubs",
499
+ "tests",
500
+ "util"
501
+ ]
502
+ ];
503
+ function createTokenPriorityMap(input) {
504
+ const groups = normalizeTokenHintGroups(input);
505
+ const entries = [];
506
+ for (const [index, group] of groups.entries()) {
507
+ entries.push(...toPriorityEntries(group, index + 1));
508
+ }
509
+ return new Map(entries);
510
+ }
511
+ function normalizeTokenHintGroups(input) {
512
+ if (!input || input.length === 0) {
513
+ return DEFAULT_TOKEN_HINT_GROUPS.map((group) => [...group]);
514
+ }
515
+ if (Array.isArray(input[0])) {
516
+ const nested = input;
517
+ return nested.map((group) => group.filter((token) => token.trim().length > 0));
518
+ }
519
+ const flat = input;
520
+ return [flat.filter((token) => token.trim().length > 0)];
521
+ }
522
+ function toPriorityEntries(tokens, priority) {
523
+ return tokens.map((token) => [normalizeToken(token), priority]);
524
+ }
525
+ function appendTokenRepresentatives(files, tokenPriorityMap, selected, selectedSet, count) {
526
+ if (selected.length >= count || files.length === 0) {
527
+ return;
528
+ }
529
+ const tokenToFiles = /* @__PURE__ */ new Map();
530
+ const tokenFirstIndex = /* @__PURE__ */ new Map();
531
+ for (const [index, file] of files.entries()) {
532
+ const token = getPrimaryToken(file, tokenPriorityMap);
533
+ if (!token) {
534
+ continue;
535
+ }
536
+ tokenFirstIndex.set(token, Math.min(tokenFirstIndex.get(token) ?? Number.POSITIVE_INFINITY, index));
537
+ const current = tokenToFiles.get(token) ?? [];
538
+ current.push(file);
539
+ tokenToFiles.set(token, current);
540
+ }
541
+ const orderedTokens = [...tokenToFiles.keys()].sort((left, right) => {
542
+ const leftPriority = tokenPriorityMap.get(left) ?? Number.POSITIVE_INFINITY;
543
+ const rightPriority = tokenPriorityMap.get(right) ?? Number.POSITIVE_INFINITY;
544
+ if (leftPriority !== rightPriority) {
545
+ return leftPriority - rightPriority;
546
+ }
547
+ const leftIndex = tokenFirstIndex.get(left) ?? Number.POSITIVE_INFINITY;
548
+ const rightIndex = tokenFirstIndex.get(right) ?? Number.POSITIVE_INFINITY;
549
+ if (leftIndex !== rightIndex) {
550
+ return leftIndex - rightIndex;
551
+ }
552
+ return left.localeCompare(right);
553
+ });
554
+ for (const token of orderedTokens) {
555
+ if (selected.length >= count) {
556
+ break;
557
+ }
558
+ const firstFile = tokenToFiles.get(token)?.[0];
559
+ if (!firstFile || selectedSet.has(firstFile)) {
560
+ continue;
561
+ }
562
+ selected.push(firstFile);
563
+ selectedSet.add(firstFile);
564
+ }
565
+ }
287
566
 
288
567
  // src/extract.ts
568
+ var import_debug2 = __toESM(require("debug"), 1);
289
569
  var import_node_child_process = require("child_process");
290
570
  var import_node_fs = require("fs");
291
571
  var import_node_module = require("module");
292
572
  var import_node_path2 = __toESM(require("path"), 1);
293
573
  var import_node_url = require("url");
574
+ var debugExtract = (0, import_debug2.default)("eslint-config-snapshot:extract");
294
575
  function resolveEslintBinForWorkspace(workspaceAbs) {
295
576
  const anchor = import_node_path2.default.join(workspaceAbs, "__snapshot_anchor__.cjs");
296
577
  const req = (0, import_node_module.createRequire)(anchor);
@@ -337,11 +618,16 @@ function findPackageRoot(entryAbs) {
337
618
  }
338
619
  function extractRulesFromPrintConfig(workspaceAbs, fileAbs) {
339
620
  const eslintBin = resolveEslintBinForWorkspace(workspaceAbs);
621
+ const commandArgs = [eslintBin, "--print-config", fileAbs];
622
+ const startedAt = Date.now();
623
+ debugExtract("spawn: cwd=%s cmd=%s %o", workspaceAbs, process.execPath, commandArgs);
340
624
  const proc = (0, import_node_child_process.spawnSync)(process.execPath, [eslintBin, "--print-config", fileAbs], {
341
625
  cwd: workspaceAbs,
342
626
  encoding: "utf8"
343
627
  });
628
+ debugExtract("spawn: done status=%s elapsedMs=%d", String(proc.status), Date.now() - startedAt);
344
629
  if (proc.status !== 0) {
630
+ debugExtract("spawn: stderr=%s", proc.stderr.trim());
345
631
  throw new Error(`Failed to run eslint --print-config for ${fileAbs}`);
346
632
  }
347
633
  const stdout = proc.stdout.trim();
@@ -371,6 +657,7 @@ function resolveEslintVersionForWorkspace(workspaceAbs) {
371
657
  return "unknown";
372
658
  }
373
659
  async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
660
+ debugExtract("workspace=%s sampleCount=%d", workspaceAbs, fileAbsList.length);
374
661
  const evaluate = await createWorkspaceEvaluator(workspaceAbs);
375
662
  const results = [];
376
663
  for (const fileAbs of fileAbsList) {
@@ -379,9 +666,16 @@ async function extractRulesForWorkspaceSamples(workspaceAbs, fileAbsList) {
379
666
  results.push({ fileAbs, rules });
380
667
  } catch (error) {
381
668
  const normalizedError = error instanceof Error ? error : new Error(String(error));
669
+ debugExtract("extract failed: workspace=%s file=%s error=%s", workspaceAbs, fileAbs, normalizedError.message);
382
670
  results.push({ fileAbs, error: normalizedError });
383
671
  }
384
672
  }
673
+ debugExtract(
674
+ "workspace=%s extracted=%d failed=%d",
675
+ workspaceAbs,
676
+ results.filter((entry) => entry.rules !== void 0).length,
677
+ results.filter((entry) => entry.error !== void 0).length
678
+ );
385
679
  return results;
386
680
  }
387
681
  async function createWorkspaceEvaluator(workspaceAbs) {
@@ -392,6 +686,7 @@ async function createWorkspaceEvaluator(workspaceAbs) {
392
686
  const eslintModule = await import((0, import_node_url.pathToFileURL)(eslintModuleEntry).href);
393
687
  const ESLintClass = eslintModule.ESLint ?? eslintModule.default?.ESLint;
394
688
  if (ESLintClass) {
689
+ debugExtract("workspace=%s evaluator=eslint-api", workspaceAbs);
395
690
  const eslint = new ESLintClass({ cwd: workspaceAbs });
396
691
  return async (fileAbs) => {
397
692
  const config = await eslint.calculateConfigForFile(fileAbs);
@@ -403,6 +698,7 @@ async function createWorkspaceEvaluator(workspaceAbs) {
403
698
  }
404
699
  } catch {
405
700
  }
701
+ debugExtract("workspace=%s evaluator=spawn-print-config", workspaceAbs);
406
702
  return (fileAbs) => Promise.resolve(extractRulesFromPrintConfig(workspaceAbs, fileAbs));
407
703
  }
408
704
  function normalizeRules(rules) {
@@ -437,45 +733,27 @@ function aggregateRules(ruleMaps) {
437
733
  const aggregated = /* @__PURE__ */ new Map();
438
734
  for (const rules of ruleMaps) {
439
735
  for (const [ruleName, nextEntry] of rules.entries()) {
440
- const currentEntry = aggregated.get(ruleName);
441
- if (!currentEntry) {
442
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
443
- continue;
444
- }
445
- const severityCmp = compareSeverity(nextEntry[0], currentEntry[0]);
446
- if (severityCmp > 0) {
447
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
448
- continue;
449
- }
450
- if (severityCmp < 0) {
451
- continue;
452
- }
453
- const currentOptions = currentEntry.length > 1 ? canonicalizeJson(currentEntry[1]) : void 0;
454
- const nextOptions = nextEntry.length > 1 ? canonicalizeJson(nextEntry[1]) : void 0;
455
- if (currentOptions === void 0 && nextOptions !== void 0) {
456
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
457
- continue;
458
- }
459
- if (currentOptions !== void 0 && nextOptions === void 0) {
460
- continue;
461
- }
462
- if (currentOptions === void 0 && nextOptions === void 0) {
463
- continue;
464
- }
465
- const currentJson = JSON.stringify(currentOptions);
466
- const nextJson = JSON.stringify(nextOptions);
467
- if (nextJson < currentJson) {
468
- aggregated.set(ruleName, canonicalizeJson(nextEntry));
469
- }
736
+ const normalizedEntry = canonicalizeJson(nextEntry);
737
+ const variantKey = toVariantKey(normalizedEntry);
738
+ const variants = aggregated.get(ruleName) ?? /* @__PURE__ */ new Map();
739
+ variants.set(variantKey, normalizedEntry);
740
+ aggregated.set(ruleName, variants);
470
741
  }
471
742
  }
472
- return new Map([...aggregated.entries()].sort(([a], [b]) => a.localeCompare(b)));
743
+ const entries = [...aggregated.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ruleName, variants]) => {
744
+ const sortedVariants = [...variants.values()].sort(compareVariants);
745
+ if (sortedVariants.length === 1) {
746
+ return [ruleName, sortedVariants[0]];
747
+ }
748
+ return [ruleName, sortedVariants];
749
+ });
750
+ return new Map(entries);
473
751
  }
474
752
  function buildSnapshot(groupId, workspaces, rules) {
475
753
  const sortedRules = [...rules.entries()].sort(([a], [b]) => a.localeCompare(b));
476
754
  const rulesObject = {};
477
755
  for (const [name, config] of sortedRules) {
478
- rulesObject[name] = canonicalizeJson(config);
756
+ rulesObject[name] = isSingleRuleEntry(config) ? canonicalizeJson(config) : config.map((variant) => canonicalizeJson(variant)).sort(compareVariants);
479
757
  }
480
758
  return {
481
759
  formatVersion: 1,
@@ -497,6 +775,17 @@ async function readSnapshotFile(fileAbs) {
497
775
  const raw = await (0, import_promises.readFile)(fileAbs, "utf8");
498
776
  return JSON.parse(raw);
499
777
  }
778
+ function toVariantKey(entry) {
779
+ return JSON.stringify(canonicalizeJson(entry));
780
+ }
781
+ function compareVariants(a, b) {
782
+ const aJson = JSON.stringify(canonicalizeJson(a));
783
+ const bJson = JSON.stringify(canonicalizeJson(b));
784
+ return aJson.localeCompare(bJson);
785
+ }
786
+ function isSingleRuleEntry(entry) {
787
+ return !Array.isArray(entry[0]);
788
+ }
500
789
 
501
790
  // src/diff.ts
502
791
  function diffSnapshots(before, after) {
@@ -511,32 +800,51 @@ function diffSnapshots(before, after) {
511
800
  for (const name of beforeNames.filter((entry) => afterNames.includes(entry))) {
512
801
  const oldEntry = beforeRules[name];
513
802
  const newEntry = afterRules[name];
514
- if (oldEntry[0] !== newEntry[0]) {
803
+ const oldVariants = toVariants(oldEntry);
804
+ const newVariants = toVariants(newEntry);
805
+ const oldSeverity = summarizeSeveritySet(oldVariants);
806
+ const newSeverity = summarizeSeveritySet(newVariants);
807
+ if (oldSeverity !== newSeverity) {
515
808
  severityChanges.push({
516
809
  rule: name,
517
- before: oldEntry[0],
518
- after: newEntry[0]
810
+ before: oldSeverity,
811
+ after: newSeverity
519
812
  });
520
813
  }
521
- const oldOptions = oldEntry.length > 1 ? canonicalizeJson(oldEntry[1]) : void 0;
522
- const newOptions = newEntry.length > 1 ? canonicalizeJson(newEntry[1]) : void 0;
523
- if (oldEntry[0] === "off" || newEntry[0] === "off") {
524
- if (oldEntry[0] === "off" && newEntry[0] === "off") {
525
- if (oldOptions !== void 0 && newOptions === void 0) {
814
+ const oldSerialized = JSON.stringify(oldVariants);
815
+ const newSerialized = JSON.stringify(newVariants);
816
+ if (oldSerialized === newSerialized) {
817
+ continue;
818
+ }
819
+ const oldIsOnlyOff = oldVariants.every((entry) => entry[0] === "off");
820
+ const newIsOnlyOff = newVariants.every((entry) => entry[0] === "off");
821
+ if (oldIsOnlyOff || newIsOnlyOff) {
822
+ if (oldIsOnlyOff && newIsOnlyOff) {
823
+ const oldHasOptions = oldVariants.some((variant) => variant.length > 1);
824
+ const newHasOptions = newVariants.some((variant) => variant.length > 1);
825
+ if (oldHasOptions && !newHasOptions) {
526
826
  removedRules.push(name);
527
- } else if (oldOptions === void 0 && newOptions !== void 0) {
827
+ } else if (!oldHasOptions && newHasOptions) {
528
828
  introducedRules.push(name);
829
+ } else if (oldVariants.length > newVariants.length) {
830
+ removedRules.push(name);
831
+ } else if (oldVariants.length < newVariants.length) {
832
+ introducedRules.push(name);
833
+ } else {
834
+ optionChanges.push({
835
+ rule: name,
836
+ before: oldVariants,
837
+ after: newVariants
838
+ });
529
839
  }
530
840
  }
531
841
  continue;
532
842
  }
533
- if (JSON.stringify(oldOptions) !== JSON.stringify(newOptions)) {
534
- optionChanges.push({
535
- rule: name,
536
- before: oldOptions,
537
- after: newOptions
538
- });
539
- }
843
+ optionChanges.push({
844
+ rule: name,
845
+ before: oldVariants,
846
+ after: newVariants
847
+ });
540
848
  }
541
849
  const beforeWorkspaces = sortUnique(before.workspaces);
542
850
  const afterWorkspaces = sortUnique(after.workspaces);
@@ -551,6 +859,17 @@ function diffSnapshots(before, after) {
551
859
  }
552
860
  };
553
861
  }
862
+ function toVariants(entry) {
863
+ if (!Array.isArray(entry[0])) {
864
+ return [canonicalizeJson(entry)];
865
+ }
866
+ return entry.map((variant) => canonicalizeJson(variant));
867
+ }
868
+ function summarizeSeveritySet(variants) {
869
+ const severityOrder = ["error", "warn", "off"];
870
+ const severities = new Set(variants.map((variant) => variant[0]));
871
+ return severityOrder.filter((severity) => severities.has(severity)).join("|");
872
+ }
554
873
  function hasDiff(diff) {
555
874
  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;
556
875
  }
@@ -565,10 +884,9 @@ var DEFAULT_CONFIG = {
565
884
  groups: [{ name: "default", match: ["**/*"] }]
566
885
  },
567
886
  sampling: {
568
- maxFilesPerWorkspace: 8,
569
- includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
570
- excludeGlobs: ["**/node_modules/**", "**/dist/**"],
571
- hintGlobs: []
887
+ maxFilesPerWorkspace: 10,
888
+ includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs,md,mdx,json,css}"],
889
+ excludeGlobs: ["**/node_modules/**", "**/dist/**"]
572
890
  }
573
891
  };
574
892
  var SPEC_SEARCH_PLACES = [
@@ -643,10 +961,35 @@ function getConfigScaffold(preset = "minimal") {
643
961
  groups: [{ name: 'default', match: ['**/*'] }]
644
962
  },
645
963
  sampling: {
646
- maxFilesPerWorkspace: 8,
647
- includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
964
+ maxFilesPerWorkspace: 10,
965
+ includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs,md,mdx,json,css}'],
648
966
  excludeGlobs: ['**/node_modules/**', '**/dist/**'],
649
- hintGlobs: []
967
+ tokenHints: [
968
+ 'chunk',
969
+ 'conf',
970
+ 'config',
971
+ 'container',
972
+ 'controller',
973
+ 'helpers',
974
+ 'mock',
975
+ 'mocks',
976
+ 'presentation',
977
+ 'repository',
978
+ 'route',
979
+ 'routes',
980
+ 'schema',
981
+ 'setup',
982
+ 'spec',
983
+ 'stories',
984
+ 'style',
985
+ 'styles',
986
+ 'test',
987
+ 'type',
988
+ 'types',
989
+ 'utils',
990
+ 'view',
991
+ 'views'
992
+ ]
650
993
  }
651
994
  }
652
995
  `;