@sarj/eslint-plugin 2.12.0 → 2.12.2

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
@@ -37,6 +37,31 @@ module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/rules/enforce-file-structure.ts
39
39
  var import_utils = require("@typescript-eslint/utils");
40
+
41
+ // src/rules/_paths.ts
42
+ var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
43
+ var STORY_FILE_RE = /\.stories\.[cm]?[jt]sx?$/i;
44
+ var GENERATED_FILE_RE = /([\\/](?:generated|openapi-gen|graphql[\\/]types)[\\/])|(\.gen\.[cm]?[jt]sx?$)|(\.generated\.[cm]?[jt]sx?$)|(\.d\.[cm]?ts$)|(\.types\.[cm]?ts$)/;
45
+ var GENERATED_MARKER_RE = /(?:@generated\b|generated (?:with|by)|generated (?:graphql )?types|do not edit(?: directly| manually)?)/i;
46
+ function isTestFile(filename) {
47
+ const normalized = filename.replaceAll("\\", "/");
48
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
49
+ if (/\.(test|spec|e2e|integration)\.[cm]?[jt]sx?$/.test(base)) {
50
+ return true;
51
+ }
52
+ return /(^|\/)(tests?|__tests__|__mocks__|fixtures|e2e|integration)\//.test(normalized);
53
+ }
54
+ function isStoryFile(filename) {
55
+ return STORY_FILE_RE.test(filename);
56
+ }
57
+ function isGeneratedFile(filename, sourceText = "") {
58
+ return GENERATED_FILE_RE.test(filename.replaceAll("\\", "/")) || GENERATED_MARKER_RE.test(sourceText.slice(0, 2048));
59
+ }
60
+ function isScriptFile(filename) {
61
+ return SCRIPT_FILE_RE.test(filename);
62
+ }
63
+
64
+ // src/rules/enforce-file-structure.ts
40
65
  var classifyStatement = (statement) => {
41
66
  switch (statement.type) {
42
67
  case import_utils.AST_NODE_TYPES.ImportDeclaration:
@@ -73,6 +98,9 @@ var enforce_file_structure_default = import_utils.ESLintUtils.RuleCreator(
73
98
  },
74
99
  defaultOptions: [],
75
100
  create(context) {
101
+ if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
102
+ return {};
103
+ }
76
104
  return {
77
105
  Program(node) {
78
106
  const body = node.body;
@@ -459,6 +487,9 @@ var no_comment_cruft_default = import_utils3.ESLintUtils.RuleCreator(
459
487
  },
460
488
  defaultOptions: [],
461
489
  create(context) {
490
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) {
491
+ return {};
492
+ }
462
493
  const sourceCode = context.sourceCode;
463
494
  function isStandalone(comment) {
464
495
  const before = sourceCode.getTokenBefore(comment, {
@@ -531,6 +562,7 @@ var no_comment_cruft_default = import_utils3.ESLintUtils.RuleCreator(
531
562
  var import_utils4 = require("@typescript-eslint/utils");
532
563
  var DEFAULT_IGNORE_PATTERNS = [
533
564
  /[\\/]generated[\\/]/,
565
+ /[\\/]openapi-gen[\\/]/,
534
566
  /\.gen\.tsx?$/,
535
567
  /\.generated\.tsx?$/
536
568
  ];
@@ -582,7 +614,7 @@ var no_enum_default = import_utils4.ESLintUtils.RuleCreator(
582
614
  (re) => re.test(filename)
583
615
  );
584
616
  const isIgnoredByOption = ignoreFiles.length > 0 && matchesAnyPattern(filename, ignoreFiles);
585
- const isGenerated = hasGeneratedMarker(sourceText);
617
+ const isGenerated = hasGeneratedMarker(sourceText) || isGeneratedFile(filename, sourceText);
586
618
  if (isIgnoredByDefault || isIgnoredByOption || isGenerated) {
587
619
  return {};
588
620
  }
@@ -599,22 +631,6 @@ var no_enum_default = import_utils4.ESLintUtils.RuleCreator(
599
631
 
600
632
  // src/rules/no-insecure-random-id.ts
601
633
  var import_utils5 = require("@typescript-eslint/utils");
602
-
603
- // src/rules/_paths.ts
604
- var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
605
- function isTestFile(filename) {
606
- const normalized = filename.replaceAll("\\", "/");
607
- const base = normalized.slice(normalized.lastIndexOf("/") + 1);
608
- if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(base)) {
609
- return true;
610
- }
611
- return /(^|\/)(tests?|__tests__|__mocks__|fixtures)\//.test(normalized);
612
- }
613
- function isScriptFile(filename) {
614
- return SCRIPT_FILE_RE.test(filename);
615
- }
616
-
617
- // src/rules/no-insecure-random-id.ts
618
634
  var STRONG_SECURITY_PATTERN = /token|secret|csrf|password|passwd|apikey|api[-_]?key|nonce|salt|uuid|authid/i;
619
635
  var NON_SECURITY_ID_PATTERN = /temp|tmp|cache|correlation|request|req|trace|execution|dev|hmr|mock|test|perf|marker/i;
620
636
  var PATH_OR_DOM_MARKER = /[\\/#]|\.[A-Za-z0-9]/;
@@ -1158,6 +1174,10 @@ var no_log_only_catch_default = import_utils8.ESLintUtils.RuleCreator(
1158
1174
  // src/rules/no-raw-env.ts
1159
1175
  var import_utils9 = require("@typescript-eslint/utils");
1160
1176
  var CONFIG_FILE_RE = /(^|[\\/])[\w.-]+\.config\.[cm]?[jt]sx?$/;
1177
+ var ENV_BOUNDARY_FILE_RE = /(^|[\\/])(?:env|client-env|server-env|client-settings|server-settings)\.[cm]?[jt]sx?$/;
1178
+ function isValidatedEnvBoundary(filename, sourceText) {
1179
+ return ENV_BOUNDARY_FILE_RE.test(filename.replaceAll("\\", "/")) && /\bz\.object\s*\(/.test(sourceText) && /\.parse\s*\(/.test(sourceText);
1180
+ }
1161
1181
  function isProcessEnv(node) {
1162
1182
  return !node.computed && node.object.type === "Identifier" && node.object.name === "process" && node.property.type === "Identifier" && node.property.name === "env";
1163
1183
  }
@@ -1207,7 +1227,7 @@ var no_raw_env_default = import_utils9.ESLintUtils.RuleCreator(
1207
1227
  defaultOptions: [],
1208
1228
  create(context) {
1209
1229
  const filename = context.filename;
1210
- if (isTestFile(filename) || isScriptFile(filename) || CONFIG_FILE_RE.test(filename.replaceAll("\\", "/"))) {
1230
+ if (isTestFile(filename) || isScriptFile(filename) || CONFIG_FILE_RE.test(filename.replaceAll("\\", "/")) || isValidatedEnvBoundary(filename, context.sourceCode.text)) {
1211
1231
  return {};
1212
1232
  }
1213
1233
  return {
@@ -1549,6 +1569,9 @@ var no_sentinel_return_on_catch_default = import_utils10.ESLintUtils.RuleCreator
1549
1569
  },
1550
1570
  defaultOptions: [{}],
1551
1571
  create(context, [loggingOptions]) {
1572
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) {
1573
+ return {};
1574
+ }
1552
1575
  const matcher = createLogMatcher(loggingOptions);
1553
1576
  function logsOrReportsError(catchBody, caughtName) {
1554
1577
  return walkWithinScope(catchBody, (current) => {
@@ -1955,6 +1978,9 @@ var no_string_concat_in_loop_default = import_utils12.ESLintUtils.RuleCreator(
1955
1978
  },
1956
1979
  defaultOptions: [],
1957
1980
  create(context) {
1981
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) {
1982
+ return {};
1983
+ }
1958
1984
  const reported = /* @__PURE__ */ new WeakMap();
1959
1985
  return {
1960
1986
  AssignmentExpression(node) {
@@ -2419,7 +2445,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
2419
2445
  },
2420
2446
  defaultOptions: [],
2421
2447
  create(context) {
2422
- if (isTestFile(context.filename)) {
2448
+ if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
2423
2449
  return {};
2424
2450
  }
2425
2451
  const unvalidatedVariables = /* @__PURE__ */ new Set();
@@ -2518,6 +2544,8 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
2518
2544
 
2519
2545
  // src/rules/prefer-semantic-colors.ts
2520
2546
  var import_utils17 = require("@typescript-eslint/utils");
2547
+ var import_fs = require("fs");
2548
+ var import_path = require("path");
2521
2549
 
2522
2550
  // src/rules/_tailwind.ts
2523
2551
  var tailwindBase = (token) => token.replace(/^(?:[a-z0-9-]+:)+/i, "").replace(/^!/, "");
@@ -2555,6 +2583,20 @@ var STYLE_COLOR_PROPS = /* @__PURE__ */ new Set([
2555
2583
  ]);
2556
2584
  var RAW_COLOR_VALUE_RE = new RegExp(`#[0-9a-fA-F]{3,8}\\b|\\b(?:${COLOR_FN})\\s*\\(`, "i");
2557
2585
  var STORIES_FILE_RE = /\.stories\.[cm]?[jt]sx?$/i;
2586
+ var SEMANTIC_TOKEN_RE = /--(?:background|foreground|primary|secondary|muted|accent|destructive|border|card|popover)\b|(?:bg|text|border)-(?:background|foreground|primary|secondary|muted|accent|destructive|border|card|popover)\b/;
2587
+ var DETECTION_FILES = [
2588
+ "components.json",
2589
+ "tailwind.config.js",
2590
+ "tailwind.config.cjs",
2591
+ "tailwind.config.mjs",
2592
+ "tailwind.config.ts",
2593
+ "app/globals.css",
2594
+ "src/app/globals.css",
2595
+ "src/index.css",
2596
+ "src/styles/globals.css",
2597
+ "styles/globals.css"
2598
+ ];
2599
+ var semanticTokenCache = /* @__PURE__ */ new Map();
2558
2600
  var SVG_DEFS_CONTAINERS = /* @__PURE__ */ new Set([
2559
2601
  "mask",
2560
2602
  "clipPath",
@@ -2573,16 +2615,66 @@ var SVG_EXEMPT_COLOR_VALUES = /* @__PURE__ */ new Set([
2573
2615
  "currentcolor",
2574
2616
  "inherit"
2575
2617
  ]);
2618
+ function jsxElementName(node) {
2619
+ const name = node.openingElement.name;
2620
+ if (name.type === import_utils17.AST_NODE_TYPES.JSXIdentifier) return name.name;
2621
+ if (name.type === import_utils17.AST_NODE_TYPES.JSXMemberExpression && name.property.type === import_utils17.AST_NODE_TYPES.JSXIdentifier) {
2622
+ return name.property.name;
2623
+ }
2624
+ return null;
2625
+ }
2626
+ function isSvgLikeElementName(name) {
2627
+ return name === "svg" || SVG_DEFS_CONTAINERS.has(name) || /svg$/i.test(name);
2628
+ }
2576
2629
  var isInsideSvg = (node) => {
2577
2630
  let current = node.parent;
2578
2631
  while (current !== void 0 && current !== null) {
2579
- if (current.type === import_utils17.AST_NODE_TYPES.JSXElement && current.openingElement.name.type === import_utils17.AST_NODE_TYPES.JSXIdentifier && (current.openingElement.name.name === "svg" || SVG_DEFS_CONTAINERS.has(current.openingElement.name.name))) {
2632
+ if (current.type === import_utils17.AST_NODE_TYPES.JSXElement) {
2633
+ const name = jsxElementName(current);
2634
+ if (name !== null && isSvgLikeElementName(name)) return true;
2635
+ }
2636
+ current = current.parent;
2637
+ }
2638
+ return false;
2639
+ };
2640
+ var isInsideIconFactoryPath = (node) => {
2641
+ let current = node.parent;
2642
+ while (current !== void 0 && current !== null) {
2643
+ if (current.type === import_utils17.AST_NODE_TYPES.Property && propName(current.key) === "path" && current.parent.type === import_utils17.AST_NODE_TYPES.ObjectExpression && current.parent.parent.type === import_utils17.AST_NODE_TYPES.CallExpression && current.parent.parent.callee.type === import_utils17.AST_NODE_TYPES.Identifier && current.parent.parent.callee.name === "createIcon") {
2580
2644
  return true;
2581
2645
  }
2582
2646
  current = current.parent;
2583
2647
  }
2584
2648
  return false;
2585
2649
  };
2650
+ var hasSemanticTokenSystem = (filename) => {
2651
+ let dir = (0, import_path.dirname)(filename);
2652
+ const root = (0, import_path.parse)(dir).root;
2653
+ for (let depth = 0; depth < 8; depth += 1) {
2654
+ const cached = semanticTokenCache.get(dir);
2655
+ if (cached !== void 0) return cached;
2656
+ let found = false;
2657
+ for (const rel of DETECTION_FILES) {
2658
+ const candidate = (0, import_path.join)(dir, rel);
2659
+ if (!(0, import_fs.existsSync)(candidate)) continue;
2660
+ if (rel === "components.json") {
2661
+ found = true;
2662
+ break;
2663
+ }
2664
+ try {
2665
+ if (SEMANTIC_TOKEN_RE.test((0, import_fs.readFileSync)(candidate, "utf8"))) {
2666
+ found = true;
2667
+ break;
2668
+ }
2669
+ } catch {
2670
+ }
2671
+ }
2672
+ semanticTokenCache.set(dir, found);
2673
+ if (found || dir === root) return found;
2674
+ dir = (0, import_path.dirname)(dir);
2675
+ }
2676
+ return false;
2677
+ };
2586
2678
  var propName = (key) => {
2587
2679
  if (key.type === import_utils17.AST_NODE_TYPES.Identifier) return key.name;
2588
2680
  if (key.type === import_utils17.AST_NODE_TYPES.Literal && typeof key.value === "string") return key.value;
@@ -2597,16 +2689,27 @@ var prefer_semantic_colors_default = import_utils17.ESLintUtils.RuleCreator(
2597
2689
  docs: {
2598
2690
  description: "Enforce design-system semantic color tokens (bg-primary, text-destructive, \u2026) over raw Tailwind palette classes (text-red-500), arbitrary color values (bg-[#fff]), and inline color literals."
2599
2691
  },
2600
- schema: [],
2692
+ schema: [
2693
+ {
2694
+ type: "object",
2695
+ additionalProperties: false,
2696
+ properties: {
2697
+ requireSemanticTokens: { type: "boolean" }
2698
+ }
2699
+ }
2700
+ ],
2601
2701
  messages: {
2602
2702
  rawPalette: "Raw palette class '{{class}}' \u2014 use a semantic token (e.g. text-foreground, bg-primary, text-destructive, bg-muted).",
2603
2703
  arbitraryColor: "Hardcoded color '{{class}}' \u2014 use a semantic token, or var(--\u2026). For charts/brand add an eslint-disable with a reason.",
2604
2704
  inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason."
2605
2705
  }
2606
2706
  },
2607
- defaultOptions: [],
2608
- create(context) {
2707
+ defaultOptions: [{}],
2708
+ create(context, [options]) {
2609
2709
  if (STORIES_FILE_RE.test(context.filename)) return {};
2710
+ if (options?.requireSemanticTokens === true && !hasSemanticTokenSystem(context.filename)) {
2711
+ return {};
2712
+ }
2610
2713
  const reportClasses = (value, node) => {
2611
2714
  for (const token of classTokens(value)) {
2612
2715
  const base = tailwindBase(token);
@@ -2686,7 +2789,7 @@ var prefer_semantic_colors_default = import_utils17.ESLintUtils.RuleCreator(
2686
2789
  if (typeof node.value.value === "string" && SVG_EXEMPT_COLOR_VALUES.has(node.value.value.toLowerCase())) {
2687
2790
  return;
2688
2791
  }
2689
- if (isInsideSvg(node)) return;
2792
+ if (isInsideSvg(node) || isInsideIconFactoryPath(node)) return;
2690
2793
  checkColorValueNode(node.value);
2691
2794
  },
2692
2795
  // Inline style objects: style={{ color: "#111827", backgroundColor: "#fff" }}
@@ -2865,7 +2968,7 @@ var INPUT_TYPE_REPLACEMENTS = {
2865
2968
  radio: "RadioGroup",
2866
2969
  range: "Slider"
2867
2970
  };
2868
- var SKIPPED_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden"]);
2971
+ var SKIPPED_INPUT_TYPES = /* @__PURE__ */ new Set(["file", "hidden"]);
2869
2972
  var kebabCase = (component) => component.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
2870
2973
  var literalTypeAttr = (node) => {
2871
2974
  for (const attribute of node.attributes) {
@@ -2879,8 +2982,12 @@ var literalTypeAttr = (node) => {
2879
2982
  }
2880
2983
  return null;
2881
2984
  };
2985
+ var hasFileAcceptAttr = (node) => node.attributes.some(
2986
+ (attribute) => attribute.type === import_utils19.AST_NODE_TYPES.JSXAttribute && attribute.name.type === import_utils19.AST_NODE_TYPES.JSXIdentifier && attribute.name.name === "accept"
2987
+ );
2882
2988
  var resolveInputReplacement = (node) => {
2883
2989
  const typeAttr = literalTypeAttr(node);
2990
+ if (hasFileAcceptAttr(node)) return null;
2884
2991
  if (typeAttr === null || typeAttr.kind === "dynamic") return "Input";
2885
2992
  if (SKIPPED_INPUT_TYPES.has(typeAttr.value)) return null;
2886
2993
  return INPUT_TYPE_REPLACEMENTS[typeAttr.value] ?? "Input";
@@ -2901,7 +3008,7 @@ var prefer_shadcn_default = import_utils19.ESLintUtils.RuleCreator(
2901
3008
  },
2902
3009
  defaultOptions: [],
2903
3010
  create(context) {
2904
- if (isTestFile(context.filename)) {
3011
+ if (isTestFile(context.filename) || isStoryFile(context.filename)) {
2905
3012
  return {};
2906
3013
  }
2907
3014
  return {
@@ -3219,7 +3326,7 @@ var zod_naming_convention_default = import_utils22.ESLintUtils.RuleCreator(
3219
3326
  const convention = optionsArg?.convention ?? "either";
3220
3327
  const { test, messageId } = CONVENTIONS[convention];
3221
3328
  const acceptsSchemaWord = convention !== "prefix";
3222
- if (isTestFile(context.filename)) {
3329
+ if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
3223
3330
  return {};
3224
3331
  }
3225
3332
  return {
@@ -3970,6 +4077,9 @@ var no_fat_try_blocks_default = import_utils27.ESLintUtils.RuleCreator(
3970
4077
  },
3971
4078
  defaultOptions: [],
3972
4079
  create(context) {
4080
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) {
4081
+ return {};
4082
+ }
3973
4083
  const sourceCode = context.sourceCode;
3974
4084
  return {
3975
4085
  TryStatement(node) {
@@ -4359,7 +4469,7 @@ var no_unsafe_cast_default = import_utils29.ESLintUtils.RuleCreator(
4359
4469
  },
4360
4470
  defaultOptions: [],
4361
4471
  create(context) {
4362
- if (isTestFile(context.filename)) {
4472
+ if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
4363
4473
  return {};
4364
4474
  }
4365
4475
  function checkAssertion(node) {
@@ -5518,7 +5628,9 @@ var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
5518
5628
  var SENTINEL_IDENTIFIERS = /* @__PURE__ */ new Set(["undefined", "NaN"]);
5519
5629
  var SENTINEL_WORDS = /(^|_)(SENTINEL|EMPTY|NONE|NULL|UNSET|MISSING|PLACEHOLDER|DUMMY|FAKE|EXAMPLE)(_|$)/;
5520
5630
  var SENTINEL_PREFIX_RE = /^(skip|sentinel|empty|none|missing|unset|placeholder|dummy|fake|example|noop)[A-Z]/;
5631
+ var AST_NODE_TYPE_RE = /^(?:TS|JSX)?[A-Z][A-Za-z]*(?:Signature|Keyword|Expression|Declaration|Element|Literal|Identifier)$/;
5521
5632
  function isConstantReference(identifier) {
5633
+ if (AST_NODE_TYPE_RE.test(identifier)) return true;
5522
5634
  if (isAuthSecretName(identifier) && !SENTINEL_WORDS.test(identifier)) return false;
5523
5635
  return identifier === identifier.toUpperCase() && /[A-Za-z]/.test(identifier);
5524
5636
  }
@@ -5785,6 +5897,7 @@ var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
5785
5897
  "window",
5786
5898
  "self"
5787
5899
  ]);
5900
+ var PRESIGNED_URL_NAME_RE = /(?:pre-?signed|signed|upload|download)Url$/i;
5788
5901
  function isGlobalFetchCall(node) {
5789
5902
  const callee = node.callee;
5790
5903
  if (callee.type === "Identifier") {
@@ -5795,6 +5908,23 @@ function isGlobalFetchCall(node) {
5795
5908
  }
5796
5909
  return false;
5797
5910
  }
5911
+ function identifierLikeName(node) {
5912
+ if (node.type === "Identifier") {
5913
+ return node.name;
5914
+ }
5915
+ if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
5916
+ return node.property.name;
5917
+ }
5918
+ return null;
5919
+ }
5920
+ function isPresignedUrlTransfer(node) {
5921
+ const first = node.arguments[0];
5922
+ if (first === void 0) {
5923
+ return false;
5924
+ }
5925
+ const name = identifierLikeName(first);
5926
+ return name !== null && PRESIGNED_URL_NAME_RE.test(name);
5927
+ }
5798
5928
  function compile(patterns) {
5799
5929
  const compiled = [];
5800
5930
  for (const pattern of patterns) {
@@ -5841,6 +5971,9 @@ var no_raw_fetch_outside_clients_default = import_utils42.ESLintUtils.RuleCreato
5841
5971
  }
5842
5972
  return {
5843
5973
  CallExpression(node) {
5974
+ if (isPresignedUrlTransfer(node)) {
5975
+ return;
5976
+ }
5844
5977
  if (isGlobalFetchCall(node)) {
5845
5978
  context.report({ node, messageId: "rawFetch" });
5846
5979
  }
@@ -6466,7 +6599,7 @@ var rules = {
6466
6599
  var plugin = {
6467
6600
  meta: {
6468
6601
  name: "@sarj/eslint-plugin",
6469
- version: "2.12.0"
6602
+ version: "2.12.2"
6470
6603
  },
6471
6604
  rules,
6472
6605
  configs: {
@@ -6491,7 +6624,7 @@ var plugin = {
6491
6624
  "@sarj/prefer-discriminated-union": "warn",
6492
6625
  "@sarj/no-comment-cruft": "warn",
6493
6626
  // Frontend / styling — distilled from frontend PR-review mining.
6494
- "@sarj/prefer-semantic-colors": "warn",
6627
+ "@sarj/prefer-semantic-colors": ["warn", { requireSemanticTokens: true }],
6495
6628
  // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
6496
6629
  "@sarj/no-fat-try-blocks": "warn",
6497
6630
  "@sarj/no-cors-wildcard-with-credentials": "warn",
@@ -6550,7 +6683,7 @@ var plugin = {
6550
6683
  "@sarj/no-comment-cruft": "error",
6551
6684
  // Frontend / styling — distilled from frontend PR-review mining. Stylistic,
6552
6685
  // no autofix → warn (rollout should prove the FP rate before raising it).
6553
- "@sarj/prefer-semantic-colors": "error",
6686
+ "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
6554
6687
  // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
6555
6688
  "@sarj/no-fat-try-blocks": "error",
6556
6689
  "@sarj/no-cors-wildcard-with-credentials": "error",