@sarj/eslint-plugin 2.0.2 → 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/dist/index.cjs CHANGED
@@ -28,45 +28,30 @@ module.exports = __toCommonJS(index_exports);
28
28
  // src/rules/enforce-file-structure.ts
29
29
  var import_utils = require("@typescript-eslint/utils");
30
30
  var SECTION = {
31
- imports: 0,
32
- types: 1,
33
- constants: 2,
34
- functions: 3,
35
- exports: 4
31
+ declarations: 0,
32
+ functions: 1,
33
+ exports: 2
36
34
  };
37
- var SECTION_NAMES = [
38
- "imports",
39
- "types",
40
- "constants",
41
- "functions",
42
- "exports"
43
- ];
35
+ var SECTION_NAMES = ["declarations", "functions", "exports"];
44
36
  var sectionName = (ordinal) => {
45
37
  const name = SECTION_NAMES[ordinal];
46
38
  return name ?? "unknown";
47
39
  };
48
- var isConstantNamed = (declarator) => {
49
- if (declarator.id.type !== import_utils.AST_NODE_TYPES.Identifier) return false;
50
- const name = declarator.id.name;
51
- return name.length > 0 && name === name.toUpperCase();
52
- };
40
+ var SERVER_ACTION_FILE_RE = /(?:^|\/)actions\/|\.action\.[jt]sx?$|(?:^|\/)actions\.[jt]sx?$/;
41
+ var isFunctionExpression = (node) => node.type === import_utils.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils.AST_NODE_TYPES.FunctionExpression;
42
+ var isFunctionLikeVariable = (statement) => statement.declarations.length > 0 && statement.declarations.every(
43
+ (decl) => decl.init !== null && isFunctionExpression(decl.init)
44
+ );
53
45
  var getStatementSection = (statement) => {
54
46
  switch (statement.type) {
55
47
  case import_utils.AST_NODE_TYPES.ImportDeclaration:
56
- return SECTION.imports;
57
48
  case import_utils.AST_NODE_TYPES.TSTypeAliasDeclaration:
58
49
  case import_utils.AST_NODE_TYPES.TSInterfaceDeclaration:
59
50
  case import_utils.AST_NODE_TYPES.TSEnumDeclaration:
60
- return SECTION.types;
61
- case import_utils.AST_NODE_TYPES.VariableDeclaration: {
62
- if (statement.kind === "const") {
63
- const firstDeclarator = statement.declarations[0];
64
- if (firstDeclarator !== void 0 && isConstantNamed(firstDeclarator)) {
65
- return SECTION.constants;
66
- }
67
- }
68
- return SECTION.functions;
69
- }
51
+ case import_utils.AST_NODE_TYPES.ClassDeclaration:
52
+ return SECTION.declarations;
53
+ case import_utils.AST_NODE_TYPES.VariableDeclaration:
54
+ return isFunctionLikeVariable(statement) ? SECTION.functions : SECTION.declarations;
70
55
  case import_utils.AST_NODE_TYPES.FunctionDeclaration:
71
56
  return SECTION.functions;
72
57
  case import_utils.AST_NODE_TYPES.ExportNamedDeclaration:
@@ -91,7 +76,7 @@ var enforce_file_structure_default = import_utils.ESLintUtils.RuleCreator(
91
76
  meta: {
92
77
  type: "suggestion",
93
78
  docs: {
94
- description: "Enforce a canonical top-of-file ordering: imports -> types -> constants -> functions -> exports. Server-action files (under `/actions/` or with `action` in the path) must also begin with a `use server` directive."
79
+ description: "Enforce that function definitions follow the file's top-of-file declarations (imports, types, constants, classes) \u2014 the stepdown rule. Ordering among non-function declarations is not enforced. Server-action files (under `/actions/`, named `*.action.ts`, or `actions.ts`) must also begin with a `use server` directive."
95
80
  },
96
81
  schema: [],
97
82
  messages: {
@@ -102,7 +87,7 @@ var enforce_file_structure_default = import_utils.ESLintUtils.RuleCreator(
102
87
  defaultOptions: [],
103
88
  create(context) {
104
89
  const filename = context.filename;
105
- const isServerAction = filename.includes("/actions/") || filename.includes("action");
90
+ const isServerAction = SERVER_ACTION_FILE_RE.test(filename);
106
91
  return {
107
92
  Program(node) {
108
93
  const body = node.body;
@@ -115,9 +100,8 @@ var enforce_file_structure_default = import_utils.ESLintUtils.RuleCreator(
115
100
  });
116
101
  }
117
102
  }
118
- let currentSection = SECTION.imports;
103
+ let currentSection = SECTION.declarations;
119
104
  for (const statement of body) {
120
- if (isUseServerDirective(statement)) continue;
121
105
  if (statement.type === import_utils.AST_NODE_TYPES.ExpressionStatement && statement.expression.type === import_utils.AST_NODE_TYPES.Literal && typeof statement.expression.value === "string" && statement.expression.value.startsWith("use ")) {
122
106
  continue;
123
107
  }
@@ -153,7 +137,7 @@ var HTTP_METHOD_NAMES = /* @__PURE__ */ new Set([
153
137
  "head",
154
138
  "options"
155
139
  ]);
156
- var ANALYTICS_KEYWORDS = [
140
+ var ANALYTICS_SEGMENTS = /* @__PURE__ */ new Set([
157
141
  "analytics",
158
142
  "telemetry",
159
143
  "track",
@@ -162,7 +146,7 @@ var ANALYTICS_KEYWORDS = [
162
146
  "beacon",
163
147
  "metrics",
164
148
  "event"
165
- ];
149
+ ]);
166
150
  function isEffectHookCall(node) {
167
151
  const callee = node.callee;
168
152
  if (callee.type === import_utils2.AST_NODE_TYPES.Identifier) {
@@ -236,7 +220,7 @@ function extractUrlString(node) {
236
220
  function isAnalyticsCall(node) {
237
221
  const url = extractUrlString(node).toLowerCase();
238
222
  if (url === "") return false;
239
- return ANALYTICS_KEYWORDS.some((keyword) => url.includes(keyword));
223
+ return url.split(/[/.]/).some((segment) => ANALYTICS_SEGMENTS.has(segment));
240
224
  }
241
225
  var no_client_side_data_fetching_default = import_utils2.ESLintUtils.RuleCreator(
242
226
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
@@ -275,8 +259,105 @@ var no_client_side_data_fetching_default = import_utils2.ESLintUtils.RuleCreator
275
259
  }
276
260
  });
277
261
 
278
- // src/rules/no-enum.ts
262
+ // src/rules/no-comment-cruft.ts
279
263
  var import_utils3 = require("@typescript-eslint/utils");
264
+ var LEADING_PREAMBLE_MIN = 4;
265
+ var DIRECTIVE_RE = /^(eslint\b|eslint-|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
266
+ var LICENSE_RE = /copyright|licen[cs]ed?|spdx|permission is hereby granted|all rights reserved/i;
267
+ var BANNER_FULL_RE = /^[\s\-=*#~_+.]{4,}$/;
268
+ var BANNER_RUN_RE = /={4,}|-{4,}|#{4,}|\*{4,}|~{4,}/;
269
+ var REGION_RE = /^#?(?:end)?region\b/i;
270
+ var CODE_KEYWORD_RE = /^(import |export |const |let |var |function\b|class |interface |type \w|enum |return\b|throw |await |async |if\s*\(|for\s*\(|while\s*\(|switch\s*\(|new |console\.)/;
271
+ var CODE_TAIL_RE = /[;{}()]\s*$|=>\s*$|,\s*$/;
272
+ var CALL_OR_ASSIGN_RE = /^[A-Za-z_$][\w.$[\]]*\s*(?:=(?![=>])|\+=|-=|\*=)\s*\S.*[;)}\]]\s*$|^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
273
+ function stripCommentMarker(line) {
274
+ return line.replace(/^\s*\/\//, "").replace(/^\s*\*+/, "").trim();
275
+ }
276
+ function isDirective(text) {
277
+ return DIRECTIVE_RE.test(text.trim());
278
+ }
279
+ function isBanner(text) {
280
+ const t = text.trim();
281
+ if (!t) return false;
282
+ return BANNER_FULL_RE.test(t) || BANNER_RUN_RE.test(t) || REGION_RE.test(t);
283
+ }
284
+ function looksLikeCode(text) {
285
+ const t = text.trim();
286
+ if (!t) return false;
287
+ if (CODE_KEYWORD_RE.test(t) && CODE_TAIL_RE.test(t)) return true;
288
+ return CALL_OR_ASSIGN_RE.test(t);
289
+ }
290
+ var no_comment_cruft_default = import_utils3.ESLintUtils.RuleCreator(
291
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
292
+ )({
293
+ name: "no-comment-cruft",
294
+ meta: {
295
+ type: "suggestion",
296
+ docs: {
297
+ description: "Flag commented-out code, section-banner comments, and leading file-header comment preambles."
298
+ },
299
+ schema: [],
300
+ messages: {
301
+ commentedOutCode: "Commented-out code \u2014 delete it; git history remembers.",
302
+ sectionBanner: "Section-banner / region comment \u2014 structure code with functions, not ASCII rules.",
303
+ fileHeaderPreamble: "File-header comment preamble \u2014 use a brief doc comment for the why, not a block of `//` lines."
304
+ }
305
+ },
306
+ defaultOptions: [],
307
+ create(context) {
308
+ const sourceCode = context.sourceCode;
309
+ function isStandalone(comment) {
310
+ const before = sourceCode.getTokenBefore(comment, {
311
+ includeComments: false
312
+ });
313
+ return !before || before.loc.end.line < comment.loc.start.line;
314
+ }
315
+ function isJsDoc(comment) {
316
+ return comment.type === "Block" && /^\*/.test(comment.value);
317
+ }
318
+ function reportLeadingPreamble(comments, firstCodeLine) {
319
+ const leading = [];
320
+ let prevLine = null;
321
+ for (const comment of comments) {
322
+ if (comment.type !== "Line") break;
323
+ if (comment.loc.start.line >= firstCodeLine) break;
324
+ if (!isStandalone(comment)) break;
325
+ const body = stripCommentMarker(comment.value);
326
+ if (isDirective(body) || body.startsWith("!")) continue;
327
+ if (prevLine !== null && comment.loc.start.line !== prevLine + 1) break;
328
+ leading.push(comment);
329
+ prevLine = comment.loc.start.line;
330
+ }
331
+ const first = leading[0];
332
+ if (first === void 0 || leading.length < LEADING_PREAMBLE_MIN) return;
333
+ const isLicense = leading.some(
334
+ (c) => LICENSE_RE.test(stripCommentMarker(c.value))
335
+ );
336
+ if (!isLicense) {
337
+ context.report({ node: first, messageId: "fileHeaderPreamble" });
338
+ }
339
+ }
340
+ return {
341
+ Program() {
342
+ const comments = sourceCode.getAllComments();
343
+ const firstCodeLine = sourceCode.ast.tokens[0]?.loc.start.line ?? Number.MAX_SAFE_INTEGER;
344
+ for (const comment of comments) {
345
+ if (isJsDoc(comment) || !isStandalone(comment)) continue;
346
+ const texts = comment.value.split("\n").map(stripCommentMarker).filter((l) => l.length > 0 && !isDirective(l));
347
+ if (texts.some(isBanner)) {
348
+ context.report({ node: comment, messageId: "sectionBanner" });
349
+ } else if (texts.some(looksLikeCode)) {
350
+ context.report({ node: comment, messageId: "commentedOutCode" });
351
+ }
352
+ }
353
+ reportLeadingPreamble(comments, firstCodeLine);
354
+ }
355
+ };
356
+ }
357
+ });
358
+
359
+ // src/rules/no-enum.ts
360
+ var import_utils4 = require("@typescript-eslint/utils");
280
361
  var DEFAULT_IGNORE_PATTERNS = [
281
362
  /[\\/]generated[\\/]/,
282
363
  /\.gen\.tsx?$/,
@@ -295,7 +376,7 @@ function hasGeneratedMarker(sourceText) {
295
376
  const head = sourceText.slice(0, 1024);
296
377
  return /@generated\b/.test(head);
297
378
  }
298
- var no_enum_default = import_utils3.ESLintUtils.RuleCreator(
379
+ var no_enum_default = import_utils4.ESLintUtils.RuleCreator(
299
380
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
300
381
  )({
301
382
  name: "no-enum",
@@ -345,9 +426,260 @@ var no_enum_default = import_utils3.ESLintUtils.RuleCreator(
345
426
  }
346
427
  });
347
428
 
429
+ // src/rules/no-insecure-random-id.ts
430
+ var import_utils5 = require("@typescript-eslint/utils");
431
+ var NAME_PATTERN = /id|token|key|secret|uuid|nonce|session|password|salt/i;
432
+ function isMathRandomCall(node) {
433
+ if (node.type !== "CallExpression") {
434
+ return false;
435
+ }
436
+ const callee = node.callee;
437
+ if (callee.type !== "MemberExpression" || callee.computed) {
438
+ return false;
439
+ }
440
+ const { object, property } = callee;
441
+ return object.type === "Identifier" && object.name === "Math" && property.type === "Identifier" && property.name === "random";
442
+ }
443
+ function isPartOfToString36Chain(node) {
444
+ let current = node;
445
+ let parent = current.parent;
446
+ while (parent) {
447
+ if (parent.type === "MemberExpression" && parent.object === current && !parent.computed && parent.property.type === "Identifier" && parent.property.name === "toString") {
448
+ const grandparent = parent.parent;
449
+ if (grandparent && grandparent.type === "CallExpression" && grandparent.callee === parent) {
450
+ const firstArg = grandparent.arguments[0];
451
+ if (firstArg && firstArg.type === "Literal" && firstArg.value === 36) {
452
+ return true;
453
+ }
454
+ }
455
+ }
456
+ if (parent.type === "MemberExpression" && parent.object === current) {
457
+ current = parent;
458
+ parent = current.parent;
459
+ continue;
460
+ }
461
+ if (parent.type === "CallExpression" && parent.callee === current) {
462
+ current = parent;
463
+ parent = current.parent;
464
+ continue;
465
+ }
466
+ break;
467
+ }
468
+ return false;
469
+ }
470
+ function findEnclosingName(node) {
471
+ let current = node;
472
+ let parent = current.parent;
473
+ while (parent) {
474
+ if (parent.type === "VariableDeclarator" && parent.init === current) {
475
+ if (parent.id.type === "Identifier") {
476
+ return parent.id.name;
477
+ }
478
+ return void 0;
479
+ }
480
+ if (parent.type === "Property" && parent.value === current) {
481
+ const key = parent.key;
482
+ if (!parent.computed && key.type === "Identifier") {
483
+ return key.name;
484
+ }
485
+ if (key.type === "Literal" && typeof key.value === "string") {
486
+ return key.value;
487
+ }
488
+ return void 0;
489
+ }
490
+ if (parent.type === "PropertyDefinition" && parent.value === current) {
491
+ const key = parent.key;
492
+ if (!parent.computed && key.type === "Identifier") {
493
+ return key.name;
494
+ }
495
+ if (key.type === "Literal" && typeof key.value === "string") {
496
+ return key.value;
497
+ }
498
+ return void 0;
499
+ }
500
+ if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression" || parent.type === "BlockStatement" || parent.type === "ReturnStatement" || parent.type === "ExpressionStatement") {
501
+ return void 0;
502
+ }
503
+ current = parent;
504
+ parent = current.parent;
505
+ }
506
+ return void 0;
507
+ }
508
+ var no_insecure_random_id_default = import_utils5.ESLintUtils.RuleCreator(
509
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
510
+ )({
511
+ name: "no-insecure-random-id",
512
+ meta: {
513
+ type: "problem",
514
+ docs: {
515
+ description: "Disallow using `Math.random()` to generate identifiers, tokens, or secrets; use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead."
516
+ },
517
+ schema: [],
518
+ messages: {
519
+ insecureRandomId: "`Math.random()` is not cryptographically secure and is predictable; do not use it to generate IDs, tokens, or secrets. Use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead."
520
+ }
521
+ },
522
+ defaultOptions: [],
523
+ create(context) {
524
+ return {
525
+ CallExpression(node) {
526
+ if (!isMathRandomCall(node)) {
527
+ return;
528
+ }
529
+ if (isPartOfToString36Chain(node)) {
530
+ context.report({ node, messageId: "insecureRandomId" });
531
+ return;
532
+ }
533
+ const name = findEnclosingName(node);
534
+ if (name !== void 0 && NAME_PATTERN.test(name)) {
535
+ context.report({ node, messageId: "insecureRandomId" });
536
+ }
537
+ }
538
+ };
539
+ }
540
+ });
541
+
542
+ // src/rules/no-json-stringify-error.ts
543
+ var import_utils6 = require("@typescript-eslint/utils");
544
+ var ERROR_NAME_PATTERN = /^(e|err|error|ex|exc)$/i;
545
+ function isCatchBinding(scope, name) {
546
+ let current = scope;
547
+ while (current) {
548
+ const variable = current.set.get(name);
549
+ if (variable) {
550
+ for (const def of variable.defs) {
551
+ if (def.type === "CatchClause") {
552
+ return true;
553
+ }
554
+ }
555
+ }
556
+ current = current.upper;
557
+ }
558
+ return false;
559
+ }
560
+ function isJsonStringify(callee) {
561
+ return callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.object.name === "JSON" && callee.property.type === "Identifier" && callee.property.name === "stringify";
562
+ }
563
+ var no_json_stringify_error_default = import_utils6.ESLintUtils.RuleCreator(
564
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
565
+ )({
566
+ name: "no-json-stringify-error",
567
+ meta: {
568
+ type: "problem",
569
+ docs: {
570
+ description: "Disallow `JSON.stringify` on an Error value; it yields `{}` because `message`/`stack` are non-enumerable."
571
+ },
572
+ schema: [],
573
+ messages: {
574
+ noJsonStringifyError: "`JSON.stringify` on an Error yields `{}` because `message`/`stack` are non-enumerable. Log `err.message` / `err.stack`, or use a proper error serializer."
575
+ }
576
+ },
577
+ defaultOptions: [],
578
+ create(context) {
579
+ return {
580
+ CallExpression(node) {
581
+ if (!isJsonStringify(node.callee)) {
582
+ return;
583
+ }
584
+ const firstArg = node.arguments[0];
585
+ if (!firstArg || firstArg.type !== "Identifier") {
586
+ return;
587
+ }
588
+ const name = firstArg.name;
589
+ const scope = context.sourceCode.getScope(firstArg);
590
+ if (ERROR_NAME_PATTERN.test(name) || isCatchBinding(scope, name)) {
591
+ context.report({
592
+ node,
593
+ messageId: "noJsonStringifyError"
594
+ });
595
+ }
596
+ }
597
+ };
598
+ }
599
+ });
600
+
601
+ // src/rules/no-log-only-catch.ts
602
+ var import_utils7 = require("@typescript-eslint/utils");
603
+ var DEFAULT_IGNORE_PATTERNS2 = [
604
+ /\.test\./,
605
+ /\.spec\./,
606
+ /[\\/]__tests__[\\/]/
607
+ ];
608
+ var CONSOLE_METHODS = /* @__PURE__ */ new Set([
609
+ "log",
610
+ "error",
611
+ "warn",
612
+ "info",
613
+ "debug"
614
+ ]);
615
+ function isConsoleCallStatement(statement) {
616
+ if (statement.type !== "ExpressionStatement") {
617
+ return false;
618
+ }
619
+ const expr = statement.expression;
620
+ if (expr.type !== "CallExpression") {
621
+ return false;
622
+ }
623
+ const callee = expr.callee;
624
+ if (callee.type !== "MemberExpression") {
625
+ return false;
626
+ }
627
+ const { object, property } = callee;
628
+ if (object.type !== "Identifier" || object.name !== "console") {
629
+ return false;
630
+ }
631
+ if (callee.computed) {
632
+ return false;
633
+ }
634
+ if (property.type !== "Identifier") {
635
+ return false;
636
+ }
637
+ return CONSOLE_METHODS.has(property.name);
638
+ }
639
+ var no_log_only_catch_default = import_utils7.ESLintUtils.RuleCreator(
640
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
641
+ )({
642
+ name: "no-log-only-catch",
643
+ meta: {
644
+ type: "problem",
645
+ docs: {
646
+ description: "Disallow `catch` clauses that only log (or do nothing) and then swallow the error; rethrow or handle it instead."
647
+ },
648
+ schema: [],
649
+ messages: {
650
+ noLogOnlyCatch: "Logging then swallowing the error hides failures. Rethrow the error or handle it for real."
651
+ }
652
+ },
653
+ defaultOptions: [],
654
+ create(context) {
655
+ const filename = context.filename;
656
+ const isIgnoredByDefault = DEFAULT_IGNORE_PATTERNS2.some(
657
+ (re) => re.test(filename)
658
+ );
659
+ if (isIgnoredByDefault) {
660
+ return {};
661
+ }
662
+ return {
663
+ CatchClause(node) {
664
+ const statements = node.body.body;
665
+ if (statements.length === 0) {
666
+ context.report({ node, messageId: "noLogOnlyCatch" });
667
+ return;
668
+ }
669
+ const everyStatementIsConsoleLog = statements.every(
670
+ (statement) => isConsoleCallStatement(statement)
671
+ );
672
+ if (everyStatementIsConsoleLog) {
673
+ context.report({ node, messageId: "noLogOnlyCatch" });
674
+ }
675
+ }
676
+ };
677
+ }
678
+ });
679
+
348
680
  // src/rules/no-raw-env.ts
349
- var import_utils4 = require("@typescript-eslint/utils");
350
- var no_raw_env_default = import_utils4.ESLintUtils.RuleCreator(
681
+ var import_utils8 = require("@typescript-eslint/utils");
682
+ var no_raw_env_default = import_utils8.ESLintUtils.RuleCreator(
351
683
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
352
684
  )({
353
685
  name: "no-raw-env",
@@ -379,8 +711,300 @@ var no_raw_env_default = import_utils4.ESLintUtils.RuleCreator(
379
711
  }
380
712
  });
381
713
 
714
+ // src/rules/no-sentinel-return-on-catch.ts
715
+ var import_utils9 = require("@typescript-eslint/utils");
716
+ function isSentinelArgument(arg) {
717
+ if (arg === null) {
718
+ return false;
719
+ }
720
+ if (arg.type === import_utils9.AST_NODE_TYPES.Literal && arg.value === null) {
721
+ return true;
722
+ }
723
+ if (arg.type === import_utils9.AST_NODE_TYPES.Literal && arg.value === false) {
724
+ return true;
725
+ }
726
+ if (arg.type === import_utils9.AST_NODE_TYPES.Identifier && arg.name === "undefined") {
727
+ return true;
728
+ }
729
+ if (arg.type === import_utils9.AST_NODE_TYPES.ArrayExpression && arg.elements.length === 0) {
730
+ return true;
731
+ }
732
+ if (arg.type === import_utils9.AST_NODE_TYPES.ObjectExpression && arg.properties.length === 0) {
733
+ return true;
734
+ }
735
+ return false;
736
+ }
737
+ function containsThrow(node) {
738
+ let found = false;
739
+ const visit = (current) => {
740
+ if (found) {
741
+ return;
742
+ }
743
+ if (current.type === import_utils9.AST_NODE_TYPES.ThrowStatement) {
744
+ found = true;
745
+ return;
746
+ }
747
+ if (current.type === import_utils9.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils9.AST_NODE_TYPES.FunctionExpression || current.type === import_utils9.AST_NODE_TYPES.ArrowFunctionExpression) {
748
+ return;
749
+ }
750
+ for (const key of Object.keys(current)) {
751
+ if (key === "parent") {
752
+ continue;
753
+ }
754
+ const value = current[key];
755
+ if (Array.isArray(value)) {
756
+ for (const child of value) {
757
+ if (isNode(child)) {
758
+ visit(child);
759
+ }
760
+ }
761
+ } else if (isNode(value)) {
762
+ visit(value);
763
+ }
764
+ }
765
+ };
766
+ visit(node);
767
+ return found;
768
+ }
769
+ function isNode(value) {
770
+ return typeof value === "object" && value !== null && typeof value.type === "string";
771
+ }
772
+ var no_sentinel_return_on_catch_default = import_utils9.ESLintUtils.RuleCreator(
773
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
774
+ )({
775
+ name: "no-sentinel-return-on-catch",
776
+ meta: {
777
+ type: "problem",
778
+ docs: {
779
+ description: "Disallow swallowing a caught error by returning an empty sentinel (`null`, `undefined`, `false`, `[]`, `{}`) as the final statement of a `catch` block."
780
+ },
781
+ schema: [],
782
+ messages: {
783
+ noSentinelReturn: "This `catch` block swallows the error by returning an empty sentinel. Rethrow it, return a typed Result, or handle the error explicitly."
784
+ }
785
+ },
786
+ defaultOptions: [],
787
+ create(context) {
788
+ return {
789
+ CatchClause(node) {
790
+ const body = node.body.body;
791
+ if (body.length === 0) {
792
+ return;
793
+ }
794
+ const last = body[body.length - 1];
795
+ if (last === void 0 || last.type !== import_utils9.AST_NODE_TYPES.ReturnStatement) {
796
+ return;
797
+ }
798
+ if (!isSentinelArgument(last.argument)) {
799
+ return;
800
+ }
801
+ if (containsThrow(node.body)) {
802
+ return;
803
+ }
804
+ context.report({
805
+ node: last,
806
+ messageId: "noSentinelReturn"
807
+ });
808
+ }
809
+ };
810
+ }
811
+ });
812
+
813
+ // src/rules/no-sequential-await.ts
814
+ var import_utils10 = require("@typescript-eslint/utils");
815
+ function isFunctionLike(node) {
816
+ return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
817
+ }
818
+ function isLoop(node) {
819
+ return node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement" || node.type === "WhileStatement" || node.type === "DoWhileStatement";
820
+ }
821
+ var no_sequential_await_default = import_utils10.ESLintUtils.RuleCreator(
822
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
823
+ )({
824
+ name: "no-sequential-await",
825
+ meta: {
826
+ type: "problem",
827
+ docs: {
828
+ description: "Disallow serial `await` inside a loop; use `await Promise.all(...)` to run the operations concurrently."
829
+ },
830
+ schema: [],
831
+ messages: {
832
+ noSequentialAwait: "Avoid `await` inside a loop \u2014 it serializes I/O. Collect the promises and `await Promise.all(xs.map(async (x) => ...))` instead."
833
+ }
834
+ },
835
+ defaultOptions: [],
836
+ create(context) {
837
+ function findAwaitInScope(node) {
838
+ if (node.type === "AwaitExpression") {
839
+ return node;
840
+ }
841
+ if (isFunctionLike(node)) {
842
+ return null;
843
+ }
844
+ for (const key of Object.keys(node)) {
845
+ if (key === "parent") {
846
+ continue;
847
+ }
848
+ const value = node[key];
849
+ if (Array.isArray(value)) {
850
+ for (const child of value) {
851
+ if (isNode2(child) && !isLoop(child)) {
852
+ const found = findAwaitInScope(child);
853
+ if (found) {
854
+ return found;
855
+ }
856
+ }
857
+ }
858
+ } else if (isNode2(value) && !isLoop(value)) {
859
+ const found = findAwaitInScope(value);
860
+ if (found) {
861
+ return found;
862
+ }
863
+ }
864
+ }
865
+ return null;
866
+ }
867
+ function isNode2(value) {
868
+ return typeof value === "object" && value !== null && typeof value.type === "string";
869
+ }
870
+ function checkLoop(node) {
871
+ const parts = [node.body];
872
+ if (node.type === "ForStatement") {
873
+ parts.push(node.init, node.test, node.update);
874
+ } else if (node.type === "ForOfStatement" || node.type === "ForInStatement") {
875
+ parts.push(node.right);
876
+ } else {
877
+ parts.push(node.test);
878
+ }
879
+ for (const part of parts) {
880
+ if (part && !isLoop(part) && findAwaitInScope(part)) {
881
+ context.report({ node, messageId: "noSequentialAwait" });
882
+ return;
883
+ }
884
+ }
885
+ }
886
+ return {
887
+ ForStatement: checkLoop,
888
+ ForInStatement: checkLoop,
889
+ WhileStatement: checkLoop,
890
+ DoWhileStatement: checkLoop,
891
+ ForOfStatement(node) {
892
+ if (node.await) {
893
+ return;
894
+ }
895
+ checkLoop(node);
896
+ }
897
+ };
898
+ }
899
+ });
900
+
901
+ // src/rules/no-string-concat-in-loop.ts
902
+ var import_utils11 = require("@typescript-eslint/utils");
903
+ var LOOP_NODE_TYPES = /* @__PURE__ */ new Set([
904
+ "ForStatement",
905
+ "ForOfStatement",
906
+ "ForInStatement",
907
+ "WhileStatement",
908
+ "DoWhileStatement"
909
+ ]);
910
+ function isStringLiteralInit(node) {
911
+ if (node === null) {
912
+ return false;
913
+ }
914
+ if (node.type === "TemplateLiteral") {
915
+ return true;
916
+ }
917
+ if (node.type === "Literal") {
918
+ return typeof node.value === "string";
919
+ }
920
+ return false;
921
+ }
922
+ function findVariable(scope, name) {
923
+ let current = scope;
924
+ while (current !== null) {
925
+ const variable = current.variables.find((v) => v.name === name);
926
+ if (variable !== void 0) {
927
+ return variable;
928
+ }
929
+ current = current.upper;
930
+ }
931
+ return void 0;
932
+ }
933
+ function isStringInitializedVariable(variable) {
934
+ if (variable.defs.length !== 1) {
935
+ return false;
936
+ }
937
+ const def = variable.defs[0];
938
+ if (def === void 0 || def.type !== "Variable") {
939
+ return false;
940
+ }
941
+ const declarator = def.node;
942
+ if (declarator.type !== "VariableDeclarator") {
943
+ return false;
944
+ }
945
+ return isStringLiteralInit(declarator.init);
946
+ }
947
+ function isInsideLoopBody(node) {
948
+ let child = node;
949
+ let parent = node.parent;
950
+ while (parent !== void 0 && parent !== null) {
951
+ if (LOOP_NODE_TYPES.has(parent.type)) {
952
+ const loop = parent;
953
+ if (loop.body === child) {
954
+ return true;
955
+ }
956
+ }
957
+ child = parent;
958
+ parent = parent.parent;
959
+ }
960
+ return false;
961
+ }
962
+ var no_string_concat_in_loop_default = import_utils11.ESLintUtils.RuleCreator(
963
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
964
+ )({
965
+ name: "no-string-concat-in-loop",
966
+ meta: {
967
+ type: "suggestion",
968
+ docs: {
969
+ description: "Disallow O(n^2) string building via `+=` on a string variable inside a loop; push parts to an array and `join` instead."
970
+ },
971
+ schema: [],
972
+ messages: {
973
+ noStringConcatInLoop: 'Avoid building a string with `+=` inside a loop \u2014 this is O(n^2). Push the parts onto an array and use `arr.join("")` after the loop.'
974
+ }
975
+ },
976
+ defaultOptions: [],
977
+ create(context) {
978
+ return {
979
+ AssignmentExpression(node) {
980
+ if (node.operator !== "+=") {
981
+ return;
982
+ }
983
+ if (node.left.type !== "Identifier") {
984
+ return;
985
+ }
986
+ if (!isInsideLoopBody(node)) {
987
+ return;
988
+ }
989
+ const scope = context.sourceCode.getScope(node);
990
+ const variable = findVariable(scope, node.left.name);
991
+ if (variable === void 0) {
992
+ return;
993
+ }
994
+ if (!isStringInitializedVariable(variable)) {
995
+ return;
996
+ }
997
+ context.report({
998
+ node,
999
+ messageId: "noStringConcatInLoop"
1000
+ });
1001
+ }
1002
+ };
1003
+ }
1004
+ });
1005
+
382
1006
  // src/rules/no-unnecessary-use-client.ts
383
- var import_utils5 = require("@typescript-eslint/utils");
1007
+ var import_utils12 = require("@typescript-eslint/utils");
384
1008
  var HOOK_REGEX = /^use([A-Z]|$)/;
385
1009
  var EVENT_PROP_REGEX = /^on[A-Z]/;
386
1010
  var ERROR_FILE_REGEX = /\b(?:global-)?error\.[jt]sx?$/;
@@ -403,16 +1027,16 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
403
1027
  ]);
404
1028
  var CLIENT_ONLY_PACKAGES_REGEX = /^(?:@radix-ui\/|framer-motion|react-dom|react-day-picker|@floating-ui\/|react-select|react-toastify|react-hook-form|recharts|react-dropzone|react-slick|react-swipeable|react-resizable|react-draggable|react-beautiful-dnd|@hello-pangea\/dnd|react-virtualized|react-window|@tanstack\/react-table|@tanstack\/react-query|react-redux|recoil|jotai|zustand|@tippyjs\/react|react-color|react-datepicker|next-themes|react-helmet|react-helmet-async|styled-components|@emotion\/)/;
405
1029
  var isUseClientDirective = (node) => {
406
- return node.type === import_utils5.AST_NODE_TYPES.ExpressionStatement && node.expression.type === import_utils5.AST_NODE_TYPES.Literal && node.expression.value === "use client";
1030
+ return node.type === import_utils12.AST_NODE_TYPES.ExpressionStatement && node.expression.type === import_utils12.AST_NODE_TYPES.Literal && node.expression.value === "use client";
407
1031
  };
408
1032
  var isGlobalReference = (node, context) => {
409
1033
  if (!BROWSER_GLOBALS.has(node.name)) return false;
410
1034
  const parent = node.parent;
411
1035
  if (parent !== void 0) {
412
- if (parent.type === import_utils5.AST_NODE_TYPES.MemberExpression && parent.property === node && !parent.computed) {
1036
+ if (parent.type === import_utils12.AST_NODE_TYPES.MemberExpression && parent.property === node && !parent.computed) {
413
1037
  return false;
414
1038
  }
415
- if (parent.type === import_utils5.AST_NODE_TYPES.Property && parent.key === node && !parent.computed) {
1039
+ if (parent.type === import_utils12.AST_NODE_TYPES.Property && parent.key === node && !parent.computed) {
416
1040
  return false;
417
1041
  }
418
1042
  if (parent.type.startsWith("TS")) {
@@ -429,7 +1053,7 @@ var isGlobalReference = (node, context) => {
429
1053
  }
430
1054
  return true;
431
1055
  };
432
- var no_unnecessary_use_client_default = import_utils5.ESLintUtils.RuleCreator(
1056
+ var no_unnecessary_use_client_default = import_utils12.ESLintUtils.RuleCreator(
433
1057
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
434
1058
  )({
435
1059
  name: "no-unnecessary-use-client",
@@ -452,13 +1076,13 @@ var no_unnecessary_use_client_default = import_utils5.ESLintUtils.RuleCreator(
452
1076
  let directiveNode = null;
453
1077
  let hasClientIndicator = false;
454
1078
  const markIfHookOrContext = (callee) => {
455
- if (callee.type === import_utils5.AST_NODE_TYPES.Identifier) {
1079
+ if (callee.type === import_utils12.AST_NODE_TYPES.Identifier) {
456
1080
  if (HOOK_REGEX.test(callee.name) || callee.name === "createContext") {
457
1081
  hasClientIndicator = true;
458
1082
  }
459
1083
  return;
460
1084
  }
461
- if (callee.type === import_utils5.AST_NODE_TYPES.MemberExpression && callee.property.type === import_utils5.AST_NODE_TYPES.Identifier) {
1085
+ if (callee.type === import_utils12.AST_NODE_TYPES.MemberExpression && callee.property.type === import_utils12.AST_NODE_TYPES.Identifier) {
462
1086
  const name = callee.property.name;
463
1087
  if (HOOK_REGEX.test(name) || name === "createContext") {
464
1088
  hasClientIndicator = true;
@@ -468,7 +1092,7 @@ var no_unnecessary_use_client_default = import_utils5.ESLintUtils.RuleCreator(
468
1092
  return {
469
1093
  Program(node) {
470
1094
  for (const stmt of node.body) {
471
- if (stmt.type !== import_utils5.AST_NODE_TYPES.ExpressionStatement) break;
1095
+ if (stmt.type !== import_utils12.AST_NODE_TYPES.ExpressionStatement) break;
472
1096
  if (isUseClientDirective(stmt)) {
473
1097
  directiveNode = stmt;
474
1098
  break;
@@ -476,35 +1100,43 @@ var no_unnecessary_use_client_default = import_utils5.ESLintUtils.RuleCreator(
476
1100
  }
477
1101
  },
478
1102
  CallExpression(node) {
1103
+ if (directiveNode === null) return;
479
1104
  markIfHookOrContext(node.callee);
480
1105
  },
481
1106
  JSXAttribute(node) {
482
- if (node.name.type === import_utils5.AST_NODE_TYPES.JSXIdentifier && EVENT_PROP_REGEX.test(node.name.name)) {
1107
+ if (directiveNode === null) return;
1108
+ if (node.name.type === import_utils12.AST_NODE_TYPES.JSXIdentifier && EVENT_PROP_REGEX.test(node.name.name)) {
483
1109
  hasClientIndicator = true;
484
1110
  }
485
1111
  },
486
1112
  ImportDeclaration(node) {
1113
+ if (directiveNode === null) return;
487
1114
  if (typeof node.source.value === "string" && CLIENT_ONLY_PACKAGES_REGEX.test(node.source.value)) {
488
1115
  hasClientIndicator = true;
489
1116
  }
490
1117
  },
491
1118
  ExportNamedDeclaration(node) {
1119
+ if (directiveNode === null) return;
492
1120
  if (node.source !== null) {
493
1121
  hasClientIndicator = true;
494
1122
  }
495
1123
  },
496
1124
  ExportAllDeclaration(node) {
1125
+ if (directiveNode === null) return;
497
1126
  if (node.source !== null) {
498
1127
  hasClientIndicator = true;
499
1128
  }
500
1129
  },
501
1130
  ClassDeclaration() {
1131
+ if (directiveNode === null) return;
502
1132
  hasClientIndicator = true;
503
1133
  },
504
1134
  ClassExpression() {
1135
+ if (directiveNode === null) return;
505
1136
  hasClientIndicator = true;
506
1137
  },
507
1138
  Identifier(node) {
1139
+ if (directiveNode === null) return;
508
1140
  if (isGlobalReference(node, context)) {
509
1141
  hasClientIndicator = true;
510
1142
  }
@@ -521,14 +1153,98 @@ var no_unnecessary_use_client_default = import_utils5.ESLintUtils.RuleCreator(
521
1153
  }
522
1154
  });
523
1155
 
1156
+ // src/rules/prefer-discriminated-union.ts
1157
+ var import_utils13 = require("@typescript-eslint/utils");
1158
+ var import_utils14 = require("@typescript-eslint/utils");
1159
+ var STATUS_MEMBER_NAMES = /* @__PURE__ */ new Set([
1160
+ "success",
1161
+ "ok",
1162
+ "error",
1163
+ "failed",
1164
+ "isError"
1165
+ ]);
1166
+ var MIN_OPTIONAL_MEMBERS = 2;
1167
+ function getMemberName(member) {
1168
+ if (member.type !== import_utils14.AST_NODE_TYPES.TSPropertySignature) {
1169
+ return null;
1170
+ }
1171
+ const { key } = member;
1172
+ if (key.type === import_utils14.AST_NODE_TYPES.Identifier) {
1173
+ return key.name;
1174
+ }
1175
+ if (key.type === import_utils14.AST_NODE_TYPES.Literal && typeof key.value === "string") {
1176
+ return key.value;
1177
+ }
1178
+ return null;
1179
+ }
1180
+ function isBooleanTyped(member) {
1181
+ return member.typeAnnotation?.typeAnnotation.type === import_utils14.AST_NODE_TYPES.TSBooleanKeyword;
1182
+ }
1183
+ function looksLikeMutuallyExclusiveState(typeLiteral) {
1184
+ let hasStatusBoolean = false;
1185
+ let optionalCount = 0;
1186
+ for (const member of typeLiteral.members) {
1187
+ if (member.type !== import_utils14.AST_NODE_TYPES.TSPropertySignature) {
1188
+ continue;
1189
+ }
1190
+ if (member.optional) {
1191
+ optionalCount += 1;
1192
+ }
1193
+ const name = getMemberName(member);
1194
+ if (name !== null && STATUS_MEMBER_NAMES.has(name) && isBooleanTyped(member)) {
1195
+ hasStatusBoolean = true;
1196
+ }
1197
+ }
1198
+ return hasStatusBoolean && optionalCount >= MIN_OPTIONAL_MEMBERS;
1199
+ }
1200
+ var prefer_discriminated_union_default = import_utils13.ESLintUtils.RuleCreator(
1201
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
1202
+ )({
1203
+ name: "prefer-discriminated-union",
1204
+ meta: {
1205
+ type: "suggestion",
1206
+ docs: {
1207
+ description: "Flag object types with a boolean status flag and many optionals; model them as a discriminated union instead."
1208
+ },
1209
+ schema: [],
1210
+ messages: {
1211
+ preferDiscriminatedUnion: "This object type uses a boolean status flag alongside several optional fields, which lets illegal states be representable. Model it as a `z.discriminatedUnion` / discriminated union (e.g. `{ ok: true; data: T } | { ok: false; error: E }`) to make illegal states unrepresentable."
1212
+ }
1213
+ },
1214
+ defaultOptions: [],
1215
+ create(context) {
1216
+ function checkTypeLiteral(typeLiteral, reportNode) {
1217
+ if (looksLikeMutuallyExclusiveState(typeLiteral)) {
1218
+ context.report({
1219
+ node: reportNode,
1220
+ messageId: "preferDiscriminatedUnion"
1221
+ });
1222
+ }
1223
+ }
1224
+ return {
1225
+ TSInterfaceDeclaration(node) {
1226
+ const synthetic = {
1227
+ ...node.body,
1228
+ type: import_utils14.AST_NODE_TYPES.TSTypeLiteral,
1229
+ members: node.body.body
1230
+ };
1231
+ checkTypeLiteral(synthetic, node);
1232
+ },
1233
+ "TSTypeAliasDeclaration > TSTypeLiteral"(node) {
1234
+ checkTypeLiteral(node, node.parent);
1235
+ }
1236
+ };
1237
+ }
1238
+ });
1239
+
524
1240
  // src/rules/prefer-schema-for-api-payload.ts
525
- var import_utils6 = require("@typescript-eslint/utils");
1241
+ var import_utils15 = require("@typescript-eslint/utils");
526
1242
  var unwrap = (node) => {
527
1243
  let current = node;
528
1244
  while (current !== null && current !== void 0) {
529
- if (current.type === import_utils6.AST_NODE_TYPES.TSAsExpression || current.type === import_utils6.AST_NODE_TYPES.TSTypeAssertion || current.type === import_utils6.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils6.AST_NODE_TYPES.TSSatisfiesExpression) {
1245
+ if (current.type === import_utils15.AST_NODE_TYPES.TSAsExpression || current.type === import_utils15.AST_NODE_TYPES.TSTypeAssertion || current.type === import_utils15.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils15.AST_NODE_TYPES.TSSatisfiesExpression) {
530
1246
  current = current.expression;
531
- } else if (current.type === import_utils6.AST_NODE_TYPES.ChainExpression) {
1247
+ } else if (current.type === import_utils15.AST_NODE_TYPES.ChainExpression) {
532
1248
  current = current.expression;
533
1249
  } else {
534
1250
  break;
@@ -539,20 +1255,20 @@ var unwrap = (node) => {
539
1255
  var isJsonCall = (node) => {
540
1256
  let current = unwrap(node);
541
1257
  if (current === null) return false;
542
- if (current.type === import_utils6.AST_NODE_TYPES.AwaitExpression) {
1258
+ if (current.type === import_utils15.AST_NODE_TYPES.AwaitExpression) {
543
1259
  current = unwrap(current.argument);
544
1260
  }
545
- if (current === null || current.type !== import_utils6.AST_NODE_TYPES.CallExpression) {
1261
+ if (current === null || current.type !== import_utils15.AST_NODE_TYPES.CallExpression) {
546
1262
  return false;
547
1263
  }
548
1264
  const callee = unwrap(current.callee);
549
- if (callee === null || callee.type !== import_utils6.AST_NODE_TYPES.MemberExpression) {
1265
+ if (callee === null || callee.type !== import_utils15.AST_NODE_TYPES.MemberExpression) {
550
1266
  return false;
551
1267
  }
552
1268
  const property = unwrap(callee.property);
553
- return property !== null && property.type === import_utils6.AST_NODE_TYPES.Identifier && property.name === "json";
1269
+ return property !== null && property.type === import_utils15.AST_NODE_TYPES.Identifier && property.name === "json";
554
1270
  };
555
- var findVariable = (scope, name) => {
1271
+ var findVariable2 = (scope, name) => {
556
1272
  let current = scope;
557
1273
  while (current !== null) {
558
1274
  const variable = current.set.get(name);
@@ -563,13 +1279,13 @@ var findVariable = (scope, name) => {
563
1279
  };
564
1280
  var isUnvalidatedVariableRef = (node, scope, tracked) => {
565
1281
  const unwrapped = unwrap(node);
566
- if (unwrapped === null || unwrapped.type !== import_utils6.AST_NODE_TYPES.Identifier) {
1282
+ if (unwrapped === null || unwrapped.type !== import_utils15.AST_NODE_TYPES.Identifier) {
567
1283
  return false;
568
1284
  }
569
- const variable = findVariable(scope, unwrapped.name);
1285
+ const variable = findVariable2(scope, unwrapped.name);
570
1286
  return variable !== null && tracked.has(variable);
571
1287
  };
572
- var prefer_schema_for_api_payload_default = import_utils6.ESLintUtils.RuleCreator(
1288
+ var prefer_schema_for_api_payload_default = import_utils15.ESLintUtils.RuleCreator(
573
1289
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
574
1290
  )({
575
1291
  name: "prefer-schema-for-api-payload",
@@ -597,11 +1313,11 @@ var prefer_schema_for_api_payload_default = import_utils6.ESLintUtils.RuleCreato
597
1313
  return {
598
1314
  VariableDeclarator(node) {
599
1315
  const scope = context.sourceCode.getScope(node);
600
- if (node.id.type === import_utils6.AST_NODE_TYPES.Identifier) {
1316
+ if (node.id.type === import_utils15.AST_NODE_TYPES.Identifier) {
601
1317
  trackInitializer(node);
602
1318
  return;
603
1319
  }
604
- if (node.id.type === import_utils6.AST_NODE_TYPES.ObjectPattern || node.id.type === import_utils6.AST_NODE_TYPES.ArrayPattern) {
1320
+ if (node.id.type === import_utils15.AST_NODE_TYPES.ObjectPattern || node.id.type === import_utils15.AST_NODE_TYPES.ArrayPattern) {
605
1321
  if (isJsonCall(node.init)) {
606
1322
  context.report({ node: node.id, messageId: "unparsedJsonAccess" });
607
1323
  return;
@@ -613,8 +1329,8 @@ var prefer_schema_for_api_payload_default = import_utils6.ESLintUtils.RuleCreato
613
1329
  },
614
1330
  AssignmentExpression(node) {
615
1331
  const scope = context.sourceCode.getScope(node);
616
- if (node.left.type === import_utils6.AST_NODE_TYPES.Identifier) {
617
- const variable = findVariable(scope, node.left.name);
1332
+ if (node.left.type === import_utils15.AST_NODE_TYPES.Identifier) {
1333
+ const variable = findVariable2(scope, node.left.name);
618
1334
  if (variable === null) return;
619
1335
  if (isJsonCall(node.right)) {
620
1336
  unvalidatedVariables.add(variable);
@@ -623,7 +1339,7 @@ var prefer_schema_for_api_payload_default = import_utils6.ESLintUtils.RuleCreato
623
1339
  }
624
1340
  return;
625
1341
  }
626
- if (node.left.type === import_utils6.AST_NODE_TYPES.ObjectPattern || node.left.type === import_utils6.AST_NODE_TYPES.ArrayPattern) {
1342
+ if (node.left.type === import_utils15.AST_NODE_TYPES.ObjectPattern || node.left.type === import_utils15.AST_NODE_TYPES.ArrayPattern) {
627
1343
  if (isJsonCall(node.right)) {
628
1344
  context.report({
629
1345
  node: node.left,
@@ -644,15 +1360,15 @@ var prefer_schema_for_api_payload_default = import_utils6.ESLintUtils.RuleCreato
644
1360
  const obj = unwrap(node.object);
645
1361
  if (isJsonCall(obj)) {
646
1362
  const parent = node.parent;
647
- if (parent.type === import_utils6.AST_NODE_TYPES.CallExpression && parent.callee === node && node.property.type === import_utils6.AST_NODE_TYPES.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
1363
+ if (parent.type === import_utils15.AST_NODE_TYPES.CallExpression && parent.callee === node && node.property.type === import_utils15.AST_NODE_TYPES.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
648
1364
  return;
649
1365
  }
650
1366
  context.report({ node, messageId: "unparsedJsonAccess" });
651
1367
  return;
652
1368
  }
653
- if (obj !== null && obj.type === import_utils6.AST_NODE_TYPES.Identifier && isUnvalidatedVariableRef(obj, scope, unvalidatedVariables)) {
1369
+ if (obj !== null && obj.type === import_utils15.AST_NODE_TYPES.Identifier && isUnvalidatedVariableRef(obj, scope, unvalidatedVariables)) {
654
1370
  context.report({ node, messageId: "unparsedJsonAccess" });
655
- const variable = findVariable(scope, obj.name);
1371
+ const variable = findVariable2(scope, obj.name);
656
1372
  if (variable !== null) {
657
1373
  unvalidatedVariables.delete(variable);
658
1374
  }
@@ -662,8 +1378,153 @@ var prefer_schema_for_api_payload_default = import_utils6.ESLintUtils.RuleCreato
662
1378
  }
663
1379
  });
664
1380
 
1381
+ // src/rules/prefer-semantic-colors.ts
1382
+ var import_utils16 = require("@typescript-eslint/utils");
1383
+
1384
+ // src/rules/_tailwind.ts
1385
+ var tailwindBase = (token) => token.replace(/^(?:[a-z0-9-]+:)+/i, "").replace(/^!/, "");
1386
+ var classTokens = (value) => value.split(/\s+/).filter(Boolean);
1387
+
1388
+ // src/rules/prefer-semantic-colors.ts
1389
+ var COLOR_PREFIXES = "text|bg|border(?:-[trblxyse])?|ring(?:-offset)?|fill|stroke|from|via|to|divide|decoration|placeholder|accent|caret|shadow|outline";
1390
+ var PALETTE = "red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone";
1391
+ var COLOR_FN = "rgba?|hsla?|hwb|oklch|oklab|lab|lch|color";
1392
+ var RAW_PALETTE_RE = new RegExp(`^(?:${COLOR_PREFIXES})-(?:${PALETTE})-\\d{2,3}(?:/\\d{1,3})?$`);
1393
+ var ARBITRARY_COLOR_RE = new RegExp(
1394
+ `^(?:${COLOR_PREFIXES})-\\[(?:#[0-9a-fA-F]{3,8}|(?:${COLOR_FN})\\([^\\]]*\\))\\]$`,
1395
+ "i"
1396
+ );
1397
+ var CLASS_FNS = /* @__PURE__ */ new Set(["cn", "clsx", "cva", "tv", "cx", "twMerge", "classnames", "classNames"]);
1398
+ var CLASS_NAME_RE = /class/i;
1399
+ var STYLE_COLOR_PROPS = /* @__PURE__ */ new Set([
1400
+ "color",
1401
+ "background",
1402
+ "backgroundColor",
1403
+ "borderColor",
1404
+ "borderTopColor",
1405
+ "borderRightColor",
1406
+ "borderBottomColor",
1407
+ "borderLeftColor",
1408
+ "outlineColor",
1409
+ "caretColor",
1410
+ "textDecorationColor",
1411
+ "columnRuleColor",
1412
+ "fill",
1413
+ "stroke",
1414
+ "stopColor",
1415
+ "floodColor",
1416
+ "lightingColor"
1417
+ ]);
1418
+ var RAW_COLOR_VALUE_RE = new RegExp(`#[0-9a-fA-F]{3,8}\\b|\\b(?:${COLOR_FN})\\s*\\(`, "i");
1419
+ var propName = (key) => {
1420
+ if (key.type === import_utils16.AST_NODE_TYPES.Identifier) return key.name;
1421
+ if (key.type === import_utils16.AST_NODE_TYPES.Literal && typeof key.value === "string") return key.value;
1422
+ return null;
1423
+ };
1424
+ var prefer_semantic_colors_default = import_utils16.ESLintUtils.RuleCreator(
1425
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
1426
+ )({
1427
+ name: "prefer-semantic-colors",
1428
+ meta: {
1429
+ type: "suggestion",
1430
+ docs: {
1431
+ 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."
1432
+ },
1433
+ schema: [],
1434
+ messages: {
1435
+ rawPalette: "Raw palette class '{{class}}' \u2014 use a semantic token (e.g. text-foreground, bg-primary, text-destructive, bg-muted).",
1436
+ arbitraryColor: "Hardcoded color '{{class}}' \u2014 use a semantic token, or var(--\u2026). For charts/brand add an eslint-disable with a reason.",
1437
+ inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason."
1438
+ }
1439
+ },
1440
+ defaultOptions: [],
1441
+ create(context) {
1442
+ const reportClasses = (value, node) => {
1443
+ for (const token of classTokens(value)) {
1444
+ const base = tailwindBase(token);
1445
+ if (RAW_PALETTE_RE.test(base)) {
1446
+ context.report({ node, messageId: "rawPalette", data: { class: token } });
1447
+ } else if (ARBITRARY_COLOR_RE.test(base)) {
1448
+ context.report({ node, messageId: "arbitraryColor", data: { class: token } });
1449
+ }
1450
+ }
1451
+ };
1452
+ const checkClassNode = (node) => {
1453
+ if (node === null) return;
1454
+ switch (node.type) {
1455
+ case import_utils16.AST_NODE_TYPES.Literal:
1456
+ if (typeof node.value === "string") reportClasses(node.value, node);
1457
+ break;
1458
+ case import_utils16.AST_NODE_TYPES.TemplateLiteral:
1459
+ for (const quasi of node.quasis) reportClasses(quasi.value.cooked ?? "", quasi);
1460
+ break;
1461
+ case import_utils16.AST_NODE_TYPES.ArrayExpression:
1462
+ for (const element of node.elements) {
1463
+ if (element !== null && element.type !== import_utils16.AST_NODE_TYPES.SpreadElement) checkClassNode(element);
1464
+ }
1465
+ break;
1466
+ case import_utils16.AST_NODE_TYPES.ObjectExpression:
1467
+ for (const property of node.properties) {
1468
+ if (property.type === import_utils16.AST_NODE_TYPES.Property) checkClassNode(property.value);
1469
+ }
1470
+ break;
1471
+ case import_utils16.AST_NODE_TYPES.ConditionalExpression:
1472
+ checkClassNode(node.consequent);
1473
+ checkClassNode(node.alternate);
1474
+ break;
1475
+ case import_utils16.AST_NODE_TYPES.LogicalExpression:
1476
+ checkClassNode(node.right);
1477
+ break;
1478
+ default:
1479
+ break;
1480
+ }
1481
+ };
1482
+ const checkColorValueNode = (node) => {
1483
+ if (node.type === import_utils16.AST_NODE_TYPES.Literal && typeof node.value === "string" && RAW_COLOR_VALUE_RE.test(node.value)) {
1484
+ context.report({ node, messageId: "inlineColor", data: { value: node.value } });
1485
+ }
1486
+ };
1487
+ return {
1488
+ "JSXAttribute[name.name='className']"(node) {
1489
+ if (node.value === null) return;
1490
+ if (node.value.type === import_utils16.AST_NODE_TYPES.Literal) checkClassNode(node.value);
1491
+ else if (node.value.type === import_utils16.AST_NODE_TYPES.JSXExpressionContainer) {
1492
+ if (node.value.expression.type !== import_utils16.AST_NODE_TYPES.JSXEmptyExpression) {
1493
+ checkClassNode(node.value.expression);
1494
+ }
1495
+ }
1496
+ },
1497
+ CallExpression(node) {
1498
+ if (node.callee.type === import_utils16.AST_NODE_TYPES.Identifier && CLASS_FNS.has(node.callee.name)) {
1499
+ for (const arg of node.arguments) {
1500
+ if (arg.type !== import_utils16.AST_NODE_TYPES.SpreadElement) checkClassNode(arg);
1501
+ }
1502
+ }
1503
+ },
1504
+ VariableDeclarator(node) {
1505
+ if (node.id.type === import_utils16.AST_NODE_TYPES.Identifier && CLASS_NAME_RE.test(node.id.name)) {
1506
+ checkClassNode(node.init);
1507
+ }
1508
+ },
1509
+ Property(node) {
1510
+ const name = propName(node.key);
1511
+ if (name !== null && CLASS_NAME_RE.test(name)) checkClassNode(node.value);
1512
+ },
1513
+ // SVG presentation attributes: <path fill="#000" stroke="#fff" />
1514
+ "JSXAttribute[name.name=/^(fill|stroke|color)$/]"(node) {
1515
+ if (node.value?.type === import_utils16.AST_NODE_TYPES.Literal) checkColorValueNode(node.value);
1516
+ },
1517
+ // Inline style objects: style={{ color: "#111827", backgroundColor: "#fff" }}
1518
+ "JSXAttribute[name.name='style'] ObjectExpression > Property"(node) {
1519
+ const name = propName(node.key);
1520
+ if (name !== null && STYLE_COLOR_PROPS.has(name)) checkColorValueNode(node.value);
1521
+ }
1522
+ };
1523
+ }
1524
+ });
1525
+
665
1526
  // src/rules/prefer-server-actions.ts
666
- var import_utils7 = require("@typescript-eslint/utils");
1527
+ var import_utils17 = require("@typescript-eslint/utils");
667
1528
  var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
668
1529
  var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
669
1530
  var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
@@ -723,7 +1584,7 @@ function isMutationMethod(node, context) {
723
1584
  }
724
1585
  return false;
725
1586
  }
726
- function getPropertyNode(objNode, propName) {
1587
+ function getPropertyNode(objNode, propName2) {
727
1588
  if (!objNode || objNode.type !== "ObjectExpression") return null;
728
1589
  for (const prop of objNode.properties) {
729
1590
  if (prop.type !== "Property") continue;
@@ -733,7 +1594,7 @@ function getPropertyNode(objNode, propName) {
733
1594
  } else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
734
1595
  keyName = prop.key.value;
735
1596
  }
736
- if (keyName === propName) {
1597
+ if (keyName === propName2) {
737
1598
  if (prop.value.type === "AssignmentPattern" || prop.value.type === "ArrayPattern" || prop.value.type === "ObjectPattern") {
738
1599
  return null;
739
1600
  }
@@ -742,7 +1603,7 @@ function getPropertyNode(objNode, propName) {
742
1603
  }
743
1604
  return null;
744
1605
  }
745
- var prefer_server_actions_default = import_utils7.ESLintUtils.RuleCreator(
1606
+ var prefer_server_actions_default = import_utils17.ESLintUtils.RuleCreator(
746
1607
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
747
1608
  )({
748
1609
  name: "prefer-server-actions",
@@ -781,7 +1642,10 @@ var prefer_server_actions_default = import_utils7.ESLintUtils.RuleCreator(
781
1642
  const methodName = node.callee.property.name.toLowerCase();
782
1643
  if (AXIOS_MUTATION_METHODS.has(methodName)) {
783
1644
  const urlArg = node.arguments[0];
784
- if (urlArg && urlArg.type !== "SpreadElement" && isApiUrl(urlArg, context)) {
1645
+ const hasHandlerArg = node.arguments.some(
1646
+ (arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression"
1647
+ );
1648
+ if (urlArg && urlArg.type !== "SpreadElement" && !hasHandlerArg && isApiUrl(urlArg, context)) {
785
1649
  isMutation = true;
786
1650
  }
787
1651
  }
@@ -807,14 +1671,14 @@ var prefer_server_actions_default = import_utils7.ESLintUtils.RuleCreator(
807
1671
  });
808
1672
 
809
1673
  // src/rules/prefer-shadcn.ts
810
- var import_utils8 = require("@typescript-eslint/utils");
1674
+ var import_utils18 = require("@typescript-eslint/utils");
811
1675
  var REPLACEMENTS = {
812
1676
  input: "Input",
813
1677
  select: "Select",
814
1678
  textarea: "Textarea",
815
1679
  dialog: "Dialog"
816
1680
  };
817
- var prefer_shadcn_default = import_utils8.ESLintUtils.RuleCreator(
1681
+ var prefer_shadcn_default = import_utils18.ESLintUtils.RuleCreator(
818
1682
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
819
1683
  )({
820
1684
  name: "prefer-shadcn",
@@ -855,36 +1719,52 @@ var prefer_shadcn_default = import_utils8.ESLintUtils.RuleCreator(
855
1719
  });
856
1720
 
857
1721
  // src/rules/require-assert-never.ts
858
- var import_utils9 = require("@typescript-eslint/utils");
1722
+ var import_utils19 = require("@typescript-eslint/utils");
859
1723
  var isAssertNeverCall = (expression) => {
860
- if (expression.type !== import_utils9.AST_NODE_TYPES.CallExpression) return false;
1724
+ if (expression.type !== import_utils19.AST_NODE_TYPES.CallExpression) return false;
861
1725
  const callee = expression.callee;
862
- return callee.type === import_utils9.AST_NODE_TYPES.Identifier && callee.name === "assertNever";
1726
+ if (callee.type === import_utils19.AST_NODE_TYPES.Identifier) {
1727
+ return callee.name === "assertNever";
1728
+ }
1729
+ if (callee.type === import_utils19.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils19.AST_NODE_TYPES.Identifier) {
1730
+ return callee.property.name === "assertNever";
1731
+ }
1732
+ return false;
863
1733
  };
864
1734
  var statementContainsAssertNever = (statement) => {
865
- if (statement.type === import_utils9.AST_NODE_TYPES.ExpressionStatement) {
1735
+ if (statement.type === import_utils19.AST_NODE_TYPES.ExpressionStatement) {
866
1736
  return isAssertNeverCall(statement.expression);
867
1737
  }
868
- if (statement.type === import_utils9.AST_NODE_TYPES.ThrowStatement) {
1738
+ if (statement.type === import_utils19.AST_NODE_TYPES.ThrowStatement) {
869
1739
  return isAssertNeverCall(statement.argument);
870
1740
  }
871
- if (statement.type === import_utils9.AST_NODE_TYPES.BlockStatement) {
1741
+ if (statement.type === import_utils19.AST_NODE_TYPES.ReturnStatement) {
1742
+ return statement.argument !== null && isAssertNeverCall(statement.argument);
1743
+ }
1744
+ if (statement.type === import_utils19.AST_NODE_TYPES.BlockStatement) {
872
1745
  return statement.body.some(statementContainsAssertNever);
873
1746
  }
874
1747
  return false;
875
1748
  };
876
- var require_assert_never_default = import_utils9.ESLintUtils.RuleCreator(
1749
+ var isRuntimeHandlingStatement = (statement) => {
1750
+ if (statement.type === import_utils19.AST_NODE_TYPES.EmptyStatement) return false;
1751
+ if (statement.type === import_utils19.AST_NODE_TYPES.BlockStatement) {
1752
+ return statement.body.some(isRuntimeHandlingStatement);
1753
+ }
1754
+ return true;
1755
+ };
1756
+ var require_assert_never_default = import_utils19.ESLintUtils.RuleCreator(
877
1757
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
878
1758
  )({
879
1759
  name: "require-assert-never",
880
1760
  meta: {
881
1761
  type: "problem",
882
1762
  docs: {
883
- description: "Require switch statements to end with `assertNever(_)` in their default case so that discriminated unions are exhaustively checked at compile time."
1763
+ description: "Require an exhaustive-style switch whose `default` case does no runtime work to call `assertNever(_)` so that discriminated unions are exhaustively checked at compile time. Switches with a legitimate runtime default (a reducer's `return state`, an HTTP-status `return fallback()`, a `break`, a `throw`, etc.) are left alone."
884
1764
  },
885
1765
  schema: [],
886
1766
  messages: {
887
- missingAssertNever: "Switch statement default case must call assertNever() for exhaustive type checking"
1767
+ missingAssertNever: "Empty switch `default` case \u2014 add runtime handling or call `assertNever()` so the discriminated union is exhaustively checked at compile time."
888
1768
  }
889
1769
  },
890
1770
  defaultOptions: [],
@@ -895,10 +1775,8 @@ var require_assert_never_default = import_utils9.ESLintUtils.RuleCreator(
895
1775
  (caseNode) => caseNode.test === null
896
1776
  );
897
1777
  if (!defaultCase) return;
898
- const hasAssertNever = defaultCase.consequent.some(
899
- statementContainsAssertNever
900
- );
901
- if (hasAssertNever) return;
1778
+ if (defaultCase.consequent.some(statementContainsAssertNever)) return;
1779
+ if (defaultCase.consequent.some(isRuntimeHandlingStatement)) return;
902
1780
  context.report({
903
1781
  node: defaultCase,
904
1782
  messageId: "missingAssertNever"
@@ -909,43 +1787,91 @@ var require_assert_never_default = import_utils9.ESLintUtils.RuleCreator(
909
1787
  });
910
1788
 
911
1789
  // src/rules/require-zod-form-validation.ts
912
- var import_utils10 = require("@typescript-eslint/utils");
913
- var isFormDataGetCall = (node) => {
914
- const callee = node.callee;
915
- if (callee.type !== import_utils10.AST_NODE_TYPES.MemberExpression) return false;
916
- if (callee.property.type !== import_utils10.AST_NODE_TYPES.Identifier || callee.property.name !== "get") {
1790
+ var import_utils20 = require("@typescript-eslint/utils");
1791
+ var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
1792
+ var looksLikeZodSchema = (node) => {
1793
+ let current = node;
1794
+ while (true) {
1795
+ if (current.type === import_utils20.AST_NODE_TYPES.Identifier) {
1796
+ return current.name === "z" || ZOD_SCHEMA_NAME_RE.test(current.name);
1797
+ }
1798
+ if (current.type === import_utils20.AST_NODE_TYPES.CallExpression) {
1799
+ current = current.callee;
1800
+ continue;
1801
+ }
1802
+ if (current.type === import_utils20.AST_NODE_TYPES.MemberExpression) {
1803
+ current = current.object;
1804
+ continue;
1805
+ }
917
1806
  return false;
918
1807
  }
919
- return callee.object.type === import_utils10.AST_NODE_TYPES.Identifier && callee.object.name === "formData";
920
1808
  };
921
- var isParseCallExpression = (node) => {
922
- if (node.type !== import_utils10.AST_NODE_TYPES.CallExpression) return false;
1809
+ var isZodParseCall = (node) => {
1810
+ if (node.type !== import_utils20.AST_NODE_TYPES.CallExpression) return false;
923
1811
  const callee = node.callee;
924
- if (callee.type !== import_utils10.AST_NODE_TYPES.MemberExpression) return false;
925
- return callee.property.type === import_utils10.AST_NODE_TYPES.Identifier && callee.property.name === "parse";
1812
+ if (callee.type !== import_utils20.AST_NODE_TYPES.MemberExpression) return false;
1813
+ if (callee.computed) return false;
1814
+ if (callee.property.type !== import_utils20.AST_NODE_TYPES.Identifier) return false;
1815
+ const method = callee.property.name;
1816
+ if (method !== "parse" && method !== "safeParse") return false;
1817
+ return looksLikeZodSchema(callee.object);
926
1818
  };
927
- var require_zod_form_validation_default = import_utils10.ESLintUtils.RuleCreator(
1819
+ var isFormDataMethodCall = (node) => {
1820
+ let current = node;
1821
+ if (current.type === import_utils20.AST_NODE_TYPES.AwaitExpression) {
1822
+ current = current.argument;
1823
+ }
1824
+ if (current.type !== import_utils20.AST_NODE_TYPES.CallExpression) return false;
1825
+ const callee = current.callee;
1826
+ return callee.type === import_utils20.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils20.AST_NODE_TYPES.Identifier && callee.property.name === "formData";
1827
+ };
1828
+ var require_zod_form_validation_default = import_utils20.ESLintUtils.RuleCreator(
928
1829
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
929
1830
  )({
930
1831
  name: "require-zod-form-validation",
931
1832
  meta: {
932
1833
  type: "problem",
933
1834
  docs: {
934
- description: "Require Zod validation (`Schema.parse(...)`) when reading values out of a `FormData` object."
1835
+ description: "Require Zod validation (`Schema.parse(...)` / `Schema.safeParse(...)`) when reading values out of a `FormData` object."
935
1836
  },
936
1837
  schema: [],
937
1838
  messages: {
938
- missingZodValidation: "FormData parsing must use Zod schema validation (e.g., Schema.parse())"
1839
+ missingZodValidation: "FormData parsing must use Zod schema validation (e.g., Schema.parse() / Schema.safeParse())"
939
1840
  }
940
1841
  },
941
1842
  defaultOptions: [],
942
1843
  create(context) {
1844
+ const isFormSourceIdentifier = (node) => {
1845
+ if (node.type !== import_utils20.AST_NODE_TYPES.Identifier) return false;
1846
+ if (/formdata/i.test(node.name)) return true;
1847
+ let scope = context.sourceCode.getScope(node);
1848
+ while (scope !== null) {
1849
+ const variable = scope.set.get(node.name);
1850
+ if (variable !== void 0 && variable.defs.length === 1) {
1851
+ const def = variable.defs[0];
1852
+ if (def !== void 0 && def.type === "Variable" && def.node.type === import_utils20.AST_NODE_TYPES.VariableDeclarator && def.node.init !== null) {
1853
+ return isFormDataMethodCall(def.node.init);
1854
+ }
1855
+ return false;
1856
+ }
1857
+ scope = scope.upper;
1858
+ }
1859
+ return false;
1860
+ };
1861
+ const isFormDataGetCall = (node) => {
1862
+ const callee = node.callee;
1863
+ if (callee.type !== import_utils20.AST_NODE_TYPES.MemberExpression) return false;
1864
+ if (callee.property.type !== import_utils20.AST_NODE_TYPES.Identifier || callee.property.name !== "get") {
1865
+ return false;
1866
+ }
1867
+ return isFormSourceIdentifier(callee.object);
1868
+ };
943
1869
  return {
944
1870
  CallExpression(node) {
945
1871
  if (!isFormDataGetCall(node)) return;
946
1872
  let parent = node.parent;
947
1873
  while (parent !== null && parent !== void 0) {
948
- if (isParseCallExpression(parent)) return;
1874
+ if (isZodParseCall(parent)) return;
949
1875
  parent = parent.parent;
950
1876
  }
951
1877
  context.report({
@@ -958,15 +1884,15 @@ var require_zod_form_validation_default = import_utils10.ESLintUtils.RuleCreator
958
1884
  });
959
1885
 
960
1886
  // src/rules/zod-naming-convention.ts
961
- var import_utils11 = require("@typescript-eslint/utils");
1887
+ var import_utils21 = require("@typescript-eslint/utils");
962
1888
  var calleeChainStartsWithZ = (node) => {
963
1889
  let current = node;
964
- while (current.type === import_utils11.AST_NODE_TYPES.MemberExpression) {
1890
+ while (current.type === import_utils21.AST_NODE_TYPES.MemberExpression) {
965
1891
  const receiver = current.object;
966
- if (receiver.type === import_utils11.AST_NODE_TYPES.Identifier && receiver.name === "z") {
1892
+ if (receiver.type === import_utils21.AST_NODE_TYPES.Identifier && receiver.name === "z") {
967
1893
  return true;
968
1894
  }
969
- if (receiver.type === import_utils11.AST_NODE_TYPES.CallExpression) {
1895
+ if (receiver.type === import_utils21.AST_NODE_TYPES.CallExpression) {
970
1896
  current = receiver.callee;
971
1897
  continue;
972
1898
  }
@@ -974,7 +1900,7 @@ var calleeChainStartsWithZ = (node) => {
974
1900
  }
975
1901
  return false;
976
1902
  };
977
- var zod_naming_convention_default = import_utils11.ESLintUtils.RuleCreator(
1903
+ var zod_naming_convention_default = import_utils21.ESLintUtils.RuleCreator(
978
1904
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
979
1905
  )({
980
1906
  name: "zod-naming-convention",
@@ -994,11 +1920,11 @@ var zod_naming_convention_default = import_utils11.ESLintUtils.RuleCreator(
994
1920
  VariableDeclarator(node) {
995
1921
  const init = node.init;
996
1922
  if (init === null || init === void 0) return;
997
- if (init.type !== import_utils11.AST_NODE_TYPES.CallExpression) return;
1923
+ if (init.type !== import_utils21.AST_NODE_TYPES.CallExpression) return;
998
1924
  const callee = init.callee;
999
- if (callee.type !== import_utils11.AST_NODE_TYPES.MemberExpression) return;
1925
+ if (callee.type !== import_utils21.AST_NODE_TYPES.MemberExpression) return;
1000
1926
  if (!calleeChainStartsWithZ(callee)) return;
1001
- if (node.id.type !== import_utils11.AST_NODE_TYPES.Identifier) return;
1927
+ if (node.id.type !== import_utils21.AST_NODE_TYPES.Identifier) return;
1002
1928
  const variableName = node.id.name;
1003
1929
  if (variableName.startsWith("Z")) return;
1004
1930
  context.report({
@@ -1014,10 +1940,19 @@ var zod_naming_convention_default = import_utils11.ESLintUtils.RuleCreator(
1014
1940
  var rules = {
1015
1941
  "enforce-file-structure": enforce_file_structure_default,
1016
1942
  "no-client-side-data-fetching": no_client_side_data_fetching_default,
1943
+ "no-comment-cruft": no_comment_cruft_default,
1017
1944
  "no-enum": no_enum_default,
1945
+ "no-insecure-random-id": no_insecure_random_id_default,
1946
+ "no-json-stringify-error": no_json_stringify_error_default,
1947
+ "no-log-only-catch": no_log_only_catch_default,
1018
1948
  "no-raw-env": no_raw_env_default,
1949
+ "no-sentinel-return-on-catch": no_sentinel_return_on_catch_default,
1950
+ "no-sequential-await": no_sequential_await_default,
1951
+ "no-string-concat-in-loop": no_string_concat_in_loop_default,
1019
1952
  "no-unnecessary-use-client": no_unnecessary_use_client_default,
1953
+ "prefer-discriminated-union": prefer_discriminated_union_default,
1020
1954
  "prefer-schema-for-api-payload": prefer_schema_for_api_payload_default,
1955
+ "prefer-semantic-colors": prefer_semantic_colors_default,
1021
1956
  "prefer-server-actions": prefer_server_actions_default,
1022
1957
  "prefer-shadcn": prefer_shadcn_default,
1023
1958
  "require-assert-never": require_assert_never_default,
@@ -1027,7 +1962,7 @@ var rules = {
1027
1962
  var plugin = {
1028
1963
  meta: {
1029
1964
  name: "@sarj/eslint-plugin",
1030
- version: "2.0.0"
1965
+ version: "2.2.0"
1031
1966
  },
1032
1967
  rules,
1033
1968
  configs: {
@@ -1041,7 +1976,18 @@ var plugin = {
1041
1976
  "@sarj/no-client-side-data-fetching": "warn",
1042
1977
  "@sarj/prefer-server-actions": "warn",
1043
1978
  "@sarj/no-unnecessary-use-client": "warn",
1044
- "@sarj/prefer-schema-for-api-payload": "warn"
1979
+ "@sarj/prefer-schema-for-api-payload": "warn",
1980
+ // Distilled from sarj-audit skills — warn in recommended, error in strict.
1981
+ "@sarj/no-sequential-await": "warn",
1982
+ "@sarj/no-sentinel-return-on-catch": "warn",
1983
+ "@sarj/no-log-only-catch": "warn",
1984
+ "@sarj/no-insecure-random-id": "warn",
1985
+ "@sarj/no-json-stringify-error": "warn",
1986
+ "@sarj/no-string-concat-in-loop": "warn",
1987
+ "@sarj/prefer-discriminated-union": "warn",
1988
+ "@sarj/no-comment-cruft": "warn",
1989
+ // Frontend / styling — distilled from frontend PR-review mining.
1990
+ "@sarj/prefer-semantic-colors": "warn"
1045
1991
  }
1046
1992
  },
1047
1993
  strict: {
@@ -1057,7 +2003,19 @@ var plugin = {
1057
2003
  "@sarj/no-client-side-data-fetching": "error",
1058
2004
  "@sarj/prefer-server-actions": "error",
1059
2005
  "@sarj/no-unnecessary-use-client": "error",
1060
- "@sarj/prefer-schema-for-api-payload": "error"
2006
+ "@sarj/prefer-schema-for-api-payload": "error",
2007
+ // Distilled from sarj-audit skills.
2008
+ "@sarj/no-sequential-await": "error",
2009
+ "@sarj/no-sentinel-return-on-catch": "error",
2010
+ "@sarj/no-log-only-catch": "error",
2011
+ "@sarj/no-insecure-random-id": "error",
2012
+ "@sarj/no-json-stringify-error": "error",
2013
+ "@sarj/no-string-concat-in-loop": "error",
2014
+ "@sarj/prefer-discriminated-union": "error",
2015
+ "@sarj/no-comment-cruft": "error",
2016
+ // Frontend / styling — distilled from frontend PR-review mining. Stylistic,
2017
+ // no autofix → warn (rollout should prove the FP rate before raising it).
2018
+ "@sarj/prefer-semantic-colors": "warn"
1061
2019
  }
1062
2020
  }
1063
2021
  }