@sarj/eslint-plugin 2.7.0 → 2.9.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.js CHANGED
@@ -316,12 +316,10 @@ var no_comment_cruft_default = ESLintUtils3.RuleCreator(
316
316
  }
317
317
  const first = leading[0];
318
318
  if (first === void 0 || leading.length < LEADING_PREAMBLE_MIN) return;
319
- const isLicense = leading.some(
320
- (c) => LICENSE_RE.test(stripCommentMarker(c.value))
321
- );
322
- if (!isLicense) {
323
- context.report({ node: first, messageId: "fileHeaderPreamble" });
324
- }
319
+ const bodies = leading.map((c) => stripCommentMarker(c.value));
320
+ if (bodies.some((body) => LICENSE_RE.test(body))) return;
321
+ if (bodies.some((body) => isProse(body))) return;
322
+ context.report({ node: first, messageId: "fileHeaderPreamble" });
325
323
  }
326
324
  return {
327
325
  Program() {
@@ -799,35 +797,15 @@ var LOGGER_NAMES = /* @__PURE__ */ new Set([
799
797
  "_logger",
800
798
  "_log"
801
799
  ]);
800
+ var LOGGER_FACTORIES = /* @__PURE__ */ new Set([
801
+ "getlogger",
802
+ "get_logger"
803
+ ]);
802
804
  var REPORT_NAME_RE = /error|report|capture|log|trace|warn/i;
803
- function isLoggerReceiver(expr) {
804
- switch (expr.type) {
805
- case "Identifier":
806
- return LOGGER_NAMES.has(expr.name.toLowerCase());
807
- case "MemberExpression": {
808
- const { property, object } = expr;
809
- if (!expr.computed && property.type === "Identifier" && LOGGER_NAMES.has(property.name.toLowerCase())) {
810
- return true;
811
- }
812
- return isLoggerReceiver(object);
813
- }
814
- default:
815
- return false;
816
- }
817
- }
818
- function isLoggingCall(expr) {
819
- if (expr.type !== "CallExpression") {
820
- return false;
821
- }
822
- const callee = expr.callee;
823
- if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
824
- return false;
825
- }
826
- if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
827
- return false;
828
- }
829
- return isLoggerReceiver(callee.object);
830
- }
805
+ var LOGGING_OPTION_PROPERTIES = {
806
+ loggerNames: { type: "array", items: { type: "string" } },
807
+ logFunctions: { type: "array", items: { type: "string" } }
808
+ };
831
809
  function calleeName(callee) {
832
810
  if (callee.type === "Identifier") {
833
811
  return callee.name;
@@ -837,6 +815,65 @@ function calleeName(callee) {
837
815
  }
838
816
  return null;
839
817
  }
818
+ function createLogMatcher(options = {}) {
819
+ const loggerNames = /* @__PURE__ */ new Set([
820
+ ...LOGGER_NAMES,
821
+ ...(options.loggerNames ?? []).map((name) => name.toLowerCase())
822
+ ]);
823
+ const logFunctions = new Set(options.logFunctions ?? []);
824
+ function isLoggerReceiver(expr) {
825
+ switch (expr.type) {
826
+ case "Identifier":
827
+ return loggerNames.has(expr.name.toLowerCase());
828
+ case "MemberExpression": {
829
+ const { property, object } = expr;
830
+ if (!expr.computed && property.type === "Identifier") {
831
+ const lowered = property.name.toLowerCase();
832
+ if (loggerNames.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
833
+ return true;
834
+ }
835
+ }
836
+ return isLoggerReceiver(object);
837
+ }
838
+ case "CallExpression": {
839
+ const callee = expr.callee;
840
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
841
+ return true;
842
+ }
843
+ if (callee.type !== "Super") {
844
+ return isLoggerReceiver(callee);
845
+ }
846
+ return false;
847
+ }
848
+ default:
849
+ return false;
850
+ }
851
+ }
852
+ function isLogFunctionCall(expr) {
853
+ if (expr.type !== "CallExpression" || logFunctions.size === 0) {
854
+ return false;
855
+ }
856
+ const name = calleeName(expr.callee);
857
+ return name !== null && logFunctions.has(name);
858
+ }
859
+ function isLoggingCall(expr) {
860
+ if (expr.type !== "CallExpression") {
861
+ return false;
862
+ }
863
+ if (isLogFunctionCall(expr)) {
864
+ return true;
865
+ }
866
+ const callee = expr.callee;
867
+ if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
868
+ return false;
869
+ }
870
+ if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
871
+ return false;
872
+ }
873
+ return isLoggerReceiver(callee.object);
874
+ }
875
+ return { isLoggerReceiver, isLogFunctionCall, isLoggingCall };
876
+ }
840
877
 
841
878
  // src/rules/no-log-only-catch.ts
842
879
  var DEFAULT_IGNORE_PATTERNS2 = [
@@ -844,12 +881,6 @@ var DEFAULT_IGNORE_PATTERNS2 = [
844
881
  /\.spec\./,
845
882
  /[\\/]__tests__[\\/]/
846
883
  ];
847
- function isLoggingCallStatement(statement) {
848
- if (statement.type !== "ExpressionStatement") {
849
- return false;
850
- }
851
- return isLoggingCall(statement.expression);
852
- }
853
884
  var no_log_only_catch_default = ESLintUtils7.RuleCreator(
854
885
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
855
886
  )({
@@ -859,15 +890,28 @@ var no_log_only_catch_default = ESLintUtils7.RuleCreator(
859
890
  docs: {
860
891
  description: "Disallow `catch` clauses that only log (or silently do nothing) and then swallow the error; rethrow or handle it instead."
861
892
  },
862
- schema: [],
893
+ schema: [
894
+ {
895
+ type: "object",
896
+ additionalProperties: false,
897
+ properties: { ...LOGGING_OPTION_PROPERTIES }
898
+ }
899
+ ],
863
900
  messages: {
864
901
  noLogOnlyCatch: "Logging then swallowing the error hides failures. Rethrow the error or handle it for real.",
865
902
  emptyCatch: "Empty catch silently swallows the error. Rethrow it, handle it, or add a comment explaining why it is safe to ignore."
866
903
  }
867
904
  },
868
- defaultOptions: [],
869
- create(context) {
905
+ defaultOptions: [{}],
906
+ create(context, [loggingOptions]) {
907
+ const matcher = createLogMatcher(loggingOptions);
870
908
  const filename = context.filename;
909
+ function isLoggingCallStatement(statement) {
910
+ if (statement.type !== "ExpressionStatement") {
911
+ return false;
912
+ }
913
+ return matcher.isLoggingCall(statement.expression);
914
+ }
871
915
  const isIgnoredByDefault = DEFAULT_IGNORE_PATTERNS2.some(
872
916
  (re) => re.test(filename)
873
917
  );
@@ -1040,46 +1084,131 @@ function containsThrow(node) {
1040
1084
  (current) => current.type === AST_NODE_TYPES3.ThrowStatement
1041
1085
  );
1042
1086
  }
1043
- function argsIncludeBinding(args, caughtName) {
1044
- if (caughtName === null) {
1045
- return false;
1087
+ function bindsName(param, name) {
1088
+ switch (param.type) {
1089
+ case AST_NODE_TYPES3.Identifier:
1090
+ return param.name === name;
1091
+ case AST_NODE_TYPES3.AssignmentPattern:
1092
+ return bindsName(param.left, name);
1093
+ case AST_NODE_TYPES3.RestElement:
1094
+ return bindsName(param.argument, name);
1095
+ case AST_NODE_TYPES3.ArrayPattern:
1096
+ return param.elements.some(
1097
+ (element) => element !== null && bindsName(element, name)
1098
+ );
1099
+ case AST_NODE_TYPES3.ObjectPattern:
1100
+ return param.properties.some(
1101
+ (property) => property.type === AST_NODE_TYPES3.RestElement ? bindsName(property.argument, name) : bindsName(property.value, name)
1102
+ );
1103
+ default:
1104
+ return false;
1046
1105
  }
1047
- return args.some(
1048
- (arg) => arg.type === AST_NODE_TYPES3.Identifier && arg.name === caughtName
1049
- );
1050
1106
  }
1051
- function logsOrReportsError(catchBody, caughtName) {
1052
- return walkWithinScope(catchBody, (current) => {
1053
- if (current.type !== AST_NODE_TYPES3.CallExpression) {
1054
- return false;
1107
+ function subtreeReadsName(node, name) {
1108
+ let found = false;
1109
+ const shadowsName = (fn) => isFunctionNode(fn) && fn.params.some(
1110
+ (param) => bindsName(param, name)
1111
+ );
1112
+ const recurse = (current) => {
1113
+ if (found) {
1114
+ return;
1055
1115
  }
1056
- if (isLoggingCall(current)) {
1057
- return true;
1116
+ if (current.type === AST_NODE_TYPES3.Identifier && current.name === name) {
1117
+ found = true;
1118
+ return;
1058
1119
  }
1059
- const name = calleeName(current.callee);
1060
- return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
1061
- });
1120
+ if (shadowsName(current)) {
1121
+ return;
1122
+ }
1123
+ for (const key of Object.keys(current)) {
1124
+ if (key === "parent") {
1125
+ continue;
1126
+ }
1127
+ if (key === "key" && current.type === AST_NODE_TYPES3.Property && !current.computed) {
1128
+ continue;
1129
+ }
1130
+ if (key === "property" && current.type === AST_NODE_TYPES3.MemberExpression && !current.computed) {
1131
+ continue;
1132
+ }
1133
+ const value = current[key];
1134
+ if (Array.isArray(value)) {
1135
+ for (const child of value) {
1136
+ if (isNode(child)) {
1137
+ recurse(child);
1138
+ }
1139
+ }
1140
+ } else if (isNode(value)) {
1141
+ recurse(value);
1142
+ }
1143
+ }
1144
+ };
1145
+ recurse(node);
1146
+ return found;
1147
+ }
1148
+ function argsIncludeBinding(args, caughtName) {
1149
+ if (caughtName === null) {
1150
+ return false;
1151
+ }
1152
+ return args.some((arg) => subtreeReadsName(arg, caughtName));
1062
1153
  }
1063
1154
  function tryBlockOf(catchNode) {
1064
1155
  return catchNode.parent.block;
1065
1156
  }
1066
- function isSafeParseExpression(arg) {
1067
- if (arg === null) {
1068
- return false;
1069
- }
1070
- if (arg.type === AST_NODE_TYPES3.CallExpression && arg.callee.type === AST_NODE_TYPES3.MemberExpression && !arg.callee.computed && arg.callee.property.type === AST_NODE_TYPES3.Identifier && arg.callee.property.name === "parse") {
1071
- return true;
1157
+ var SAFE_PARSE_CONSTRUCTORS = /* @__PURE__ */ new Set([
1158
+ "RegExp",
1159
+ "URL",
1160
+ "URLPattern"
1161
+ ]);
1162
+ function isParseShapedNode(node) {
1163
+ if (node.type === AST_NODE_TYPES3.CallExpression && node.callee.type === AST_NODE_TYPES3.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES3.Identifier) {
1164
+ return node.callee.property.name === "parse";
1072
1165
  }
1073
- if (arg.type === AST_NODE_TYPES3.NewExpression && arg.callee.type === AST_NODE_TYPES3.Identifier) {
1074
- return arg.callee.name === "RegExp" || arg.callee.name === "URL";
1166
+ if (node.type === AST_NODE_TYPES3.NewExpression && node.callee.type === AST_NODE_TYPES3.Identifier) {
1167
+ return SAFE_PARSE_CONSTRUCTORS.has(node.callee.name);
1075
1168
  }
1076
1169
  return false;
1077
1170
  }
1171
+ var BODY_DECODE_METHODS = /* @__PURE__ */ new Set([
1172
+ "json",
1173
+ "text",
1174
+ "arrayBuffer"
1175
+ ]);
1176
+ function isBodyDecodeNode(node) {
1177
+ return node.type === AST_NODE_TYPES3.CallExpression && node.callee.type === AST_NODE_TYPES3.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES3.Identifier && BODY_DECODE_METHODS.has(node.callee.property.name);
1178
+ }
1179
+ function returnsMatching(stmt, predicate) {
1180
+ return stmt.type === AST_NODE_TYPES3.ReturnStatement && stmt.argument !== null && walkWithinScope(stmt.argument, predicate);
1181
+ }
1182
+ function enclosingReturnTypeNode(node) {
1183
+ let current = node.parent;
1184
+ while (current !== void 0 && current !== null) {
1185
+ if (isFunctionNode(current) && "returnType" in current) {
1186
+ return current.returnType?.typeAnnotation ?? null;
1187
+ }
1188
+ current = current.parent;
1189
+ }
1190
+ return null;
1191
+ }
1192
+ function isDeclaredBooleanPredicate(catchNode, kind) {
1193
+ if (kind !== "boolean") {
1194
+ return false;
1195
+ }
1196
+ let declared = enclosingReturnTypeNode(catchNode);
1197
+ if (declared?.type === AST_NODE_TYPES3.TSTypeReference && declared.typeName.type === AST_NODE_TYPES3.Identifier && declared.typeName.name === "Promise") {
1198
+ declared = declared.typeArguments?.params[0] ?? null;
1199
+ }
1200
+ return declared?.type === AST_NODE_TYPES3.TSBooleanKeyword;
1201
+ }
1078
1202
  function tryReturnsSafeParse(catchNode) {
1079
- return walkWithinScope(
1080
- tryBlockOf(catchNode),
1081
- (current) => current.type === AST_NODE_TYPES3.ReturnStatement && isSafeParseExpression(current.argument)
1082
- );
1203
+ const tryBlock = tryBlockOf(catchNode);
1204
+ if (walkWithinScope(
1205
+ tryBlock,
1206
+ (current) => returnsMatching(current, isParseShapedNode)
1207
+ )) {
1208
+ return true;
1209
+ }
1210
+ const only = tryBlock.body.length === 1 ? tryBlock.body[0] : void 0;
1211
+ return only !== void 0 && returnsMatching(only, isBodyDecodeNode);
1083
1212
  }
1084
1213
  function enclosingFunctionBody(node) {
1085
1214
  let current = node.parent;
@@ -1125,13 +1254,32 @@ var no_sentinel_return_on_catch_default = ESLintUtils9.RuleCreator(
1125
1254
  docs: {
1126
1255
  description: "Disallow swallowing a caught error by returning an empty sentinel (`null`, `undefined`, `false`, `[]`, `{}`) as the final statement of a `catch` block, unless the error is logged/reported or the sentinel is the declared safe-parse/predicate contract."
1127
1256
  },
1128
- schema: [],
1257
+ schema: [
1258
+ {
1259
+ type: "object",
1260
+ additionalProperties: false,
1261
+ properties: { ...LOGGING_OPTION_PROPERTIES }
1262
+ }
1263
+ ],
1129
1264
  messages: {
1130
1265
  noSentinelReturn: "This `catch` block swallows the error by returning an empty sentinel without logging it. Rethrow it, log/report it, or return a typed Result."
1131
1266
  }
1132
1267
  },
1133
- defaultOptions: [],
1134
- create(context) {
1268
+ defaultOptions: [{}],
1269
+ create(context, [loggingOptions]) {
1270
+ const matcher = createLogMatcher(loggingOptions);
1271
+ function logsOrReportsError(catchBody, caughtName) {
1272
+ return walkWithinScope(catchBody, (current) => {
1273
+ if (current.type !== AST_NODE_TYPES3.CallExpression) {
1274
+ return false;
1275
+ }
1276
+ if (matcher.isLoggingCall(current)) {
1277
+ return true;
1278
+ }
1279
+ const name = calleeName(current.callee);
1280
+ return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
1281
+ });
1282
+ }
1135
1283
  return {
1136
1284
  CatchClause(node) {
1137
1285
  const body = node.body.body;
@@ -1156,6 +1304,9 @@ var no_sentinel_return_on_catch_default = ESLintUtils9.RuleCreator(
1156
1304
  return;
1157
1305
  }
1158
1306
  const kind = sentinelKind(last.argument);
1307
+ if (kind !== null && isDeclaredBooleanPredicate(node, kind)) {
1308
+ return;
1309
+ }
1159
1310
  if (kind !== null && functionReturnsSameSentinelKindElsewhere(node, kind)) {
1160
1311
  return;
1161
1312
  }
@@ -1260,6 +1411,28 @@ function isThreadedAccumulator(node) {
1260
1411
  }
1261
1412
  return referencesName(node.argument, target);
1262
1413
  }
1414
+ function namesReadBy(test) {
1415
+ const names = /* @__PURE__ */ new Set();
1416
+ visitScope(test, (node) => {
1417
+ if (node.type === "Identifier") {
1418
+ names.add(node.name);
1419
+ }
1420
+ });
1421
+ return names;
1422
+ }
1423
+ function testStateIsAssignedInBody(test, body) {
1424
+ const testNames = namesReadBy(test);
1425
+ if (testNames.size === 0) {
1426
+ return false;
1427
+ }
1428
+ let found = false;
1429
+ visitScope(body, (node) => {
1430
+ if (node.type === "AssignmentExpression" && node.operator === "=" && node.left.type === "Identifier" && testNames.has(node.left.name)) {
1431
+ found = true;
1432
+ }
1433
+ });
1434
+ return found;
1435
+ }
1263
1436
  function shouldReport(awaits, earlyExit, iterableText) {
1264
1437
  if (awaits.length === 0) {
1265
1438
  return false;
@@ -1306,6 +1479,9 @@ var no_sequential_await_default = ESLintUtils10.RuleCreator(
1306
1479
  return null;
1307
1480
  }
1308
1481
  function checkLoop(node) {
1482
+ if ((node.type === "WhileStatement" || node.type === "DoWhileStatement") && testStateIsAssignedInBody(node.test, node.body)) {
1483
+ return;
1484
+ }
1309
1485
  const awaits = [];
1310
1486
  let earlyExit = false;
1311
1487
  for (const part of loopParts(node)) {
@@ -1734,7 +1910,7 @@ var unwrap = (node) => {
1734
1910
  }
1735
1911
  return current ?? null;
1736
1912
  };
1737
- var isJsonCall = (node) => {
1913
+ var isRawPayloadSource = (node) => {
1738
1914
  let current = unwrap(node);
1739
1915
  if (current === null) return false;
1740
1916
  if (current.type === AST_NODE_TYPES6.AwaitExpression) {
@@ -1748,7 +1924,14 @@ var isJsonCall = (node) => {
1748
1924
  return false;
1749
1925
  }
1750
1926
  const property = unwrap(callee.property);
1751
- return property !== null && property.type === AST_NODE_TYPES6.Identifier && property.name === "json";
1927
+ if (property === null || property.type !== AST_NODE_TYPES6.Identifier) {
1928
+ return false;
1929
+ }
1930
+ if (property.name === "json") {
1931
+ return true;
1932
+ }
1933
+ const object = unwrap(callee.object);
1934
+ return property.name === "parse" && object !== null && object.type === AST_NODE_TYPES6.Identifier && object.name === "JSON";
1752
1935
  };
1753
1936
  var findVariable2 = (scope, name) => {
1754
1937
  let current = scope;
@@ -1798,18 +1981,18 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
1798
1981
  meta: {
1799
1982
  type: "problem",
1800
1983
  docs: {
1801
- description: "Require Zod (or similar) schema validation on `response.json()` before property access."
1984
+ description: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access."
1802
1985
  },
1803
1986
  schema: [],
1804
1987
  messages: {
1805
- unparsedJsonAccess: "Property access on the result of `response.json()` without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
1988
+ unparsedJsonAccess: "Property access on an unvalidated payload (`response.json()` / `JSON.parse()`) without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
1806
1989
  }
1807
1990
  },
1808
1991
  defaultOptions: [],
1809
1992
  create(context) {
1810
1993
  const unvalidatedVariables = /* @__PURE__ */ new Set();
1811
1994
  const trackInitializer = (declarator) => {
1812
- if (!isJsonCall(declarator.init)) return;
1995
+ if (!isRawPayloadSource(declarator.init)) return;
1813
1996
  const declaredVars = context.sourceCode.getDeclaredVariables(declarator);
1814
1997
  const variable = declaredVars[0];
1815
1998
  if (variable !== void 0) {
@@ -1824,7 +2007,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
1824
2007
  return;
1825
2008
  }
1826
2009
  if (node.id.type === AST_NODE_TYPES6.ObjectPattern || node.id.type === AST_NODE_TYPES6.ArrayPattern) {
1827
- if (isJsonCall(node.init)) {
2010
+ if (isRawPayloadSource(node.init)) {
1828
2011
  context.report({ node: node.id, messageId: "unparsedJsonAccess" });
1829
2012
  return;
1830
2013
  }
@@ -1838,7 +2021,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
1838
2021
  if (node.left.type === AST_NODE_TYPES6.Identifier) {
1839
2022
  const variable = findVariable2(scope, node.left.name);
1840
2023
  if (variable === null) return;
1841
- if (isJsonCall(node.right)) {
2024
+ if (isRawPayloadSource(node.right)) {
1842
2025
  unvalidatedVariables.add(variable);
1843
2026
  } else {
1844
2027
  unvalidatedVariables.delete(variable);
@@ -1846,7 +2029,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
1846
2029
  return;
1847
2030
  }
1848
2031
  if (node.left.type === AST_NODE_TYPES6.ObjectPattern || node.left.type === AST_NODE_TYPES6.ArrayPattern) {
1849
- if (isJsonCall(node.right)) {
2032
+ if (isRawPayloadSource(node.right)) {
1850
2033
  context.report({
1851
2034
  node: node.left,
1852
2035
  messageId: "unparsedJsonAccess"
@@ -1880,7 +2063,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
1880
2063
  MemberExpression(node) {
1881
2064
  const scope = context.sourceCode.getScope(node);
1882
2065
  const obj = unwrap(node.object);
1883
- if (isJsonCall(obj)) {
2066
+ if (isRawPayloadSource(obj)) {
1884
2067
  const parent = node.parent;
1885
2068
  if (parent.type === AST_NODE_TYPES6.CallExpression && parent.callee === node && node.property.type === AST_NODE_TYPES6.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
1886
2069
  return;
@@ -2397,7 +2580,13 @@ var require_assert_never_default = ESLintUtils18.RuleCreator(
2397
2580
 
2398
2581
  // src/rules/require-zod-form-validation.ts
2399
2582
  import { ESLintUtils as ESLintUtils19, AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
2583
+
2584
+ // src/rules/_zod.ts
2585
+ var ZOD_PREFIX_RE = /^Z[A-Z]/;
2586
+ var ZOD_SUFFIX_RE = /Schema$/;
2400
2587
  var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
2588
+
2589
+ // src/rules/require-zod-form-validation.ts
2401
2590
  var looksLikeZodSchema = (node) => {
2402
2591
  let current = node;
2403
2592
  while (true) {
@@ -2475,14 +2664,38 @@ var require_zod_form_validation_default = ESLintUtils19.RuleCreator(
2475
2664
  }
2476
2665
  return isFormSourceIdentifier(callee.object);
2477
2666
  };
2667
+ const hasZodParseAncestor = (node) => {
2668
+ let parent = node.parent;
2669
+ while (parent !== null && parent !== void 0) {
2670
+ if (isZodParseCall(parent)) return true;
2671
+ parent = parent.parent;
2672
+ }
2673
+ return false;
2674
+ };
2675
+ const isInstanceofNarrowing = (node) => {
2676
+ const parent = node.parent;
2677
+ return parent !== null && parent !== void 0 && parent.type === AST_NODE_TYPES10.BinaryExpression && parent.operator === "instanceof" && parent.left === node;
2678
+ };
2679
+ const boundDeclarator = (node) => {
2680
+ const parent = node.parent;
2681
+ if (parent.type === AST_NODE_TYPES10.VariableDeclarator && parent.init === node && parent.id.type === AST_NODE_TYPES10.Identifier) {
2682
+ return parent;
2683
+ }
2684
+ return null;
2685
+ };
2686
+ const bindingIsValidated = (declarator) => {
2687
+ const variable = context.sourceCode.getDeclaredVariables(declarator)[0];
2688
+ if (variable === void 0) return false;
2689
+ return variable.references.some(
2690
+ (ref) => hasZodParseAncestor(ref.identifier) || isInstanceofNarrowing(ref.identifier)
2691
+ );
2692
+ };
2478
2693
  return {
2479
2694
  CallExpression(node) {
2480
2695
  if (!isFormDataGetCall(node)) return;
2481
- let parent = node.parent;
2482
- while (parent !== null && parent !== void 0) {
2483
- if (isZodParseCall(parent)) return;
2484
- parent = parent.parent;
2485
- }
2696
+ if (hasZodParseAncestor(node) || isInstanceofNarrowing(node)) return;
2697
+ const declarator = boundDeclarator(node);
2698
+ if (declarator !== null && bindingIsValidated(declarator)) return;
2486
2699
  context.report({
2487
2700
  node,
2488
2701
  messageId: "missingZodValidation"
@@ -2494,6 +2707,11 @@ var require_zod_form_validation_default = ESLintUtils19.RuleCreator(
2494
2707
 
2495
2708
  // src/rules/zod-naming-convention.ts
2496
2709
  import { ESLintUtils as ESLintUtils20, AST_NODE_TYPES as AST_NODE_TYPES11 } from "@typescript-eslint/utils";
2710
+ var CONVENTIONS = {
2711
+ prefix: { test: ZOD_PREFIX_RE, messageId: "zPrefix" },
2712
+ suffix: { test: ZOD_SUFFIX_RE, messageId: "schemaSuffix" },
2713
+ either: { test: ZOD_SCHEMA_NAME_RE, messageId: "zodSchemaName" }
2714
+ };
2497
2715
  var calleeChainStartsWithZ = (node) => {
2498
2716
  let current = node;
2499
2717
  while (current.type === AST_NODE_TYPES11.MemberExpression) {
@@ -2516,15 +2734,29 @@ var zod_naming_convention_default = ESLintUtils20.RuleCreator(
2516
2734
  meta: {
2517
2735
  type: "suggestion",
2518
2736
  docs: {
2519
- description: "Enforce Zod schemas to be named with a Z prefix (e.g. `ZUser = z.object({...})`)."
2737
+ description: "Enforce a consistent Zod schema naming convention \u2014 a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default."
2520
2738
  },
2521
- schema: [],
2739
+ schema: [
2740
+ {
2741
+ type: "object",
2742
+ additionalProperties: false,
2743
+ properties: {
2744
+ convention: {
2745
+ type: "string",
2746
+ enum: ["prefix", "suffix", "either"]
2747
+ }
2748
+ }
2749
+ }
2750
+ ],
2522
2751
  messages: {
2523
- zPrefix: "Zod schema names should start with Z"
2752
+ zPrefix: "Zod schema names should start with Z (e.g. `ZUser`)",
2753
+ schemaSuffix: "Zod schema names should end with Schema (e.g. `userSchema`)",
2754
+ zodSchemaName: "Zod schema names should start with Z (`ZUser`) or end with Schema (`userSchema`)"
2524
2755
  }
2525
2756
  },
2526
- defaultOptions: [],
2527
- create(context) {
2757
+ defaultOptions: [{}],
2758
+ create(context, [optionsArg]) {
2759
+ const { test, messageId } = CONVENTIONS[optionsArg?.convention ?? "either"];
2528
2760
  return {
2529
2761
  VariableDeclarator(node) {
2530
2762
  const init = node.init;
@@ -2534,11 +2766,10 @@ var zod_naming_convention_default = ESLintUtils20.RuleCreator(
2534
2766
  if (callee.type !== AST_NODE_TYPES11.MemberExpression) return;
2535
2767
  if (!calleeChainStartsWithZ(callee)) return;
2536
2768
  if (node.id.type !== AST_NODE_TYPES11.Identifier) return;
2537
- const variableName = node.id.name;
2538
- if (variableName.startsWith("Z")) return;
2769
+ if (test.test(node.id.name)) return;
2539
2770
  context.report({
2540
2771
  node: node.id,
2541
- messageId: "zPrefix"
2772
+ messageId
2542
2773
  });
2543
2774
  }
2544
2775
  };
@@ -2759,10 +2990,14 @@ var no_cors_wildcard_with_credentials_default = ESLintUtils21.RuleCreator(
2759
2990
  import { AST_NODE_TYPES as AST_NODE_TYPES12, ESLintUtils as ESLintUtils22 } from "@typescript-eslint/utils";
2760
2991
 
2761
2992
  // src/rules/_paths.ts
2762
- var TEST_FILE_RE = /(\.(test|spec)\.)|([\\/]__tests__[\\/])/;
2763
2993
  var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
2764
2994
  function isTestFile(filename) {
2765
- return TEST_FILE_RE.test(filename);
2995
+ const normalized = filename.replaceAll("\\", "/");
2996
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
2997
+ if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(base)) {
2998
+ return true;
2999
+ }
3000
+ return /(^|\/)(tests?|__tests__|__mocks__|fixtures)\//.test(normalized);
2766
3001
  }
2767
3002
  function isScriptFile(filename) {
2768
3003
  return SCRIPT_FILE_RE.test(filename);
@@ -2928,7 +3163,7 @@ var require_fetch_timeout_default = ESLintUtils23.RuleCreator(
2928
3163
  const variable = ASTUtils.findVariable(scope, identifier.name);
2929
3164
  return variable === null || variable.defs.length === 0;
2930
3165
  }
2931
- function isGlobalFetchCall(callee) {
3166
+ function isGlobalFetchCall2(callee) {
2932
3167
  if (callee.type === AST_NODE_TYPES13.Identifier) {
2933
3168
  return callee.name === "fetch" && resolvesToGlobal(callee);
2934
3169
  }
@@ -2936,7 +3171,7 @@ var require_fetch_timeout_default = ESLintUtils23.RuleCreator(
2936
3171
  }
2937
3172
  return {
2938
3173
  CallExpression(node) {
2939
- if (!isGlobalFetchCall(node.callee)) {
3174
+ if (!isGlobalFetchCall2(node.callee)) {
2940
3175
  return;
2941
3176
  }
2942
3177
  const [first, init] = node.arguments;
@@ -3195,9 +3430,9 @@ function subtreeMatches(stmt, predicate) {
3195
3430
  visit(stmt);
3196
3431
  return found;
3197
3432
  }
3198
- var hasAwait = (stmt) => subtreeMatches(stmt, (n) => n.type === AST_NODE_TYPES15.AwaitExpression);
3199
- var hasThrowingCallOrNew = (stmt) => subtreeMatches(
3200
- stmt,
3433
+ var hasAwait = (node) => subtreeMatches(node, (n) => n.type === AST_NODE_TYPES15.AwaitExpression);
3434
+ var hasThrowingCallOrNew = (node) => subtreeMatches(
3435
+ node,
3201
3436
  (n) => n.type === AST_NODE_TYPES15.CallExpression && !isPureCall(n) || n.type === AST_NODE_TYPES15.NewExpression && !isPureNew(n)
3202
3437
  );
3203
3438
  function unwrap2(expr) {
@@ -3207,13 +3442,22 @@ function unwrap2(expr) {
3207
3442
  }
3208
3443
  return current;
3209
3444
  }
3445
+ function isBareCallStatement(stmt) {
3446
+ return stmt.type === AST_NODE_TYPES15.ExpressionStatement && unwrap2(stmt.expression).type === AST_NODE_TYPES15.CallExpression;
3447
+ }
3210
3448
  function canThrow(stmt) {
3211
3449
  if (hasAwait(stmt)) {
3212
3450
  return true;
3213
3451
  }
3214
- if (stmt.type === AST_NODE_TYPES15.ExpressionStatement && unwrap2(stmt.expression).type === AST_NODE_TYPES15.CallExpression) {
3452
+ if (isBareCallStatement(stmt)) {
3215
3453
  return false;
3216
3454
  }
3455
+ if (stmt.type === AST_NODE_TYPES15.BlockStatement) {
3456
+ return stmt.body.some(canThrow);
3457
+ }
3458
+ if (stmt.type === AST_NODE_TYPES15.IfStatement) {
3459
+ return hasThrowingCallOrNew(stmt.test) || canThrow(stmt.consequent) || stmt.alternate !== null && canThrow(stmt.alternate);
3460
+ }
3217
3461
  return hasThrowingCallOrNew(stmt);
3218
3462
  }
3219
3463
  function handlerRethrows(handler) {
@@ -3266,29 +3510,8 @@ var no_fat_try_blocks_default = ESLintUtils25.RuleCreator(
3266
3510
 
3267
3511
  // src/rules/no-secret-in-log.ts
3268
3512
  import { ESLintUtils as ESLintUtils26 } from "@typescript-eslint/utils";
3269
- var LOG_METHODS2 = /* @__PURE__ */ new Set([
3270
- "debug",
3271
- "info",
3272
- "warn",
3273
- "warning",
3274
- "error",
3275
- "exception",
3276
- "critical",
3277
- "trace",
3278
- "log",
3279
- "fatal",
3280
- "success"
3281
- ]);
3282
- var LOGGER_NAMES2 = /* @__PURE__ */ new Set([
3283
- "logger",
3284
- "log",
3285
- "logging",
3286
- "loguru",
3287
- "console",
3288
- "_logger",
3289
- "_log"
3290
- ]);
3291
- var LOGGER_FACTORIES = /* @__PURE__ */ new Set(["getlogger", "get_logger"]);
3513
+
3514
+ // src/rules/_secret_names.ts
3292
3515
  var SECRET_WORDS = /* @__PURE__ */ new Set([
3293
3516
  "token",
3294
3517
  "secret",
@@ -3304,7 +3527,8 @@ var SECRET_WORDS = /* @__PURE__ */ new Set([
3304
3527
  "hmac",
3305
3528
  "digest",
3306
3529
  "hash",
3307
- "apikey"
3530
+ "apikey",
3531
+ "bearer"
3308
3532
  ]);
3309
3533
  var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
3310
3534
  "count",
@@ -3327,53 +3551,44 @@ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
3327
3551
  "valid",
3328
3552
  "invalid",
3329
3553
  "exists",
3554
+ "type",
3555
+ "types"
3556
+ ]);
3557
+ var DESCRIPTOR_WORDS = /* @__PURE__ */ new Set([
3330
3558
  "type",
3331
3559
  "types",
3332
3560
  "name",
3333
3561
  "names",
3334
- "label",
3335
- "labels",
3336
- "title",
3337
- "expiry",
3338
- "expiration",
3339
- "expires",
3340
- "ttl",
3341
- "version",
3342
- "versions",
3343
- "policy",
3344
- "rotation",
3345
- "arn",
3346
- "path",
3347
- "paths",
3348
- "issuer",
3349
- "audience",
3350
- "strength",
3351
- "manager",
3352
- "service",
3353
- "services",
3354
- "repository",
3355
- "provider",
3356
- "providers",
3357
- "store",
3358
- "factory",
3359
- "handler",
3360
- "controller",
3361
- "bucket",
3362
- "url",
3363
- "uri",
3364
- "endpoint",
3365
- "endpoints",
3366
- "scope",
3367
- "scopes",
3368
- "event",
3369
- "events",
3370
- "format",
3371
- "at",
3372
- "len",
3373
- "length"
3562
+ "id",
3563
+ "ids",
3564
+ "kind",
3565
+ "kinds"
3566
+ ]);
3567
+ var CATEGORY_WORDS = /* @__PURE__ */ new Set(["type", "types", "kind", "kinds"]);
3568
+ var FLAG_PREFIXES = /* @__PURE__ */ new Set([
3569
+ "is",
3570
+ "has",
3571
+ "was",
3572
+ "are",
3573
+ "can",
3574
+ "should"
3575
+ ]);
3576
+ var AUTH_WORDS = /* @__PURE__ */ new Set([
3577
+ "token",
3578
+ "secret",
3579
+ "secrets",
3580
+ "password",
3581
+ "passwd",
3582
+ "passwords",
3583
+ "jwt",
3584
+ "credential",
3585
+ "credentials",
3586
+ "authorization",
3587
+ "signature",
3588
+ "hmac",
3589
+ "apikey",
3590
+ "bearer"
3374
3591
  ]);
3375
- var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
3376
- var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
3377
3592
  var CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
3378
3593
  var SEGMENT_RE = /[^A-Za-z0-9]+/;
3379
3594
  function tokenize(identifier) {
@@ -3389,6 +3604,14 @@ function tokenize(identifier) {
3389
3604
  }
3390
3605
  return tokens;
3391
3606
  }
3607
+ function leadingWord(identifier) {
3608
+ for (const segment of identifier.split(SEGMENT_RE)) {
3609
+ if (segment) {
3610
+ return (segment.match(CAMEL_RE) ?? [segment])[0]?.toLowerCase();
3611
+ }
3612
+ }
3613
+ return void 0;
3614
+ }
3392
3615
  function hasApiKey(tokens) {
3393
3616
  for (let i = 0; i + 1 < tokens.length; i++) {
3394
3617
  if (tokens[i] === "api" && tokens[i + 1] === "key") {
@@ -3397,10 +3620,10 @@ function hasApiKey(tokens) {
3397
3620
  }
3398
3621
  return false;
3399
3622
  }
3400
- function isSecretName(identifier) {
3623
+ function isSecretName(identifier, innocuous = INNOCUOUS_WORDS) {
3401
3624
  const tokens = tokenize(identifier);
3402
3625
  const last = tokens.at(-1);
3403
- if (last !== void 0 && INNOCUOUS_WORDS.has(last)) {
3626
+ if (last !== void 0 && innocuous.has(last)) {
3404
3627
  return false;
3405
3628
  }
3406
3629
  if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
@@ -3408,6 +3631,76 @@ function isSecretName(identifier) {
3408
3631
  }
3409
3632
  return hasApiKey(tokens);
3410
3633
  }
3634
+ function isAuthSecretName(identifier) {
3635
+ if (!isSecretName(identifier)) {
3636
+ return false;
3637
+ }
3638
+ const tokens = tokenize(identifier);
3639
+ const first = leadingWord(identifier);
3640
+ if (first !== void 0 && FLAG_PREFIXES.has(first)) {
3641
+ return false;
3642
+ }
3643
+ const last = tokens.at(-1);
3644
+ if (last !== void 0 && DESCRIPTOR_WORDS.has(last)) {
3645
+ return false;
3646
+ }
3647
+ if (tokens.some((tok) => CATEGORY_WORDS.has(tok))) {
3648
+ return false;
3649
+ }
3650
+ if (tokens.some((tok) => AUTH_WORDS.has(tok))) {
3651
+ return true;
3652
+ }
3653
+ return hasApiKey(tokens);
3654
+ }
3655
+
3656
+ // src/rules/no-secret-in-log.ts
3657
+ var LOG_INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
3658
+ ...INNOCUOUS_WORDS,
3659
+ "name",
3660
+ "names",
3661
+ "label",
3662
+ "labels",
3663
+ "title",
3664
+ "expiry",
3665
+ "expiration",
3666
+ "expires",
3667
+ "ttl",
3668
+ "version",
3669
+ "versions",
3670
+ "policy",
3671
+ "rotation",
3672
+ "arn",
3673
+ "path",
3674
+ "paths",
3675
+ "issuer",
3676
+ "audience",
3677
+ "strength",
3678
+ "manager",
3679
+ "service",
3680
+ "services",
3681
+ "repository",
3682
+ "provider",
3683
+ "providers",
3684
+ "store",
3685
+ "factory",
3686
+ "handler",
3687
+ "controller",
3688
+ "bucket",
3689
+ "url",
3690
+ "uri",
3691
+ "endpoint",
3692
+ "endpoints",
3693
+ "scope",
3694
+ "scopes",
3695
+ "event",
3696
+ "events",
3697
+ "format",
3698
+ "at",
3699
+ "len",
3700
+ "length"
3701
+ ]);
3702
+ var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
3703
+ var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
3411
3704
  function isSecretKeyword(name) {
3412
3705
  if (REDACTION_RE.test(name)) {
3413
3706
  return false;
@@ -3415,35 +3708,7 @@ function isSecretKeyword(name) {
3415
3708
  if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
3416
3709
  return false;
3417
3710
  }
3418
- return isSecretName(name);
3419
- }
3420
- function isLoggerExpr(expr) {
3421
- switch (expr.type) {
3422
- case "Identifier":
3423
- return LOGGER_NAMES2.has(expr.name.toLowerCase());
3424
- case "MemberExpression": {
3425
- const { property, object } = expr;
3426
- if (!expr.computed && property.type === "Identifier") {
3427
- const lowered = property.name.toLowerCase();
3428
- if (LOGGER_NAMES2.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
3429
- return true;
3430
- }
3431
- }
3432
- return isLoggerExpr(object);
3433
- }
3434
- case "CallExpression": {
3435
- const callee = expr.callee;
3436
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
3437
- return true;
3438
- }
3439
- if (callee.type !== "Super") {
3440
- return isLoggerExpr(callee);
3441
- }
3442
- return false;
3443
- }
3444
- default:
3445
- return false;
3446
- }
3711
+ return isSecretName(name, LOG_INNOCUOUS_WORDS);
3447
3712
  }
3448
3713
  function isRawSecretValue(prop) {
3449
3714
  if (prop.shorthand) {
@@ -3472,20 +3737,23 @@ var no_secret_in_log_default = ESLintUtils26.RuleCreator(
3472
3737
  docs: {
3473
3738
  description: "Disallow passing a secret-named value to a logging call; it leaks to log sinks. Redact or omit it."
3474
3739
  },
3475
- schema: [],
3740
+ schema: [
3741
+ {
3742
+ type: "object",
3743
+ additionalProperties: false,
3744
+ properties: { ...LOGGING_OPTION_PROPERTIES }
3745
+ }
3746
+ ],
3476
3747
  messages: {
3477
3748
  noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it."
3478
3749
  }
3479
3750
  },
3480
- defaultOptions: [],
3481
- create(context) {
3751
+ defaultOptions: [{}],
3752
+ create(context, [loggingOptions]) {
3753
+ const matcher = createLogMatcher(loggingOptions);
3482
3754
  return {
3483
3755
  CallExpression(node) {
3484
- const callee = node.callee;
3485
- if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !LOG_METHODS2.has(callee.property.name)) {
3486
- return;
3487
- }
3488
- if (!isLoggerExpr(callee.object)) {
3756
+ if (!matcher.isLoggingCall(node)) {
3489
3757
  return;
3490
3758
  }
3491
3759
  for (const arg of node.arguments) {
@@ -3691,19 +3959,33 @@ var prefer_string_literal_union_default = ESLintUtils28.RuleCreator(
3691
3959
  docs: {
3692
3960
  description: "Flag raw `string` choice fields and string-literal comparison clusters; prefer a string-literal union type."
3693
3961
  },
3694
- schema: [],
3962
+ schema: [
3963
+ {
3964
+ type: "object",
3965
+ additionalProperties: false,
3966
+ properties: {
3967
+ ignoreFields: {
3968
+ type: "array",
3969
+ items: { type: "string" }
3970
+ }
3971
+ }
3972
+ }
3973
+ ],
3695
3974
  messages: {
3696
3975
  bareChoiceField: '`{{name}}: string` looks like a choice field \u2014 prefer a string-literal union type (e.g. `type X = "a" | "b"`). Enums are banned by `no-enum`; use a union.',
3697
3976
  comparisonCluster: '`{{key}}` is compared against a closed set of string literals \u2014 define a string-literal union type (e.g. `type X = "a" | "b"`).'
3698
3977
  }
3699
3978
  },
3700
- defaultOptions: [],
3701
- create(context) {
3979
+ defaultOptions: [{}],
3980
+ create(context, [optionsArg]) {
3702
3981
  const filename = context.filename;
3703
3982
  const sourceText = context.sourceCode.getText();
3704
3983
  if (isIgnoredFile(filename, sourceText)) {
3705
3984
  return {};
3706
3985
  }
3986
+ const ignoredFields = new Set(
3987
+ (optionsArg?.ignoreFields ?? []).map((name) => name.toLowerCase())
3988
+ );
3707
3989
  let services;
3708
3990
  try {
3709
3991
  services = ESLintUtils28.getParserServices(context);
@@ -3751,18 +4033,46 @@ var prefer_string_literal_union_default = ESLintUtils28.RuleCreator(
3751
4033
  function operandIsFlaggable(node) {
3752
4034
  return operandIsRawString(node) && !originIsExternal(services?.esTreeNodeToTSNodeMap.get(node), 0);
3753
4035
  }
3754
- function pushScope() {
3755
- scopeStack.push({ clusters: /* @__PURE__ */ new Map() });
4036
+ function declaredReturnLiterals(fn) {
4037
+ const annotation = fn.returnType?.typeAnnotation;
4038
+ if (annotation === void 0 || services === null) {
4039
+ return null;
4040
+ }
4041
+ const tsNode = services.esTreeNodeToTSNodeMap.get(annotation);
4042
+ if (!ts.isTypeNode(tsNode)) {
4043
+ return null;
4044
+ }
4045
+ const checker = services.program.getTypeChecker();
4046
+ const declared = checker.getTypeFromTypeNode(tsNode);
4047
+ const type = checker.getAwaitedType(declared) ?? declared;
4048
+ const literals = /* @__PURE__ */ new Set();
4049
+ for (const part of type.isUnion() ? type.types : [type]) {
4050
+ if (part.isStringLiteral()) {
4051
+ literals.add(part.value);
4052
+ }
4053
+ }
4054
+ return literals.size >= MIN_CLUSTER_SIZE ? literals : null;
4055
+ }
4056
+ function pushScope(node) {
4057
+ scopeStack.push({ clusters: /* @__PURE__ */ new Map(), fn: node });
3756
4058
  }
3757
4059
  function popScope() {
3758
4060
  const scope = scopeStack.pop();
3759
4061
  if (scope === void 0) {
3760
4062
  return;
3761
4063
  }
3762
- for (const entry of scope.clusters.values()) {
3763
- if (entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE) {
3764
- validClusters.push(entry.node);
4064
+ const candidates = [...scope.clusters.values()].filter(
4065
+ (entry) => entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE
4066
+ );
4067
+ if (candidates.length === 0) {
4068
+ return;
4069
+ }
4070
+ const returnLiterals = scope.fn === null ? null : declaredReturnLiterals(scope.fn);
4071
+ for (const entry of candidates) {
4072
+ if (returnLiterals !== null && [...entry.literals].every((lit) => returnLiterals.has(lit))) {
4073
+ continue;
3765
4074
  }
4075
+ validClusters.push(entry.node);
3766
4076
  }
3767
4077
  }
3768
4078
  function accumulate(key, literals, node) {
@@ -3794,7 +4104,7 @@ var prefer_string_literal_union_default = ESLintUtils28.RuleCreator(
3794
4104
  return;
3795
4105
  }
3796
4106
  const name = keyName(key);
3797
- if (name === null || !isChoiceLikeName(name)) {
4107
+ if (name === null || !isChoiceLikeName(name) || ignoredFields.has(name.toLowerCase())) {
3798
4108
  return;
3799
4109
  }
3800
4110
  bareChoiceProps.push({ name, container, node });
@@ -3913,7 +4223,7 @@ var ACRONYM_OVERRIDES = [
3913
4223
  [/gRPC/g, "Grpc"]
3914
4224
  ];
3915
4225
  var CAMEL_BOUNDARY_RE = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g;
3916
- var TEST_FILE_RE2 = /\.(test|spec)\.[cm]?[jt]sx?$/i;
4226
+ var TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
3917
4227
  var SCRIPT_EXT_RE = /\.[cm]?[jt]sx?$/i;
3918
4228
  var basename = (filename) => filename.split(/[/\\]/).pop() ?? filename;
3919
4229
  var stemOf = (base) => base.replace(SCRIPT_EXT_RE, "");
@@ -4013,7 +4323,7 @@ var single_public_export_default = ESLintUtils29.RuleCreator(
4013
4323
  create(context) {
4014
4324
  const base = basename(context.filename);
4015
4325
  if (base.endsWith(".d.ts")) return {};
4016
- if (TEST_FILE_RE2.test(base)) return {};
4326
+ if (TEST_FILE_RE.test(base)) return {};
4017
4327
  const stem = stemOf(base);
4018
4328
  if (!JUNK_DRAWER_STEMS.has(stem.toLowerCase())) return {};
4019
4329
  return {
@@ -4034,6 +4344,1459 @@ var single_public_export_default = ESLintUtils29.RuleCreator(
4034
4344
  }
4035
4345
  });
4036
4346
 
4347
+ // src/rules/no-offset-pagination.ts
4348
+ import { ESLintUtils as ESLintUtils30 } from "@typescript-eslint/utils";
4349
+
4350
+ // src/rules/_sql.ts
4351
+ import { AST_NODE_TYPES as AST_NODE_TYPES19 } from "@typescript-eslint/utils";
4352
+ function stripSqlNoise(text) {
4353
+ const out = [...text];
4354
+ const n = text.length;
4355
+ let i = 0;
4356
+ while (i < n) {
4357
+ const ch = text[i];
4358
+ if (ch === "'" || ch === '"') {
4359
+ out[i] = " ";
4360
+ i += 1;
4361
+ while (i < n) {
4362
+ const c = text[i];
4363
+ if (c === ch) {
4364
+ if (i + 1 < n && text[i + 1] === ch) {
4365
+ out[i] = " ";
4366
+ out[i + 1] = " ";
4367
+ i += 2;
4368
+ continue;
4369
+ }
4370
+ out[i] = " ";
4371
+ i += 1;
4372
+ break;
4373
+ }
4374
+ if (c !== "\n") {
4375
+ out[i] = " ";
4376
+ }
4377
+ i += 1;
4378
+ }
4379
+ continue;
4380
+ }
4381
+ if (ch === "-" && text[i + 1] === "-") {
4382
+ while (i < n && text[i] !== "\n") {
4383
+ out[i] = " ";
4384
+ i += 1;
4385
+ }
4386
+ continue;
4387
+ }
4388
+ if (ch === "/" && text[i + 1] === "*") {
4389
+ out[i] = " ";
4390
+ out[i + 1] = " ";
4391
+ i += 2;
4392
+ while (i < n && !(text[i] === "*" && text[i + 1] === "/")) {
4393
+ if (text[i] !== "\n") {
4394
+ out[i] = " ";
4395
+ }
4396
+ i += 1;
4397
+ }
4398
+ if (i < n) {
4399
+ out[i] = " ";
4400
+ out[i + 1] = " ";
4401
+ i += 2;
4402
+ }
4403
+ continue;
4404
+ }
4405
+ i += 1;
4406
+ }
4407
+ return out.join("");
4408
+ }
4409
+ var SUBSTITUTION_MARKER = "?";
4410
+ function sqlTextOf(node) {
4411
+ switch (node.type) {
4412
+ case AST_NODE_TYPES19.Literal:
4413
+ return typeof node.value === "string" ? node.value : null;
4414
+ case AST_NODE_TYPES19.TemplateLiteral:
4415
+ return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join(SUBSTITUTION_MARKER);
4416
+ case AST_NODE_TYPES19.TaggedTemplateExpression:
4417
+ return sqlTextOf(node.quasi);
4418
+ case AST_NODE_TYPES19.BinaryExpression: {
4419
+ if (node.operator !== "+") {
4420
+ return null;
4421
+ }
4422
+ const left = sqlTextOf(node.left);
4423
+ const right = sqlTextOf(node.right);
4424
+ return left !== null && right !== null ? left + right : null;
4425
+ }
4426
+ case AST_NODE_TYPES19.ArrayExpression: {
4427
+ const parts = [];
4428
+ for (const element of node.elements) {
4429
+ if (element === null) {
4430
+ return null;
4431
+ }
4432
+ const part = sqlTextOf(element);
4433
+ if (part === null) {
4434
+ return null;
4435
+ }
4436
+ parts.push(part);
4437
+ }
4438
+ return parts.length > 0 ? parts.join(" ") : null;
4439
+ }
4440
+ default:
4441
+ return null;
4442
+ }
4443
+ }
4444
+ function isJoinedFragmentArray(node) {
4445
+ const parent = node.parent;
4446
+ return parent?.type === AST_NODE_TYPES19.MemberExpression && parent.object === node && !parent.computed && parent.property.type === AST_NODE_TYPES19.Identifier && parent.property.name === "join" && parent.parent?.type === AST_NODE_TYPES19.CallExpression;
4447
+ }
4448
+ function markConsumed(node, consumed) {
4449
+ consumed.add(node);
4450
+ for (const key of Object.keys(node)) {
4451
+ if (key === "parent") {
4452
+ continue;
4453
+ }
4454
+ const value = node[key];
4455
+ for (const child of Array.isArray(value) ? value : [value]) {
4456
+ if (child !== null && typeof child === "object" && "type" in child) {
4457
+ markConsumed(child, consumed);
4458
+ }
4459
+ }
4460
+ }
4461
+ }
4462
+ function createSqlListener(handler) {
4463
+ const consumed = /* @__PURE__ */ new WeakSet();
4464
+ const visit = (node) => {
4465
+ if (consumed.has(node)) {
4466
+ return;
4467
+ }
4468
+ const text = sqlTextOf(node);
4469
+ if (text === null) {
4470
+ return;
4471
+ }
4472
+ markConsumed(node, consumed);
4473
+ handler(stripSqlNoise(text), node);
4474
+ };
4475
+ return {
4476
+ BinaryExpression: (node) => {
4477
+ visit(node);
4478
+ },
4479
+ ArrayExpression: (node) => {
4480
+ if (isJoinedFragmentArray(node)) {
4481
+ visit(node);
4482
+ }
4483
+ },
4484
+ TemplateLiteral: (node) => {
4485
+ visit(node);
4486
+ },
4487
+ Literal: (node) => {
4488
+ visit(node);
4489
+ }
4490
+ };
4491
+ }
4492
+
4493
+ // src/rules/no-offset-pagination.ts
4494
+ var OFFSET_PAGINATION = /\bOFFSET\s+(?:\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
4495
+ var OFFSET_GATE = /offset/i;
4496
+ var no_offset_pagination_default = ESLintUtils30.RuleCreator(
4497
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4498
+ )({
4499
+ name: "no-offset-pagination",
4500
+ meta: {
4501
+ type: "problem",
4502
+ docs: {
4503
+ description: "Disallow OFFSET pagination in embedded SQL; it is O(N) per page and drops or repeats rows under concurrent writes. Use a keyset cursor."
4504
+ },
4505
+ schema: [],
4506
+ messages: {
4507
+ noOffsetPagination: "OFFSET pagination scans and discards every skipped row (O(N) per page) and shifts under concurrent inserts, so rows get repeated or missed. Use a keyset cursor: `WHERE id > ? ORDER BY id LIMIT ?`."
4508
+ }
4509
+ },
4510
+ defaultOptions: [],
4511
+ create(context) {
4512
+ if (isTestFile(context.filename) || !OFFSET_GATE.test(context.sourceCode.text)) {
4513
+ return {};
4514
+ }
4515
+ return createSqlListener((sql, node) => {
4516
+ if (!OFFSET_PAGINATION.test(sql)) {
4517
+ return;
4518
+ }
4519
+ context.report({ node, messageId: "noOffsetPagination" });
4520
+ });
4521
+ }
4522
+ });
4523
+
4524
+ // src/rules/no-positional-tuple-return.ts
4525
+ import { AST_NODE_TYPES as AST_NODE_TYPES20, ESLintUtils as ESLintUtils31 } from "@typescript-eslint/utils";
4526
+ var MIN_ELEMENTS = 2;
4527
+ var AWAITABLE_TYPES = /* @__PURE__ */ new Set(["Promise", "PromiseLike", "Awaited"]);
4528
+ function tupleReturnType(node) {
4529
+ if (node.type === AST_NODE_TYPES20.TSTupleType) {
4530
+ return node;
4531
+ }
4532
+ if (node.type === AST_NODE_TYPES20.TSTypeReference && node.typeName.type === AST_NODE_TYPES20.Identifier && AWAITABLE_TYPES.has(node.typeName.name)) {
4533
+ const argument = node.typeArguments?.params[0];
4534
+ return argument === void 0 ? null : tupleReturnType(argument);
4535
+ }
4536
+ return null;
4537
+ }
4538
+ function normalizedText(sourceCode, node) {
4539
+ return sourceCode.getText(node).replaceAll(/\s+/g, " ").trim();
4540
+ }
4541
+ function isPermittedTuple(tuple, sourceCode) {
4542
+ const elements = tuple.elementTypes;
4543
+ if (elements.length < MIN_ELEMENTS) {
4544
+ return true;
4545
+ }
4546
+ if (elements.some((element) => element.type === AST_NODE_TYPES20.TSRestType)) {
4547
+ return true;
4548
+ }
4549
+ if (elements.some((element) => element.type === AST_NODE_TYPES20.TSNamedTupleMember)) {
4550
+ return true;
4551
+ }
4552
+ if (elements[0]?.type === AST_NODE_TYPES20.TSLiteralType) {
4553
+ return true;
4554
+ }
4555
+ const texts = new Set(elements.map((element) => normalizedText(sourceCode, element)));
4556
+ return texts.size === 1;
4557
+ }
4558
+ function functionName(node) {
4559
+ if (node.type === AST_NODE_TYPES20.FunctionDeclaration) {
4560
+ return node.id?.name ?? null;
4561
+ }
4562
+ const parent = node.parent;
4563
+ if (parent?.type === AST_NODE_TYPES20.VariableDeclarator && parent.id.type === AST_NODE_TYPES20.Identifier) {
4564
+ return parent.id.name;
4565
+ }
4566
+ if ((parent?.type === AST_NODE_TYPES20.MethodDefinition || parent?.type === AST_NODE_TYPES20.PropertyDefinition || parent?.type === AST_NODE_TYPES20.Property) && parent.key.type === AST_NODE_TYPES20.Identifier) {
4567
+ return parent.key.name;
4568
+ }
4569
+ return null;
4570
+ }
4571
+ function isExported(node) {
4572
+ for (let current = node; current != null; current = current.parent) {
4573
+ const parent = current.parent;
4574
+ if (parent?.type === AST_NODE_TYPES20.ExportNamedDeclaration || parent?.type === AST_NODE_TYPES20.ExportDefaultDeclaration) {
4575
+ return true;
4576
+ }
4577
+ }
4578
+ return false;
4579
+ }
4580
+ var no_positional_tuple_return_default = ESLintUtils31.RuleCreator(
4581
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4582
+ )({
4583
+ name: "no-positional-tuple-return",
4584
+ meta: {
4585
+ type: "suggestion",
4586
+ docs: {
4587
+ description: "Disallow returning a positional tuple of distinct fields from an exported function; return a named object so call sites cannot mismatch slots."
4588
+ },
4589
+ schema: [],
4590
+ messages: {
4591
+ noPositionalTupleReturn: "Exported `{{name}}` returns a positional {{count}}-tuple, so every call site re-invents the field names and can disagree. Return a named object (or label the tuple members) instead."
4592
+ }
4593
+ },
4594
+ defaultOptions: [],
4595
+ create(context) {
4596
+ const check = (node) => {
4597
+ const annotation = node.returnType?.typeAnnotation;
4598
+ if (annotation === void 0) {
4599
+ return;
4600
+ }
4601
+ const tuple = tupleReturnType(annotation);
4602
+ if (tuple === null || isPermittedTuple(tuple, context.sourceCode)) {
4603
+ return;
4604
+ }
4605
+ const name = functionName(node);
4606
+ if (name === null || name.startsWith("_") || /^use[A-Z]/.test(name)) {
4607
+ return;
4608
+ }
4609
+ if (!isExported(node)) {
4610
+ return;
4611
+ }
4612
+ context.report({
4613
+ node: tuple,
4614
+ messageId: "noPositionalTupleReturn",
4615
+ data: { name, count: String(tuple.elementTypes.length) }
4616
+ });
4617
+ };
4618
+ return {
4619
+ FunctionDeclaration: check,
4620
+ FunctionExpression: check,
4621
+ ArrowFunctionExpression: check
4622
+ };
4623
+ }
4624
+ });
4625
+
4626
+ // src/rules/no-repeated-string-literal.ts
4627
+ import { AST_NODE_TYPES as AST_NODE_TYPES21, ESLintUtils as ESLintUtils32 } from "@typescript-eslint/utils";
4628
+ var MIN_LENGTH = 40;
4629
+ var MIN_OCCURRENCES = 3;
4630
+ var MIN_DISTINCT_SCOPES = 2;
4631
+ var PREVIEW_LENGTH = 40;
4632
+ var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
4633
+ var IDENTIFIER_RE = /^[a-z_][a-z0-9_.]*$/;
4634
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
4635
+ AST_NODE_TYPES21.FunctionDeclaration,
4636
+ AST_NODE_TYPES21.FunctionExpression,
4637
+ AST_NODE_TYPES21.ArrowFunctionExpression
4638
+ ]);
4639
+ function isStructured(value) {
4640
+ return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value);
4641
+ }
4642
+ function preview(value) {
4643
+ const oneLine = value.replaceAll("\n", " ").trim();
4644
+ return oneLine.length <= PREVIEW_LENGTH ? oneLine : `${oneLine.slice(0, PREVIEW_LENGTH)}...`;
4645
+ }
4646
+ function enclosingFunction(node) {
4647
+ for (let current = node.parent; current != null; current = current.parent) {
4648
+ if (FUNCTION_TYPES.has(current.type)) {
4649
+ return current;
4650
+ }
4651
+ }
4652
+ return null;
4653
+ }
4654
+ function isScaffolding(node) {
4655
+ const parent = node.parent;
4656
+ if (parent === void 0) {
4657
+ return true;
4658
+ }
4659
+ return parent.type === AST_NODE_TYPES21.ImportDeclaration || parent.type === AST_NODE_TYPES21.ExportNamedDeclaration || parent.type === AST_NODE_TYPES21.ExportAllDeclaration || parent.type === AST_NODE_TYPES21.TSImportType || parent.type === AST_NODE_TYPES21.JSXAttribute || parent.type === AST_NODE_TYPES21.TSLiteralType;
4660
+ }
4661
+ var no_repeated_string_literal_default = ESLintUtils32.RuleCreator(
4662
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4663
+ )({
4664
+ name: "no-repeated-string-literal",
4665
+ meta: {
4666
+ type: "suggestion",
4667
+ docs: {
4668
+ description: "Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant."
4669
+ },
4670
+ schema: [],
4671
+ messages: {
4672
+ noRepeatedStringLiteral: 'Structured string literal "{{preview}}" is repeated across functions (first use on line {{line}}) \u2014 extract a module-level constant so the copies cannot drift.'
4673
+ }
4674
+ },
4675
+ defaultOptions: [],
4676
+ create(context) {
4677
+ if (isTestFile(context.filename)) {
4678
+ return {};
4679
+ }
4680
+ const occurrences = /* @__PURE__ */ new Map();
4681
+ const scopes = /* @__PURE__ */ new WeakMap();
4682
+ const record = (value, node) => {
4683
+ if (value.length < MIN_LENGTH || !isStructured(value) || isScaffolding(node)) {
4684
+ return;
4685
+ }
4686
+ const existing = occurrences.get(value);
4687
+ if (existing === void 0) {
4688
+ occurrences.set(value, [node]);
4689
+ } else {
4690
+ existing.push(node);
4691
+ }
4692
+ scopes.set(node, enclosingFunction(node));
4693
+ };
4694
+ return {
4695
+ Literal(node) {
4696
+ if (typeof node.value === "string") {
4697
+ record(node.value, node);
4698
+ }
4699
+ },
4700
+ TemplateLiteral(node) {
4701
+ const [only] = node.quasis;
4702
+ if (node.expressions.length === 0 && only !== void 0) {
4703
+ record(only.value.cooked ?? only.value.raw, node);
4704
+ }
4705
+ },
4706
+ "Program:exit": () => {
4707
+ for (const [value, nodes] of occurrences) {
4708
+ if (nodes.length < MIN_OCCURRENCES) {
4709
+ continue;
4710
+ }
4711
+ const distinctScopes = new Set(
4712
+ nodes.map((node) => scopes.get(node)).filter((scope) => scope != null)
4713
+ );
4714
+ if (distinctScopes.size < MIN_DISTINCT_SCOPES) {
4715
+ continue;
4716
+ }
4717
+ const [first, ...repeats] = nodes;
4718
+ if (first === void 0) {
4719
+ continue;
4720
+ }
4721
+ for (const node of repeats) {
4722
+ context.report({
4723
+ node,
4724
+ messageId: "noRepeatedStringLiteral",
4725
+ data: { preview: preview(value), line: String(first.loc.start.line) }
4726
+ });
4727
+ }
4728
+ }
4729
+ }
4730
+ };
4731
+ }
4732
+ });
4733
+
4734
+ // src/rules/no-select-star.ts
4735
+ import { ESLintUtils as ESLintUtils33 } from "@typescript-eslint/utils";
4736
+ var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
4737
+ var SELECT_KEYWORD = /\bSELECT\b/gi;
4738
+ var FROM_KEYWORD = /^FROM\b/i;
4739
+ var EXISTS_BEFORE = /\bEXISTS\s*\(\s*$/i;
4740
+ var QUALIFIED_PREFIX = /\w\.$/;
4741
+ var SELECT_GATE = /select/i;
4742
+ function isProjectionStar(sql, pos) {
4743
+ if (QUALIFIED_PREFIX.test(sql.slice(0, pos))) {
4744
+ return true;
4745
+ }
4746
+ let before = pos - 1;
4747
+ while (before >= 0 && /\s/.test(sql[before] ?? "")) {
4748
+ before -= 1;
4749
+ }
4750
+ let after = pos + 1;
4751
+ while (after < sql.length && /\s/.test(sql[after] ?? "")) {
4752
+ after += 1;
4753
+ }
4754
+ const beforeChar = before >= 0 ? sql[before] ?? "" : "";
4755
+ const afterChar = after < sql.length ? sql[after] ?? "" : "";
4756
+ const terminates = afterChar === "" || afterChar === "," || afterChar === ")" || FROM_KEYWORD.test(sql.slice(after));
4757
+ if (!terminates) {
4758
+ return false;
4759
+ }
4760
+ return !(beforeChar === "(" && afterChar === ")");
4761
+ }
4762
+ function hasRealSelectStar(sql) {
4763
+ const selects = [...sql.matchAll(SELECT_KEYWORD)].map((m) => m.index);
4764
+ for (let pos = 0; pos < sql.length; pos++) {
4765
+ if (sql[pos] !== "*" || !isProjectionStar(sql, pos)) {
4766
+ continue;
4767
+ }
4768
+ const owning = selects.filter((start) => start < pos).at(-1);
4769
+ if (owning !== void 0 && !EXISTS_BEFORE.test(sql.slice(0, owning))) {
4770
+ return true;
4771
+ }
4772
+ }
4773
+ return false;
4774
+ }
4775
+ var no_select_star_default = ESLintUtils33.RuleCreator(
4776
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4777
+ )({
4778
+ name: "no-select-star",
4779
+ meta: {
4780
+ type: "problem",
4781
+ docs: {
4782
+ description: "Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently."
4783
+ },
4784
+ schema: [],
4785
+ messages: {
4786
+ noSelectStar: "`SELECT *` over-fetches and leaves the row shape implicit \u2014 a new or reordered column silently changes what this query returns. List the columns explicitly."
4787
+ }
4788
+ },
4789
+ defaultOptions: [],
4790
+ create(context) {
4791
+ if (isTestFile(context.filename) || !SELECT_GATE.test(context.sourceCode.text)) {
4792
+ return {};
4793
+ }
4794
+ return createSqlListener((sql, node) => {
4795
+ if (!QUERY_SHAPE.test(sql) || !hasRealSelectStar(sql)) {
4796
+ return;
4797
+ }
4798
+ context.report({ node, messageId: "noSelectStar" });
4799
+ });
4800
+ }
4801
+ });
4802
+
4803
+ // src/rules/no-sleep-in-test-body.ts
4804
+ import { AST_NODE_TYPES as AST_NODE_TYPES22, ESLintUtils as ESLintUtils34 } from "@typescript-eslint/utils";
4805
+ var SLEEP_HELPERS = /* @__PURE__ */ new Set(["sleep", "delay", "wait", "pause"]);
4806
+ var TEST_CALLERS = /* @__PURE__ */ new Set([
4807
+ "it",
4808
+ "test",
4809
+ "beforeEach",
4810
+ "afterEach"
4811
+ ]);
4812
+ var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
4813
+ AST_NODE_TYPES22.FunctionDeclaration,
4814
+ AST_NODE_TYPES22.FunctionExpression,
4815
+ AST_NODE_TYPES22.ArrowFunctionExpression
4816
+ ]);
4817
+ function isNonzeroNumericLiteral(node) {
4818
+ return node?.type === AST_NODE_TYPES22.Literal && typeof node.value === "number" && node.value !== 0;
4819
+ }
4820
+ function isTimedSetTimeout(node) {
4821
+ return node.type === AST_NODE_TYPES22.CallExpression && node.callee.type === AST_NODE_TYPES22.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
4822
+ }
4823
+ function isPromiseSleep(node) {
4824
+ if (node.callee.type !== AST_NODE_TYPES22.Identifier || node.callee.name !== "Promise") {
4825
+ return false;
4826
+ }
4827
+ const executor = node.arguments[0];
4828
+ if (executor?.type !== AST_NODE_TYPES22.ArrowFunctionExpression && executor?.type !== AST_NODE_TYPES22.FunctionExpression) {
4829
+ return false;
4830
+ }
4831
+ const body = executor.body;
4832
+ if (body.type !== AST_NODE_TYPES22.BlockStatement) {
4833
+ return isTimedSetTimeout(body);
4834
+ }
4835
+ return body.body.some(
4836
+ (stmt) => stmt.type === AST_NODE_TYPES22.ExpressionStatement && isTimedSetTimeout(stmt.expression)
4837
+ );
4838
+ }
4839
+ function isHelperSleep(node) {
4840
+ return node.callee.type === AST_NODE_TYPES22.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
4841
+ }
4842
+ function nearestEnclosingFunction(node) {
4843
+ for (let current = node.parent; current != null; current = current.parent) {
4844
+ if (!FUNCTION_TYPES2.has(current.type)) {
4845
+ continue;
4846
+ }
4847
+ const grandparent = current.parent;
4848
+ const isPromiseExecutor = grandparent?.type === AST_NODE_TYPES22.NewExpression && isPromiseSleep(grandparent);
4849
+ if (!isPromiseExecutor) {
4850
+ return current;
4851
+ }
4852
+ }
4853
+ return null;
4854
+ }
4855
+ function testCallerName(callee) {
4856
+ if (callee.type === AST_NODE_TYPES22.Identifier) {
4857
+ return callee.name;
4858
+ }
4859
+ if (callee.type === AST_NODE_TYPES22.MemberExpression) {
4860
+ return testCallerName(callee.object);
4861
+ }
4862
+ if (callee.type === AST_NODE_TYPES22.CallExpression) {
4863
+ return testCallerName(callee.callee);
4864
+ }
4865
+ if (callee.type === AST_NODE_TYPES22.TaggedTemplateExpression) {
4866
+ return testCallerName(callee.tag);
4867
+ }
4868
+ return null;
4869
+ }
4870
+ function isTestBody(fn) {
4871
+ const call = fn.parent;
4872
+ if (call?.type !== AST_NODE_TYPES22.CallExpression || !call.arguments.some((argument) => argument === fn)) {
4873
+ return false;
4874
+ }
4875
+ const name = testCallerName(call.callee);
4876
+ return name !== null && TEST_CALLERS.has(name);
4877
+ }
4878
+ var no_sleep_in_test_body_default = ESLintUtils34.RuleCreator(
4879
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4880
+ )({
4881
+ name: "no-sleep-in-test-body",
4882
+ meta: {
4883
+ type: "problem",
4884
+ docs: {
4885
+ description: "Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers."
4886
+ },
4887
+ schema: [],
4888
+ messages: {
4889
+ noSleepInTestBody: "A fixed sleep in a test body asserts on wall-clock time and flakes under CI load. Await the promise the code returns, or drive time with `vi.useFakeTimers()` + `await vi.advanceTimersByTimeAsync(ms)`."
4890
+ }
4891
+ },
4892
+ defaultOptions: [],
4893
+ create(context) {
4894
+ if (!isTestFile(context.filename)) {
4895
+ return {};
4896
+ }
4897
+ const report = (node) => {
4898
+ const enclosing = nearestEnclosingFunction(node);
4899
+ if (enclosing === null || !isTestBody(enclosing)) {
4900
+ return;
4901
+ }
4902
+ context.report({ node, messageId: "noSleepInTestBody" });
4903
+ };
4904
+ return {
4905
+ NewExpression(node) {
4906
+ if (isPromiseSleep(node)) {
4907
+ report(node);
4908
+ }
4909
+ },
4910
+ CallExpression(node) {
4911
+ if (isHelperSleep(node)) {
4912
+ report(node);
4913
+ }
4914
+ }
4915
+ };
4916
+ }
4917
+ });
4918
+
4919
+ // src/rules/prefer-constant-time-secret-compare.ts
4920
+ import { AST_NODE_TYPES as AST_NODE_TYPES23, ESLintUtils as ESLintUtils35 } from "@typescript-eslint/utils";
4921
+ var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
4922
+ var SENTINEL_IDENTIFIERS = /* @__PURE__ */ new Set(["undefined", "NaN"]);
4923
+ var SENTINEL_WORDS = /(^|_)(SENTINEL|EMPTY|NONE|NULL|UNSET|MISSING|PLACEHOLDER|DUMMY|FAKE|EXAMPLE)(_|$)/;
4924
+ function isConstantReference(identifier) {
4925
+ if (isAuthSecretName(identifier) && !SENTINEL_WORDS.test(identifier)) return false;
4926
+ return identifier === identifier.toUpperCase() && /[A-Za-z]/.test(identifier);
4927
+ }
4928
+ function isExcludedOperand(node) {
4929
+ switch (node.type) {
4930
+ case AST_NODE_TYPES23.Literal:
4931
+ return true;
4932
+ case AST_NODE_TYPES23.TemplateLiteral:
4933
+ return node.expressions.length === 0;
4934
+ case AST_NODE_TYPES23.Identifier:
4935
+ return SENTINEL_IDENTIFIERS.has(node.name) || isConstantReference(node.name);
4936
+ case AST_NODE_TYPES23.MemberExpression:
4937
+ return !node.computed && node.property.type === AST_NODE_TYPES23.Identifier && isConstantReference(node.property.name);
4938
+ default:
4939
+ return false;
4940
+ }
4941
+ }
4942
+ function operandName(node) {
4943
+ if (node.type === AST_NODE_TYPES23.Identifier) {
4944
+ return node.name;
4945
+ }
4946
+ if (node.type === AST_NODE_TYPES23.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES23.Identifier) {
4947
+ return node.property.name;
4948
+ }
4949
+ return null;
4950
+ }
4951
+ function isSecretOperand(node) {
4952
+ if (node.type === AST_NODE_TYPES23.TemplateLiteral) {
4953
+ return node.expressions.some((expression) => isSecretOperand(expression));
4954
+ }
4955
+ const name = operandName(node);
4956
+ return name !== null && isAuthSecretName(name);
4957
+ }
4958
+ function secretNameOf(node) {
4959
+ if (node.type === AST_NODE_TYPES23.TemplateLiteral) {
4960
+ for (const expression of node.expressions) {
4961
+ const nested = secretNameOf(expression);
4962
+ if (nested !== null) {
4963
+ return nested;
4964
+ }
4965
+ }
4966
+ return null;
4967
+ }
4968
+ return operandName(node);
4969
+ }
4970
+ var prefer_constant_time_secret_compare_default = ESLintUtils35.RuleCreator(
4971
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4972
+ )({
4973
+ name: "prefer-constant-time-secret-compare",
4974
+ meta: {
4975
+ type: "problem",
4976
+ docs: {
4977
+ description: "Disallow `===`/`!==` on a secret-like value; short-circuiting comparison leaks the secret through timing. Use a constant-time compare."
4978
+ },
4979
+ schema: [],
4980
+ messages: {
4981
+ preferConstantTimeSecretCompare: "`{{operator}}` on secret `{{name}}` short-circuits on the first differing byte and leaks it through timing. Compare constant-time instead (`crypto.subtle.timingSafeEqual` over equal-length SHA-256 digests)."
4982
+ }
4983
+ },
4984
+ defaultOptions: [],
4985
+ create(context) {
4986
+ if (isTestFile(context.filename)) {
4987
+ return {};
4988
+ }
4989
+ return {
4990
+ BinaryExpression(node) {
4991
+ if (!EQUALITY_OPERATORS.has(node.operator)) {
4992
+ return;
4993
+ }
4994
+ const { left, right } = node;
4995
+ if (isExcludedOperand(left) || isExcludedOperand(right)) {
4996
+ return;
4997
+ }
4998
+ const secret = [left, right].find((operand) => isSecretOperand(operand));
4999
+ if (secret === void 0) {
5000
+ return;
5001
+ }
5002
+ context.report({
5003
+ node,
5004
+ messageId: "preferConstantTimeSecretCompare",
5005
+ data: { operator: node.operator, name: secretNameOf(secret) ?? "" }
5006
+ });
5007
+ }
5008
+ };
5009
+ }
5010
+ });
5011
+
5012
+ // src/rules/store-insert-requires-on-conflict.ts
5013
+ import { ESLintUtils as ESLintUtils36 } from "@typescript-eslint/utils";
5014
+ var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
5015
+ var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/i;
5016
+ var INSERT_GATE = /insert/i;
5017
+ var store_insert_requires_on_conflict_default = ESLintUtils36.RuleCreator(
5018
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5019
+ )({
5020
+ name: "store-insert-requires-on-conflict",
5021
+ meta: {
5022
+ type: "problem",
5023
+ docs: {
5024
+ description: "Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts."
5025
+ },
5026
+ schema: [],
5027
+ messages: {
5028
+ storeInsertRequiresOnConflict: "This INSERT is not replay-safe: a cron re-run or queue redelivery duplicates the row (or fails the handler on a unique-constraint violation). Add `ON CONFLICT (...) DO UPDATE` / `DO NOTHING` (or `INSERT OR IGNORE`)."
5029
+ }
5030
+ },
5031
+ defaultOptions: [],
5032
+ create(context) {
5033
+ if (isTestFile(context.filename) || !INSERT_GATE.test(context.sourceCode.text)) {
5034
+ return {};
5035
+ }
5036
+ return createSqlListener((sql, node) => {
5037
+ if (!INSERT_WRITE.test(sql) || CONFLICT_HANDLED.test(sql)) {
5038
+ return;
5039
+ }
5040
+ context.report({ node, messageId: "storeInsertRequiresOnConflict" });
5041
+ });
5042
+ }
5043
+ });
5044
+
5045
+ // src/rules/no-dynamic-sql.ts
5046
+ import { AST_NODE_TYPES as AST_NODE_TYPES24, ESLintUtils as ESLintUtils37 } from "@typescript-eslint/utils";
5047
+ var DEFAULT_METHODS = ["prepare", "exec", "query"];
5048
+ var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
5049
+ function isStaticFragment(expression) {
5050
+ if (expression.type === AST_NODE_TYPES24.Identifier) {
5051
+ return CONSTANT_CASE_RE.test(expression.name);
5052
+ }
5053
+ if (expression.type === AST_NODE_TYPES24.MemberExpression && !expression.computed && expression.property.type === AST_NODE_TYPES24.Identifier) {
5054
+ return CONSTANT_CASE_RE.test(expression.property.name);
5055
+ }
5056
+ if (expression.type === AST_NODE_TYPES24.Literal) {
5057
+ return typeof expression.value === "string";
5058
+ }
5059
+ return false;
5060
+ }
5061
+ function runtimeInterpolations(template) {
5062
+ return template.expressions.filter(
5063
+ (expression) => !isStaticFragment(expression)
5064
+ );
5065
+ }
5066
+ function concatOperands(node) {
5067
+ if (node.type === AST_NODE_TYPES24.BinaryExpression && node.operator === "+") {
5068
+ return [...concatOperands(node.left), ...concatOperands(node.right)];
5069
+ }
5070
+ return [node];
5071
+ }
5072
+ function runtimeConcatOperands(node) {
5073
+ if (node.type !== AST_NODE_TYPES24.BinaryExpression || node.operator !== "+") {
5074
+ return [];
5075
+ }
5076
+ const operands = concatOperands(node);
5077
+ const hasStringLiteral = operands.some(
5078
+ (operand) => operand.type === AST_NODE_TYPES24.Literal && typeof operand.value === "string"
5079
+ );
5080
+ if (!hasStringLiteral) {
5081
+ return [];
5082
+ }
5083
+ return operands.filter(
5084
+ (operand) => operand.type !== AST_NODE_TYPES24.Literal && !isStaticFragment(operand)
5085
+ );
5086
+ }
5087
+ function statementMethodName(node, methods) {
5088
+ const callee = node.callee;
5089
+ if (callee.type !== AST_NODE_TYPES24.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES24.Identifier) {
5090
+ return null;
5091
+ }
5092
+ const name = callee.property.name;
5093
+ return methods.has(name) ? name : null;
5094
+ }
5095
+ var no_dynamic_sql_default = ESLintUtils37.RuleCreator(
5096
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5097
+ )({
5098
+ name: "no-dynamic-sql",
5099
+ meta: {
5100
+ type: "problem",
5101
+ docs: {
5102
+ description: "Disallow interpolating or concatenating a runtime value into a SQL statement passed to `prepare`/`exec`/`query`; use a placeholder and bind the value."
5103
+ },
5104
+ schema: [
5105
+ {
5106
+ type: "object",
5107
+ properties: {
5108
+ methods: {
5109
+ type: "array",
5110
+ items: { type: "string" },
5111
+ description: "Statement-taking method names to inspect. Replaces the defaults."
5112
+ }
5113
+ },
5114
+ additionalProperties: false
5115
+ }
5116
+ ],
5117
+ messages: {
5118
+ dynamicSql: "Runtime value built into a SQL statement passed to `{{method}}()`. Use a `?` placeholder and pass the value through `.bind(...)` so the driver parameterises it."
5119
+ }
5120
+ },
5121
+ defaultOptions: [{}],
5122
+ create(context, [options]) {
5123
+ const methods = new Set(options?.methods ?? DEFAULT_METHODS);
5124
+ return {
5125
+ CallExpression(node) {
5126
+ const method = statementMethodName(node, methods);
5127
+ if (method === null) {
5128
+ return;
5129
+ }
5130
+ const statement = node.arguments[0];
5131
+ if (statement === void 0) {
5132
+ return;
5133
+ }
5134
+ const offenders = statement.type === AST_NODE_TYPES24.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
5135
+ for (const offender of offenders) {
5136
+ context.report({
5137
+ node: offender,
5138
+ messageId: "dynamicSql",
5139
+ data: { method }
5140
+ });
5141
+ }
5142
+ }
5143
+ };
5144
+ }
5145
+ });
5146
+
5147
+ // src/rules/no-raw-fetch-outside-clients.ts
5148
+ import { ESLintUtils as ESLintUtils38 } from "@typescript-eslint/utils";
5149
+ var DEFAULT_ALLOW = [
5150
+ "[\\\\/]clients?[\\\\/]",
5151
+ "-client\\.[cm]?[jt]sx?$",
5152
+ "[\\\\/]http-client\\.[cm]?[jt]sx?$",
5153
+ "\\.test\\.",
5154
+ "\\.spec\\.",
5155
+ "[\\\\/]__tests__[\\\\/]",
5156
+ "[\\\\/]__mocks__[\\\\/]"
5157
+ ];
5158
+ var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
5159
+ "globalThis",
5160
+ "window",
5161
+ "self"
5162
+ ]);
5163
+ function isGlobalFetchCall(node) {
5164
+ const callee = node.callee;
5165
+ if (callee.type === "Identifier") {
5166
+ return callee.name === "fetch";
5167
+ }
5168
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "fetch" && callee.object.type === "Identifier") {
5169
+ return GLOBAL_RECEIVERS.has(callee.object.name);
5170
+ }
5171
+ return false;
5172
+ }
5173
+ function compile(patterns) {
5174
+ const compiled = [];
5175
+ for (const pattern of patterns) {
5176
+ try {
5177
+ compiled.push(new RegExp(pattern));
5178
+ } catch {
5179
+ }
5180
+ }
5181
+ return compiled;
5182
+ }
5183
+ var no_raw_fetch_outside_clients_default = ESLintUtils38.RuleCreator(
5184
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5185
+ )({
5186
+ name: "no-raw-fetch-outside-clients",
5187
+ meta: {
5188
+ type: "problem",
5189
+ docs: {
5190
+ description: "Disallow calling the global `fetch` outside the client layer; route outbound HTTP through a client module that owns retry, timeout and status handling."
5191
+ },
5192
+ schema: [
5193
+ {
5194
+ type: "object",
5195
+ properties: {
5196
+ allow: {
5197
+ type: "array",
5198
+ items: { type: "string" },
5199
+ description: "Regular-expression sources matched against the filename. Replaces the defaults."
5200
+ }
5201
+ },
5202
+ additionalProperties: false
5203
+ }
5204
+ ],
5205
+ messages: {
5206
+ rawFetch: "Raw `fetch()` outside a client module. Route the call through a client (e.g. `clients/*-client.ts`) so it inherits retry, timeout and status handling and stays stubbable in tests."
5207
+ }
5208
+ },
5209
+ defaultOptions: [{}],
5210
+ create(context, [options]) {
5211
+ const patterns = options?.allow ?? DEFAULT_ALLOW;
5212
+ const allowed = compile(patterns);
5213
+ const filename = context.filename;
5214
+ if (allowed.some((re) => re.test(filename))) {
5215
+ return {};
5216
+ }
5217
+ return {
5218
+ CallExpression(node) {
5219
+ if (isGlobalFetchCall(node)) {
5220
+ context.report({ node, messageId: "rawFetch" });
5221
+ }
5222
+ }
5223
+ };
5224
+ }
5225
+ });
5226
+
5227
+ // src/rules/no-storage-in-stateless-modules.ts
5228
+ import { AST_NODE_TYPES as AST_NODE_TYPES25, ESLintUtils as ESLintUtils39 } from "@typescript-eslint/utils";
5229
+ var DEFAULT_METHODS2 = [
5230
+ "prepare",
5231
+ "put",
5232
+ "getWithMetadata"
5233
+ ];
5234
+ var MIN_ARGUMENTS = /* @__PURE__ */ new Map([["put", 2]]);
5235
+ function compile2(patterns) {
5236
+ const compiled = [];
5237
+ for (const pattern of patterns) {
5238
+ try {
5239
+ compiled.push(new RegExp(pattern));
5240
+ } catch {
5241
+ }
5242
+ }
5243
+ return compiled;
5244
+ }
5245
+ function storageMethodName(node, methods) {
5246
+ const callee = node.callee;
5247
+ if (callee.type !== AST_NODE_TYPES25.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES25.Identifier) {
5248
+ return null;
5249
+ }
5250
+ const name = callee.property.name;
5251
+ if (!methods.has(name)) {
5252
+ return null;
5253
+ }
5254
+ if (node.arguments.length < (MIN_ARGUMENTS.get(name) ?? 1)) {
5255
+ return null;
5256
+ }
5257
+ return name;
5258
+ }
5259
+ var no_storage_in_stateless_modules_default = ESLintUtils39.RuleCreator(
5260
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5261
+ )({
5262
+ name: "no-storage-in-stateless-modules",
5263
+ meta: {
5264
+ type: "problem",
5265
+ docs: {
5266
+ description: "Disallow SQL or key/value access inside modules a team has declared stateless; derive state from the systems of record instead. No-op until `modules` is configured."
5267
+ },
5268
+ schema: [
5269
+ {
5270
+ type: "object",
5271
+ properties: {
5272
+ modules: {
5273
+ type: "array",
5274
+ items: { type: "string" },
5275
+ description: "Regex sources matched against the filename. Empty (the default) disables the rule."
5276
+ },
5277
+ methods: {
5278
+ type: "array",
5279
+ items: { type: "string" },
5280
+ description: "Storage method names to flag. Replaces the defaults."
5281
+ }
5282
+ },
5283
+ additionalProperties: false
5284
+ }
5285
+ ],
5286
+ messages: {
5287
+ storageInStatelessModule: "`{{method}}()` reaches for private storage inside a module declared stateless. Derive the state from a read against the system of record, or from a marker in the artefact this feature already produces."
5288
+ }
5289
+ },
5290
+ defaultOptions: [{}],
5291
+ create(context, [options]) {
5292
+ const modules = options?.modules ?? [];
5293
+ if (modules.length === 0) {
5294
+ return {};
5295
+ }
5296
+ const scoped = compile2(modules);
5297
+ if (!scoped.some((re) => re.test(context.filename))) {
5298
+ return {};
5299
+ }
5300
+ const methods = new Set(options?.methods ?? DEFAULT_METHODS2);
5301
+ return {
5302
+ CallExpression(node) {
5303
+ const method = storageMethodName(node, methods);
5304
+ if (method !== null) {
5305
+ context.report({
5306
+ node,
5307
+ messageId: "storageInStatelessModule",
5308
+ data: { method }
5309
+ });
5310
+ }
5311
+ }
5312
+ };
5313
+ }
5314
+ });
5315
+
5316
+ // src/rules/no-zod-native-enum.ts
5317
+ import {
5318
+ ESLintUtils as ESLintUtils40,
5319
+ AST_NODE_TYPES as AST_NODE_TYPES26
5320
+ } from "@typescript-eslint/utils";
5321
+ import * as ts2 from "typescript";
5322
+ var IGNORE_PATTERNS2 = [
5323
+ /[\\/]generated[\\/]/,
5324
+ /\.gen\.tsx?$/,
5325
+ /\.generated\.tsx?$/,
5326
+ /\.d\.ts$/
5327
+ ];
5328
+ function isIgnoredFile2(filename, sourceText) {
5329
+ if (IGNORE_PATTERNS2.some((re) => re.test(filename))) {
5330
+ return true;
5331
+ }
5332
+ return /@generated\b/.test(sourceText.slice(0, 1024));
5333
+ }
5334
+ function isZodModule(source) {
5335
+ return /(^|[/@-])zod([/-]|$)/.test(source);
5336
+ }
5337
+ function unwrap3(node) {
5338
+ if (node.type === AST_NODE_TYPES26.TSAsExpression || node.type === AST_NODE_TYPES26.TSSatisfiesExpression) {
5339
+ return unwrap3(node.expression);
5340
+ }
5341
+ return node;
5342
+ }
5343
+ function stringValueTexts(node, sourceCode) {
5344
+ const texts = [];
5345
+ for (const prop of node.properties) {
5346
+ if (prop.type !== AST_NODE_TYPES26.Property) {
5347
+ return null;
5348
+ }
5349
+ if (prop.computed || prop.shorthand || prop.method || prop.kind !== "init") {
5350
+ return null;
5351
+ }
5352
+ const value = prop.value;
5353
+ if (value.type !== AST_NODE_TYPES26.Literal || typeof value.value !== "string") {
5354
+ return null;
5355
+ }
5356
+ const text = sourceCode.getText(value);
5357
+ if (!texts.includes(text)) {
5358
+ texts.push(text);
5359
+ }
5360
+ }
5361
+ return texts.length > 0 ? texts : null;
5362
+ }
5363
+ function resolvesToLocalEnum(node, scope) {
5364
+ let current = scope;
5365
+ while (current !== null) {
5366
+ const variable = current.variables.find((v) => v.name === node.name);
5367
+ if (variable !== void 0) {
5368
+ return variable.defs.some(
5369
+ (def) => def.node.type === AST_NODE_TYPES26.TSEnumDeclaration
5370
+ );
5371
+ }
5372
+ current = current.upper;
5373
+ }
5374
+ return false;
5375
+ }
5376
+ var ENUM_SYMBOL_FLAGS = ts2.SymbolFlags.RegularEnum | ts2.SymbolFlags.ConstEnum | ts2.SymbolFlags.Enum;
5377
+ function resolvesToImportedEnum(node, services) {
5378
+ const checker = services.program.getTypeChecker();
5379
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
5380
+ let symbol = checker.getSymbolAtLocation(tsNode);
5381
+ if (symbol === void 0) {
5382
+ return false;
5383
+ }
5384
+ if ((symbol.flags & ts2.SymbolFlags.Alias) !== 0) {
5385
+ symbol = checker.getAliasedSymbol(symbol);
5386
+ }
5387
+ return (symbol.flags & ENUM_SYMBOL_FLAGS) !== 0;
5388
+ }
5389
+ var no_zod_native_enum_default = ESLintUtils40.RuleCreator(
5390
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
5391
+ )({
5392
+ name: "no-zod-native-enum",
5393
+ meta: {
5394
+ type: "suggestion",
5395
+ fixable: "code",
5396
+ docs: {
5397
+ description: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.'
5398
+ },
5399
+ schema: [],
5400
+ messages: {
5401
+ nativeEnum: '`z.nativeEnum()` exists to wrap a TypeScript `enum`, which `no-enum` bans. Use `z.enum(["a", "b"])` and derive the union with `z.infer<typeof Schema>`.',
5402
+ enumOfTsEnum: '`z.enum()` is being passed the TypeScript enum `{{name}}`, which `no-enum` bans. Pass a string-literal array instead: `z.enum(["a", "b"])`.'
5403
+ }
5404
+ },
5405
+ defaultOptions: [],
5406
+ create(context) {
5407
+ const sourceCode = context.sourceCode;
5408
+ if (isIgnoredFile2(context.filename, sourceCode.getText())) {
5409
+ return {};
5410
+ }
5411
+ let services;
5412
+ try {
5413
+ services = ESLintUtils40.getParserServices(context);
5414
+ } catch {
5415
+ services = null;
5416
+ }
5417
+ const zodImportedNames = /* @__PURE__ */ new Map();
5418
+ function isZodMemberCall(node, api) {
5419
+ const callee = node.callee;
5420
+ if (callee.type === AST_NODE_TYPES26.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES26.Identifier) {
5421
+ return callee.property.name === api;
5422
+ }
5423
+ if (callee.type === AST_NODE_TYPES26.Identifier) {
5424
+ return zodImportedNames.get(callee.name) === api;
5425
+ }
5426
+ return false;
5427
+ }
5428
+ function buildFix(node) {
5429
+ const callee = node.callee;
5430
+ if (callee.type !== AST_NODE_TYPES26.MemberExpression || callee.property.type !== AST_NODE_TYPES26.Identifier) {
5431
+ return null;
5432
+ }
5433
+ const arg = node.arguments[0];
5434
+ if (arg === void 0 || node.arguments.length !== 1 || arg.type === AST_NODE_TYPES26.SpreadElement) {
5435
+ return null;
5436
+ }
5437
+ const inner = unwrap3(arg);
5438
+ if (inner.type !== AST_NODE_TYPES26.ObjectExpression) {
5439
+ return null;
5440
+ }
5441
+ const values = stringValueTexts(inner, sourceCode);
5442
+ if (values === null) {
5443
+ return null;
5444
+ }
5445
+ const property = callee.property;
5446
+ const replacementArg = `[${values.join(", ")}]`;
5447
+ return (fixer) => [
5448
+ fixer.replaceText(property, "enum"),
5449
+ fixer.replaceText(arg, replacementArg)
5450
+ ];
5451
+ }
5452
+ return {
5453
+ ImportDeclaration(node) {
5454
+ if (!isZodModule(node.source.value)) {
5455
+ return;
5456
+ }
5457
+ for (const spec of node.specifiers) {
5458
+ if (spec.type === AST_NODE_TYPES26.ImportSpecifier && spec.imported.type === AST_NODE_TYPES26.Identifier) {
5459
+ zodImportedNames.set(spec.local.name, spec.imported.name);
5460
+ }
5461
+ }
5462
+ },
5463
+ CallExpression(node) {
5464
+ if (isZodMemberCall(node, "nativeEnum")) {
5465
+ const fix = buildFix(node);
5466
+ context.report({
5467
+ node,
5468
+ messageId: "nativeEnum",
5469
+ ...fix === null ? {} : { fix }
5470
+ });
5471
+ return;
5472
+ }
5473
+ if (!isZodMemberCall(node, "enum")) {
5474
+ return;
5475
+ }
5476
+ const arg = node.arguments[0];
5477
+ if (arg === void 0 || arg.type !== AST_NODE_TYPES26.Identifier) {
5478
+ return;
5479
+ }
5480
+ const isEnum = resolvesToLocalEnum(arg, sourceCode.getScope(arg)) || services !== null && resolvesToImportedEnum(arg, services);
5481
+ if (isEnum) {
5482
+ context.report({
5483
+ node,
5484
+ messageId: "enumOfTsEnum",
5485
+ data: { name: arg.name }
5486
+ });
5487
+ }
5488
+ }
5489
+ };
5490
+ }
5491
+ });
5492
+
5493
+ // src/rules/prefer-module-level-constant.ts
5494
+ import {
5495
+ ESLintUtils as ESLintUtils41,
5496
+ AST_NODE_TYPES as AST_NODE_TYPES27
5497
+ } from "@typescript-eslint/utils";
5498
+ var DEFAULT_MIN_ELEMENTS = 3;
5499
+ var MAX_LITERAL_DEPTH = 4;
5500
+ var IGNORE_PATTERNS3 = [
5501
+ /[\\/]generated[\\/]/,
5502
+ /\.gen\.tsx?$/,
5503
+ /\.generated\.tsx?$/,
5504
+ /\.d\.ts$/
5505
+ ];
5506
+ var TEST_FILE_PATTERNS = [
5507
+ /\.(?:test|spec)\.[cm]?[jt]sx?$/,
5508
+ /[\\/]__tests__[\\/]/,
5509
+ /[\\/]__mocks__[\\/]/,
5510
+ /[\\/]tests?[\\/]/,
5511
+ /\.stories\.[cm]?[jt]sx?$/
5512
+ ];
5513
+ var MUTATING_METHODS = /* @__PURE__ */ new Set([
5514
+ // Array
5515
+ "push",
5516
+ "pop",
5517
+ "shift",
5518
+ "unshift",
5519
+ "splice",
5520
+ "sort",
5521
+ "reverse",
5522
+ "fill",
5523
+ "copyWithin",
5524
+ // Set / Map
5525
+ "add",
5526
+ "set",
5527
+ "delete",
5528
+ "clear",
5529
+ // Object-ish escape hatches
5530
+ "assign"
5531
+ ]);
5532
+ var FUNCTION_TYPES3 = /* @__PURE__ */ new Set([
5533
+ AST_NODE_TYPES27.FunctionDeclaration,
5534
+ AST_NODE_TYPES27.FunctionExpression,
5535
+ AST_NODE_TYPES27.ArrowFunctionExpression
5536
+ ]);
5537
+ var COLLECTION_CONSTRUCTORS = /* @__PURE__ */ new Set(["Set", "Map"]);
5538
+ function isIgnoredFile3(filename, sourceText) {
5539
+ if (IGNORE_PATTERNS3.some((re) => re.test(filename))) {
5540
+ return true;
5541
+ }
5542
+ return /@generated\b/.test(sourceText.slice(0, 1024));
5543
+ }
5544
+ function isTestFile2(filename) {
5545
+ return TEST_FILE_PATTERNS.some((re) => re.test(filename));
5546
+ }
5547
+ function unwrap4(node) {
5548
+ if (node.type === AST_NODE_TYPES27.TSAsExpression || node.type === AST_NODE_TYPES27.TSSatisfiesExpression || node.type === AST_NODE_TYPES27.TSNonNullExpression) {
5549
+ return unwrap4(node.expression);
5550
+ }
5551
+ return node;
5552
+ }
5553
+ function isRegexLiteral(node) {
5554
+ return node.type === AST_NODE_TYPES27.Literal && "regex" in node && node.regex !== void 0;
5555
+ }
5556
+ function isLiteralOnly(node, depth) {
5557
+ if (depth > MAX_LITERAL_DEPTH) {
5558
+ return false;
5559
+ }
5560
+ const inner = unwrap4(node);
5561
+ switch (inner.type) {
5562
+ case AST_NODE_TYPES27.Literal: {
5563
+ return true;
5564
+ }
5565
+ case AST_NODE_TYPES27.TemplateLiteral: {
5566
+ return inner.expressions.length === 0;
5567
+ }
5568
+ case AST_NODE_TYPES27.UnaryExpression: {
5569
+ return (inner.operator === "-" || inner.operator === "+") && inner.argument.type === AST_NODE_TYPES27.Literal && typeof inner.argument.value === "number";
5570
+ }
5571
+ case AST_NODE_TYPES27.ArrayExpression: {
5572
+ return inner.elements.every(
5573
+ (el) => el !== null && el.type !== AST_NODE_TYPES27.SpreadElement && isLiteralOnly(el, depth + 1)
5574
+ );
5575
+ }
5576
+ case AST_NODE_TYPES27.ObjectExpression: {
5577
+ return inner.properties.every((prop) => {
5578
+ if (prop.type !== AST_NODE_TYPES27.Property) {
5579
+ return false;
5580
+ }
5581
+ if (prop.shorthand || prop.method || prop.kind !== "init") {
5582
+ return false;
5583
+ }
5584
+ if (prop.computed && prop.key.type !== AST_NODE_TYPES27.Literal) {
5585
+ return false;
5586
+ }
5587
+ return isLiteralOnly(prop.value, depth + 1);
5588
+ });
5589
+ }
5590
+ default: {
5591
+ return false;
5592
+ }
5593
+ }
5594
+ }
5595
+ function unwrapObjectFreeze(node) {
5596
+ const inner = unwrap4(node);
5597
+ if (inner.type === AST_NODE_TYPES27.CallExpression && inner.callee.type === AST_NODE_TYPES27.MemberExpression && !inner.callee.computed && inner.callee.object.type === AST_NODE_TYPES27.Identifier && inner.callee.object.name === "Object" && inner.callee.property.type === AST_NODE_TYPES27.Identifier && inner.callee.property.name === "freeze" && inner.arguments.length === 1 && inner.arguments[0] !== void 0 && inner.arguments[0].type !== AST_NODE_TYPES27.SpreadElement) {
5598
+ return unwrap4(inner.arguments[0]);
5599
+ }
5600
+ return inner;
5601
+ }
5602
+ function classify(init, checkRegex) {
5603
+ const node = unwrapObjectFreeze(init);
5604
+ if (isRegexLiteral(node)) {
5605
+ if (!checkRegex) {
5606
+ return null;
5607
+ }
5608
+ if (/[gy]/.test(node.regex.flags)) {
5609
+ return null;
5610
+ }
5611
+ return { kind: "regex", size: 1 };
5612
+ }
5613
+ if (node.type === AST_NODE_TYPES27.ArrayExpression) {
5614
+ return isLiteralOnly(node, 0) ? { kind: "array", size: node.elements.length } : null;
5615
+ }
5616
+ if (node.type === AST_NODE_TYPES27.ObjectExpression) {
5617
+ return isLiteralOnly(node, 0) ? { kind: "object", size: node.properties.length } : null;
5618
+ }
5619
+ if (node.type === AST_NODE_TYPES27.NewExpression && node.callee.type === AST_NODE_TYPES27.Identifier && COLLECTION_CONSTRUCTORS.has(node.callee.name)) {
5620
+ const arg = node.arguments[0];
5621
+ if (node.arguments.length !== 1 || arg === void 0 || arg.type === AST_NODE_TYPES27.SpreadElement) {
5622
+ return null;
5623
+ }
5624
+ const entries = unwrap4(arg);
5625
+ if (entries.type !== AST_NODE_TYPES27.ArrayExpression) {
5626
+ return null;
5627
+ }
5628
+ return isLiteralOnly(entries, 0) ? { kind: node.callee.name === "Set" ? "Set" : "Map", size: entries.elements.length } : null;
5629
+ }
5630
+ return null;
5631
+ }
5632
+ function enclosingFunction2(node) {
5633
+ let current = node.parent;
5634
+ while (current !== void 0 && current !== null) {
5635
+ if (FUNCTION_TYPES3.has(current.type)) {
5636
+ return current;
5637
+ }
5638
+ current = current.parent;
5639
+ }
5640
+ return null;
5641
+ }
5642
+ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
5643
+ [
5644
+ [
5645
+ "Object",
5646
+ /* @__PURE__ */ new Set(["keys", "values", "entries", "freeze", "fromEntries", "assign"])
5647
+ ],
5648
+ ["Array", /* @__PURE__ */ new Set(["from", "isArray"])],
5649
+ ["JSON", /* @__PURE__ */ new Set(["stringify"])]
5650
+ ]
5651
+ );
5652
+ function isNonRetainingBuiltinCall(node, argument) {
5653
+ const callee = node.callee;
5654
+ if (callee.type === AST_NODE_TYPES27.Identifier && callee.name === "structuredClone") {
5655
+ return true;
5656
+ }
5657
+ if (callee.type !== AST_NODE_TYPES27.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES27.Identifier || callee.property.type !== AST_NODE_TYPES27.Identifier) {
5658
+ return false;
5659
+ }
5660
+ const members = NON_RETAINING_BUILTINS.get(callee.object.name);
5661
+ if (members === void 0 || !members.has(callee.property.name)) {
5662
+ return false;
5663
+ }
5664
+ if (callee.object.name === "Object" && callee.property.name === "assign") {
5665
+ return node.arguments[0] !== argument;
5666
+ }
5667
+ return true;
5668
+ }
5669
+ function isSafeRead(identifier) {
5670
+ const parent = identifier.parent;
5671
+ if (parent.type === AST_NODE_TYPES27.MemberExpression) {
5672
+ if (parent.object !== identifier) {
5673
+ return true;
5674
+ }
5675
+ const grandparent = parent.parent;
5676
+ if (grandparent.type === AST_NODE_TYPES27.AssignmentExpression && grandparent.left === parent) {
5677
+ return false;
5678
+ }
5679
+ if (grandparent.type === AST_NODE_TYPES27.UpdateExpression) {
5680
+ return false;
5681
+ }
5682
+ if (grandparent.type === AST_NODE_TYPES27.UnaryExpression && grandparent.operator === "delete") {
5683
+ return false;
5684
+ }
5685
+ if (!parent.computed && parent.property.type === AST_NODE_TYPES27.Identifier && MUTATING_METHODS.has(parent.property.name) && grandparent.type === AST_NODE_TYPES27.CallExpression && grandparent.callee === parent) {
5686
+ return false;
5687
+ }
5688
+ return true;
5689
+ }
5690
+ if (parent.type === AST_NODE_TYPES27.ForOfStatement && parent.right === identifier) {
5691
+ return true;
5692
+ }
5693
+ if (parent.type === AST_NODE_TYPES27.SpreadElement) {
5694
+ return true;
5695
+ }
5696
+ if (parent.type === AST_NODE_TYPES27.BinaryExpression) {
5697
+ return true;
5698
+ }
5699
+ if (parent.type === AST_NODE_TYPES27.CallExpression && parent.arguments.includes(identifier) && isNonRetainingBuiltinCall(parent, identifier)) {
5700
+ return true;
5701
+ }
5702
+ if (parent.type === AST_NODE_TYPES27.UnaryExpression && parent.operator !== "delete") {
5703
+ return true;
5704
+ }
5705
+ return false;
5706
+ }
5707
+ var prefer_module_level_constant_default = ESLintUtils41.RuleCreator(
5708
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
5709
+ )({
5710
+ name: "prefer-module-level-constant",
5711
+ meta: {
5712
+ type: "suggestion",
5713
+ docs: {
5714
+ description: "Hoist literal-only constant collections and regexes out of function bodies to module scope so they are allocated once."
5715
+ },
5716
+ schema: [
5717
+ {
5718
+ type: "object",
5719
+ additionalProperties: false,
5720
+ properties: {
5721
+ minElements: { type: "number", minimum: 1 },
5722
+ checkRegex: { type: "boolean" },
5723
+ ignoreTestFiles: { type: "boolean" }
5724
+ }
5725
+ }
5726
+ ],
5727
+ messages: {
5728
+ hoistCollection: "`{{name}}` is a literal-only {{kind}} rebuilt on every call. Hoist it to module scope so it is allocated once and can be reused, exported, and tested.",
5729
+ hoistRegex: "`{{name}}` is a constant regex recompiled on every call. Hoist it to module scope."
5730
+ }
5731
+ },
5732
+ defaultOptions: [{}],
5733
+ create(context, [optionsArg]) {
5734
+ const options = optionsArg ?? {};
5735
+ const minElements = options.minElements ?? DEFAULT_MIN_ELEMENTS;
5736
+ const checkRegex = options.checkRegex ?? true;
5737
+ const ignoreTestFiles = options.ignoreTestFiles ?? true;
5738
+ const sourceCode = context.sourceCode;
5739
+ const filename = context.filename;
5740
+ if (isIgnoredFile3(filename, sourceCode.getText())) {
5741
+ return {};
5742
+ }
5743
+ if (ignoreTestFiles && isTestFile2(filename)) {
5744
+ return {};
5745
+ }
5746
+ function allReferencesAreSafeReads(declarator) {
5747
+ const variables = sourceCode.getDeclaredVariables(declarator);
5748
+ const variable = variables[0];
5749
+ if (variable === void 0) {
5750
+ return false;
5751
+ }
5752
+ for (const reference of variable.references) {
5753
+ if (reference.init === true) {
5754
+ continue;
5755
+ }
5756
+ if (reference.isWrite()) {
5757
+ return false;
5758
+ }
5759
+ if (reference.identifier.type !== AST_NODE_TYPES27.Identifier) {
5760
+ return false;
5761
+ }
5762
+ if (!isSafeRead(reference.identifier)) {
5763
+ return false;
5764
+ }
5765
+ }
5766
+ return true;
5767
+ }
5768
+ return {
5769
+ VariableDeclarator(node) {
5770
+ const declaration = node.parent;
5771
+ if (declaration.type !== AST_NODE_TYPES27.VariableDeclaration || declaration.kind !== "const" || declaration.declare === true) {
5772
+ return;
5773
+ }
5774
+ if (node.id.type !== AST_NODE_TYPES27.Identifier || node.init === null) {
5775
+ return;
5776
+ }
5777
+ if (enclosingFunction2(node) === null) {
5778
+ return;
5779
+ }
5780
+ const candidate = classify(node.init, checkRegex);
5781
+ if (candidate === null) {
5782
+ return;
5783
+ }
5784
+ if (candidate.kind !== "regex" && candidate.size < minElements) {
5785
+ return;
5786
+ }
5787
+ if (!allReferencesAreSafeReads(node)) {
5788
+ return;
5789
+ }
5790
+ context.report({
5791
+ node: node.id,
5792
+ messageId: candidate.kind === "regex" ? "hoistRegex" : "hoistCollection",
5793
+ data: { name: node.id.name, kind: candidate.kind }
5794
+ });
5795
+ }
5796
+ };
5797
+ }
5798
+ });
5799
+
4037
5800
  // src/index.ts
4038
5801
  var rules = {
4039
5802
  "enforce-file-structure": enforce_file_structure_default,
@@ -4064,12 +5827,24 @@ var rules = {
4064
5827
  "single-public-export": single_public_export_default,
4065
5828
  "no-silent-promise-catch": no_silent_promise_catch_default,
4066
5829
  "require-fetch-timeout": require_fetch_timeout_default,
4067
- "require-schema-validate-search": require_schema_validate_search_default
5830
+ "require-schema-validate-search": require_schema_validate_search_default,
5831
+ "no-offset-pagination": no_offset_pagination_default,
5832
+ "no-positional-tuple-return": no_positional_tuple_return_default,
5833
+ "no-repeated-string-literal": no_repeated_string_literal_default,
5834
+ "no-select-star": no_select_star_default,
5835
+ "no-sleep-in-test-body": no_sleep_in_test_body_default,
5836
+ "prefer-constant-time-secret-compare": prefer_constant_time_secret_compare_default,
5837
+ "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
5838
+ "no-dynamic-sql": no_dynamic_sql_default,
5839
+ "no-raw-fetch-outside-clients": no_raw_fetch_outside_clients_default,
5840
+ "no-storage-in-stateless-modules": no_storage_in_stateless_modules_default,
5841
+ "no-zod-native-enum": no_zod_native_enum_default,
5842
+ "prefer-module-level-constant": prefer_module_level_constant_default
4068
5843
  };
4069
5844
  var plugin = {
4070
5845
  meta: {
4071
5846
  name: "@sarj/eslint-plugin",
4072
- version: "2.7.0"
5847
+ version: "2.9.0"
4073
5848
  },
4074
5849
  rules,
4075
5850
  configs: {
@@ -4105,7 +5880,27 @@ var plugin = {
4105
5880
  // Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
4106
5881
  "@sarj/require-fetch-timeout": "warn",
4107
5882
  "@sarj/no-silent-promise-catch": "warn",
4108
- "@sarj/require-schema-validate-search": "warn"
5883
+ "@sarj/require-schema-validate-search": "warn",
5884
+ // Second SARJ port wave — the TS/Python parity gap. Each targets a
5885
+ // defect class seen in production Workers code: timing-leaky secret
5886
+ // compares, non-idempotent store writes under queue redelivery,
5887
+ // O(N) pagination, implicit row contracts, flaky timed tests.
5888
+ "@sarj/prefer-constant-time-secret-compare": "error",
5889
+ "@sarj/store-insert-requires-on-conflict": "warn",
5890
+ "@sarj/no-offset-pagination": "warn",
5891
+ "@sarj/no-select-star": "warn",
5892
+ "@sarj/no-sleep-in-test-body": "warn",
5893
+ "@sarj/no-repeated-string-literal": "warn",
5894
+ "@sarj/no-positional-tuple-return": "warn",
5895
+ // Injection guard — low FP, applies to any repo touching SQL.
5896
+ "@sarj/no-dynamic-sql": "warn",
5897
+ // Mined from two years of PR review (SARJ-928). Schema-layer sibling of
5898
+ // `no-enum`; autofixable for inline string-literal objects.
5899
+ "@sarj/no-zod-native-enum": "warn",
5900
+ // Mined from two years of PR review — the single most frequent uncovered
5901
+ // theme (~37 PRs). Measured 17 hits / 1085 real TS files, all true
5902
+ // positives, so it is safe to run everywhere.
5903
+ "@sarj/prefer-module-level-constant": "warn"
4109
5904
  }
4110
5905
  },
4111
5906
  strict: {
@@ -4145,7 +5940,30 @@ var plugin = {
4145
5940
  // Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
4146
5941
  "@sarj/require-fetch-timeout": "error",
4147
5942
  "@sarj/no-silent-promise-catch": "error",
4148
- "@sarj/require-schema-validate-search": "error"
5943
+ "@sarj/require-schema-validate-search": "error",
5944
+ // Second SARJ port wave — the TS/Python parity gap.
5945
+ "@sarj/prefer-constant-time-secret-compare": "error",
5946
+ "@sarj/store-insert-requires-on-conflict": "error",
5947
+ "@sarj/no-offset-pagination": "error",
5948
+ "@sarj/no-select-star": "error",
5949
+ "@sarj/no-sleep-in-test-body": "error",
5950
+ "@sarj/no-repeated-string-literal": "error",
5951
+ // API-shape advice rather than a runtime defect — a corpus sweep found its
5952
+ // only hits are parser `[value, cursor]` returns, which are conventional.
5953
+ // Warn even in strict until a rollout justifies more.
5954
+ "@sarj/no-positional-tuple-return": "warn",
5955
+ "@sarj/no-dynamic-sql": "error",
5956
+ // Architectural: both need per-repo config to be meaningful, so they
5957
+ // are strict-only. `no-storage-in-stateless-modules` is a no-op until
5958
+ // its `modules` option names the directories a team declared stateless;
5959
+ // `no-raw-fetch-outside-clients` defaults to the `clients/` convention
5960
+ // and takes an `allow` list for repos that lay their client layer out
5961
+ // differently.
5962
+ "@sarj/no-raw-fetch-outside-clients": "error",
5963
+ "@sarj/no-storage-in-stateless-modules": "error",
5964
+ // Mined from two years of PR review (SARJ-928).
5965
+ "@sarj/no-zod-native-enum": "error",
5966
+ "@sarj/prefer-module-level-constant": "error"
4149
5967
  }
4150
5968
  }
4151
5969
  }