@sarj/eslint-plugin 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -350,12 +350,10 @@ var no_comment_cruft_default = import_utils3.ESLintUtils.RuleCreator(
350
350
  }
351
351
  const first = leading[0];
352
352
  if (first === void 0 || leading.length < LEADING_PREAMBLE_MIN) return;
353
- const isLicense = leading.some(
354
- (c) => LICENSE_RE.test(stripCommentMarker(c.value))
355
- );
356
- if (!isLicense) {
357
- context.report({ node: first, messageId: "fileHeaderPreamble" });
358
- }
353
+ const bodies = leading.map((c) => stripCommentMarker(c.value));
354
+ if (bodies.some((body) => LICENSE_RE.test(body))) return;
355
+ if (bodies.some((body) => isProse(body))) return;
356
+ context.report({ node: first, messageId: "fileHeaderPreamble" });
359
357
  }
360
358
  return {
361
359
  Program() {
@@ -833,35 +831,15 @@ var LOGGER_NAMES = /* @__PURE__ */ new Set([
833
831
  "_logger",
834
832
  "_log"
835
833
  ]);
834
+ var LOGGER_FACTORIES = /* @__PURE__ */ new Set([
835
+ "getlogger",
836
+ "get_logger"
837
+ ]);
836
838
  var REPORT_NAME_RE = /error|report|capture|log|trace|warn/i;
837
- function isLoggerReceiver(expr) {
838
- switch (expr.type) {
839
- case "Identifier":
840
- return LOGGER_NAMES.has(expr.name.toLowerCase());
841
- case "MemberExpression": {
842
- const { property, object } = expr;
843
- if (!expr.computed && property.type === "Identifier" && LOGGER_NAMES.has(property.name.toLowerCase())) {
844
- return true;
845
- }
846
- return isLoggerReceiver(object);
847
- }
848
- default:
849
- return false;
850
- }
851
- }
852
- function isLoggingCall(expr) {
853
- if (expr.type !== "CallExpression") {
854
- return false;
855
- }
856
- const callee = expr.callee;
857
- if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
858
- return false;
859
- }
860
- if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
861
- return false;
862
- }
863
- return isLoggerReceiver(callee.object);
864
- }
839
+ var LOGGING_OPTION_PROPERTIES = {
840
+ loggerNames: { type: "array", items: { type: "string" } },
841
+ logFunctions: { type: "array", items: { type: "string" } }
842
+ };
865
843
  function calleeName(callee) {
866
844
  if (callee.type === "Identifier") {
867
845
  return callee.name;
@@ -871,6 +849,65 @@ function calleeName(callee) {
871
849
  }
872
850
  return null;
873
851
  }
852
+ function createLogMatcher(options = {}) {
853
+ const loggerNames = /* @__PURE__ */ new Set([
854
+ ...LOGGER_NAMES,
855
+ ...(options.loggerNames ?? []).map((name) => name.toLowerCase())
856
+ ]);
857
+ const logFunctions = new Set(options.logFunctions ?? []);
858
+ function isLoggerReceiver(expr) {
859
+ switch (expr.type) {
860
+ case "Identifier":
861
+ return loggerNames.has(expr.name.toLowerCase());
862
+ case "MemberExpression": {
863
+ const { property, object } = expr;
864
+ if (!expr.computed && property.type === "Identifier") {
865
+ const lowered = property.name.toLowerCase();
866
+ if (loggerNames.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
867
+ return true;
868
+ }
869
+ }
870
+ return isLoggerReceiver(object);
871
+ }
872
+ case "CallExpression": {
873
+ const callee = expr.callee;
874
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
875
+ return true;
876
+ }
877
+ if (callee.type !== "Super") {
878
+ return isLoggerReceiver(callee);
879
+ }
880
+ return false;
881
+ }
882
+ default:
883
+ return false;
884
+ }
885
+ }
886
+ function isLogFunctionCall(expr) {
887
+ if (expr.type !== "CallExpression" || logFunctions.size === 0) {
888
+ return false;
889
+ }
890
+ const name = calleeName(expr.callee);
891
+ return name !== null && logFunctions.has(name);
892
+ }
893
+ function isLoggingCall(expr) {
894
+ if (expr.type !== "CallExpression") {
895
+ return false;
896
+ }
897
+ if (isLogFunctionCall(expr)) {
898
+ return true;
899
+ }
900
+ const callee = expr.callee;
901
+ if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
902
+ return false;
903
+ }
904
+ if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
905
+ return false;
906
+ }
907
+ return isLoggerReceiver(callee.object);
908
+ }
909
+ return { isLoggerReceiver, isLogFunctionCall, isLoggingCall };
910
+ }
874
911
 
875
912
  // src/rules/no-log-only-catch.ts
876
913
  var DEFAULT_IGNORE_PATTERNS2 = [
@@ -878,12 +915,6 @@ var DEFAULT_IGNORE_PATTERNS2 = [
878
915
  /\.spec\./,
879
916
  /[\\/]__tests__[\\/]/
880
917
  ];
881
- function isLoggingCallStatement(statement) {
882
- if (statement.type !== "ExpressionStatement") {
883
- return false;
884
- }
885
- return isLoggingCall(statement.expression);
886
- }
887
918
  var no_log_only_catch_default = import_utils8.ESLintUtils.RuleCreator(
888
919
  (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
889
920
  )({
@@ -893,15 +924,28 @@ var no_log_only_catch_default = import_utils8.ESLintUtils.RuleCreator(
893
924
  docs: {
894
925
  description: "Disallow `catch` clauses that only log (or silently do nothing) and then swallow the error; rethrow or handle it instead."
895
926
  },
896
- schema: [],
927
+ schema: [
928
+ {
929
+ type: "object",
930
+ additionalProperties: false,
931
+ properties: { ...LOGGING_OPTION_PROPERTIES }
932
+ }
933
+ ],
897
934
  messages: {
898
935
  noLogOnlyCatch: "Logging then swallowing the error hides failures. Rethrow the error or handle it for real.",
899
936
  emptyCatch: "Empty catch silently swallows the error. Rethrow it, handle it, or add a comment explaining why it is safe to ignore."
900
937
  }
901
938
  },
902
- defaultOptions: [],
903
- create(context) {
939
+ defaultOptions: [{}],
940
+ create(context, [loggingOptions]) {
941
+ const matcher = createLogMatcher(loggingOptions);
904
942
  const filename = context.filename;
943
+ function isLoggingCallStatement(statement) {
944
+ if (statement.type !== "ExpressionStatement") {
945
+ return false;
946
+ }
947
+ return matcher.isLoggingCall(statement.expression);
948
+ }
905
949
  const isIgnoredByDefault = DEFAULT_IGNORE_PATTERNS2.some(
906
950
  (re) => re.test(filename)
907
951
  );
@@ -1071,46 +1115,131 @@ function containsThrow(node) {
1071
1115
  (current) => current.type === import_utils10.AST_NODE_TYPES.ThrowStatement
1072
1116
  );
1073
1117
  }
1074
- function argsIncludeBinding(args, caughtName) {
1075
- if (caughtName === null) {
1076
- return false;
1118
+ function bindsName(param, name) {
1119
+ switch (param.type) {
1120
+ case import_utils10.AST_NODE_TYPES.Identifier:
1121
+ return param.name === name;
1122
+ case import_utils10.AST_NODE_TYPES.AssignmentPattern:
1123
+ return bindsName(param.left, name);
1124
+ case import_utils10.AST_NODE_TYPES.RestElement:
1125
+ return bindsName(param.argument, name);
1126
+ case import_utils10.AST_NODE_TYPES.ArrayPattern:
1127
+ return param.elements.some(
1128
+ (element) => element !== null && bindsName(element, name)
1129
+ );
1130
+ case import_utils10.AST_NODE_TYPES.ObjectPattern:
1131
+ return param.properties.some(
1132
+ (property) => property.type === import_utils10.AST_NODE_TYPES.RestElement ? bindsName(property.argument, name) : bindsName(property.value, name)
1133
+ );
1134
+ default:
1135
+ return false;
1077
1136
  }
1078
- return args.some(
1079
- (arg) => arg.type === import_utils10.AST_NODE_TYPES.Identifier && arg.name === caughtName
1080
- );
1081
1137
  }
1082
- function logsOrReportsError(catchBody, caughtName) {
1083
- return walkWithinScope(catchBody, (current) => {
1084
- if (current.type !== import_utils10.AST_NODE_TYPES.CallExpression) {
1085
- return false;
1138
+ function subtreeReadsName(node, name) {
1139
+ let found = false;
1140
+ const shadowsName = (fn) => isFunctionNode(fn) && fn.params.some(
1141
+ (param) => bindsName(param, name)
1142
+ );
1143
+ const recurse = (current) => {
1144
+ if (found) {
1145
+ return;
1086
1146
  }
1087
- if (isLoggingCall(current)) {
1088
- return true;
1147
+ if (current.type === import_utils10.AST_NODE_TYPES.Identifier && current.name === name) {
1148
+ found = true;
1149
+ return;
1089
1150
  }
1090
- const name = calleeName(current.callee);
1091
- return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
1092
- });
1151
+ if (shadowsName(current)) {
1152
+ return;
1153
+ }
1154
+ for (const key of Object.keys(current)) {
1155
+ if (key === "parent") {
1156
+ continue;
1157
+ }
1158
+ if (key === "key" && current.type === import_utils10.AST_NODE_TYPES.Property && !current.computed) {
1159
+ continue;
1160
+ }
1161
+ if (key === "property" && current.type === import_utils10.AST_NODE_TYPES.MemberExpression && !current.computed) {
1162
+ continue;
1163
+ }
1164
+ const value = current[key];
1165
+ if (Array.isArray(value)) {
1166
+ for (const child of value) {
1167
+ if (isNode(child)) {
1168
+ recurse(child);
1169
+ }
1170
+ }
1171
+ } else if (isNode(value)) {
1172
+ recurse(value);
1173
+ }
1174
+ }
1175
+ };
1176
+ recurse(node);
1177
+ return found;
1178
+ }
1179
+ function argsIncludeBinding(args, caughtName) {
1180
+ if (caughtName === null) {
1181
+ return false;
1182
+ }
1183
+ return args.some((arg) => subtreeReadsName(arg, caughtName));
1093
1184
  }
1094
1185
  function tryBlockOf(catchNode) {
1095
1186
  return catchNode.parent.block;
1096
1187
  }
1097
- function isSafeParseExpression(arg) {
1098
- if (arg === null) {
1099
- return false;
1100
- }
1101
- if (arg.type === import_utils10.AST_NODE_TYPES.CallExpression && arg.callee.type === import_utils10.AST_NODE_TYPES.MemberExpression && !arg.callee.computed && arg.callee.property.type === import_utils10.AST_NODE_TYPES.Identifier && arg.callee.property.name === "parse") {
1102
- return true;
1188
+ var SAFE_PARSE_CONSTRUCTORS = /* @__PURE__ */ new Set([
1189
+ "RegExp",
1190
+ "URL",
1191
+ "URLPattern"
1192
+ ]);
1193
+ function isParseShapedNode(node) {
1194
+ if (node.type === import_utils10.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils10.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils10.AST_NODE_TYPES.Identifier) {
1195
+ return node.callee.property.name === "parse";
1103
1196
  }
1104
- if (arg.type === import_utils10.AST_NODE_TYPES.NewExpression && arg.callee.type === import_utils10.AST_NODE_TYPES.Identifier) {
1105
- return arg.callee.name === "RegExp" || arg.callee.name === "URL";
1197
+ if (node.type === import_utils10.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils10.AST_NODE_TYPES.Identifier) {
1198
+ return SAFE_PARSE_CONSTRUCTORS.has(node.callee.name);
1106
1199
  }
1107
1200
  return false;
1108
1201
  }
1202
+ var BODY_DECODE_METHODS = /* @__PURE__ */ new Set([
1203
+ "json",
1204
+ "text",
1205
+ "arrayBuffer"
1206
+ ]);
1207
+ function isBodyDecodeNode(node) {
1208
+ return node.type === import_utils10.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils10.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils10.AST_NODE_TYPES.Identifier && BODY_DECODE_METHODS.has(node.callee.property.name);
1209
+ }
1210
+ function returnsMatching(stmt, predicate) {
1211
+ return stmt.type === import_utils10.AST_NODE_TYPES.ReturnStatement && stmt.argument !== null && walkWithinScope(stmt.argument, predicate);
1212
+ }
1213
+ function enclosingReturnTypeNode(node) {
1214
+ let current = node.parent;
1215
+ while (current !== void 0 && current !== null) {
1216
+ if (isFunctionNode(current) && "returnType" in current) {
1217
+ return current.returnType?.typeAnnotation ?? null;
1218
+ }
1219
+ current = current.parent;
1220
+ }
1221
+ return null;
1222
+ }
1223
+ function isDeclaredBooleanPredicate(catchNode, kind) {
1224
+ if (kind !== "boolean") {
1225
+ return false;
1226
+ }
1227
+ let declared = enclosingReturnTypeNode(catchNode);
1228
+ if (declared?.type === import_utils10.AST_NODE_TYPES.TSTypeReference && declared.typeName.type === import_utils10.AST_NODE_TYPES.Identifier && declared.typeName.name === "Promise") {
1229
+ declared = declared.typeArguments?.params[0] ?? null;
1230
+ }
1231
+ return declared?.type === import_utils10.AST_NODE_TYPES.TSBooleanKeyword;
1232
+ }
1109
1233
  function tryReturnsSafeParse(catchNode) {
1110
- return walkWithinScope(
1111
- tryBlockOf(catchNode),
1112
- (current) => current.type === import_utils10.AST_NODE_TYPES.ReturnStatement && isSafeParseExpression(current.argument)
1113
- );
1234
+ const tryBlock = tryBlockOf(catchNode);
1235
+ if (walkWithinScope(
1236
+ tryBlock,
1237
+ (current) => returnsMatching(current, isParseShapedNode)
1238
+ )) {
1239
+ return true;
1240
+ }
1241
+ const only = tryBlock.body.length === 1 ? tryBlock.body[0] : void 0;
1242
+ return only !== void 0 && returnsMatching(only, isBodyDecodeNode);
1114
1243
  }
1115
1244
  function enclosingFunctionBody(node) {
1116
1245
  let current = node.parent;
@@ -1156,13 +1285,32 @@ var no_sentinel_return_on_catch_default = import_utils10.ESLintUtils.RuleCreator
1156
1285
  docs: {
1157
1286
  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."
1158
1287
  },
1159
- schema: [],
1288
+ schema: [
1289
+ {
1290
+ type: "object",
1291
+ additionalProperties: false,
1292
+ properties: { ...LOGGING_OPTION_PROPERTIES }
1293
+ }
1294
+ ],
1160
1295
  messages: {
1161
1296
  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."
1162
1297
  }
1163
1298
  },
1164
- defaultOptions: [],
1165
- create(context) {
1299
+ defaultOptions: [{}],
1300
+ create(context, [loggingOptions]) {
1301
+ const matcher = createLogMatcher(loggingOptions);
1302
+ function logsOrReportsError(catchBody, caughtName) {
1303
+ return walkWithinScope(catchBody, (current) => {
1304
+ if (current.type !== import_utils10.AST_NODE_TYPES.CallExpression) {
1305
+ return false;
1306
+ }
1307
+ if (matcher.isLoggingCall(current)) {
1308
+ return true;
1309
+ }
1310
+ const name = calleeName(current.callee);
1311
+ return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
1312
+ });
1313
+ }
1166
1314
  return {
1167
1315
  CatchClause(node) {
1168
1316
  const body = node.body.body;
@@ -1187,6 +1335,9 @@ var no_sentinel_return_on_catch_default = import_utils10.ESLintUtils.RuleCreator
1187
1335
  return;
1188
1336
  }
1189
1337
  const kind = sentinelKind(last.argument);
1338
+ if (kind !== null && isDeclaredBooleanPredicate(node, kind)) {
1339
+ return;
1340
+ }
1190
1341
  if (kind !== null && functionReturnsSameSentinelKindElsewhere(node, kind)) {
1191
1342
  return;
1192
1343
  }
@@ -1291,6 +1442,28 @@ function isThreadedAccumulator(node) {
1291
1442
  }
1292
1443
  return referencesName(node.argument, target);
1293
1444
  }
1445
+ function namesReadBy(test) {
1446
+ const names = /* @__PURE__ */ new Set();
1447
+ visitScope(test, (node) => {
1448
+ if (node.type === "Identifier") {
1449
+ names.add(node.name);
1450
+ }
1451
+ });
1452
+ return names;
1453
+ }
1454
+ function testStateIsAssignedInBody(test, body) {
1455
+ const testNames = namesReadBy(test);
1456
+ if (testNames.size === 0) {
1457
+ return false;
1458
+ }
1459
+ let found = false;
1460
+ visitScope(body, (node) => {
1461
+ if (node.type === "AssignmentExpression" && node.operator === "=" && node.left.type === "Identifier" && testNames.has(node.left.name)) {
1462
+ found = true;
1463
+ }
1464
+ });
1465
+ return found;
1466
+ }
1294
1467
  function shouldReport(awaits, earlyExit, iterableText) {
1295
1468
  if (awaits.length === 0) {
1296
1469
  return false;
@@ -1337,6 +1510,9 @@ var no_sequential_await_default = import_utils11.ESLintUtils.RuleCreator(
1337
1510
  return null;
1338
1511
  }
1339
1512
  function checkLoop(node) {
1513
+ if ((node.type === "WhileStatement" || node.type === "DoWhileStatement") && testStateIsAssignedInBody(node.test, node.body)) {
1514
+ return;
1515
+ }
1340
1516
  const awaits = [];
1341
1517
  let earlyExit = false;
1342
1518
  for (const part of loopParts(node)) {
@@ -1759,7 +1935,7 @@ var unwrap = (node) => {
1759
1935
  }
1760
1936
  return current ?? null;
1761
1937
  };
1762
- var isJsonCall = (node) => {
1938
+ var isRawPayloadSource = (node) => {
1763
1939
  let current = unwrap(node);
1764
1940
  if (current === null) return false;
1765
1941
  if (current.type === import_utils16.AST_NODE_TYPES.AwaitExpression) {
@@ -1773,7 +1949,14 @@ var isJsonCall = (node) => {
1773
1949
  return false;
1774
1950
  }
1775
1951
  const property = unwrap(callee.property);
1776
- return property !== null && property.type === import_utils16.AST_NODE_TYPES.Identifier && property.name === "json";
1952
+ if (property === null || property.type !== import_utils16.AST_NODE_TYPES.Identifier) {
1953
+ return false;
1954
+ }
1955
+ if (property.name === "json") {
1956
+ return true;
1957
+ }
1958
+ const object = unwrap(callee.object);
1959
+ return property.name === "parse" && object !== null && object.type === import_utils16.AST_NODE_TYPES.Identifier && object.name === "JSON";
1777
1960
  };
1778
1961
  var findVariable2 = (scope, name) => {
1779
1962
  let current = scope;
@@ -1823,18 +2006,18 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
1823
2006
  meta: {
1824
2007
  type: "problem",
1825
2008
  docs: {
1826
- description: "Require Zod (or similar) schema validation on `response.json()` before property access."
2009
+ description: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access."
1827
2010
  },
1828
2011
  schema: [],
1829
2012
  messages: {
1830
- unparsedJsonAccess: "Property access on the result of `response.json()` without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
2013
+ unparsedJsonAccess: "Property access on an unvalidated payload (`response.json()` / `JSON.parse()`) without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
1831
2014
  }
1832
2015
  },
1833
2016
  defaultOptions: [],
1834
2017
  create(context) {
1835
2018
  const unvalidatedVariables = /* @__PURE__ */ new Set();
1836
2019
  const trackInitializer = (declarator) => {
1837
- if (!isJsonCall(declarator.init)) return;
2020
+ if (!isRawPayloadSource(declarator.init)) return;
1838
2021
  const declaredVars = context.sourceCode.getDeclaredVariables(declarator);
1839
2022
  const variable = declaredVars[0];
1840
2023
  if (variable !== void 0) {
@@ -1849,7 +2032,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
1849
2032
  return;
1850
2033
  }
1851
2034
  if (node.id.type === import_utils16.AST_NODE_TYPES.ObjectPattern || node.id.type === import_utils16.AST_NODE_TYPES.ArrayPattern) {
1852
- if (isJsonCall(node.init)) {
2035
+ if (isRawPayloadSource(node.init)) {
1853
2036
  context.report({ node: node.id, messageId: "unparsedJsonAccess" });
1854
2037
  return;
1855
2038
  }
@@ -1863,7 +2046,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
1863
2046
  if (node.left.type === import_utils16.AST_NODE_TYPES.Identifier) {
1864
2047
  const variable = findVariable2(scope, node.left.name);
1865
2048
  if (variable === null) return;
1866
- if (isJsonCall(node.right)) {
2049
+ if (isRawPayloadSource(node.right)) {
1867
2050
  unvalidatedVariables.add(variable);
1868
2051
  } else {
1869
2052
  unvalidatedVariables.delete(variable);
@@ -1871,7 +2054,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
1871
2054
  return;
1872
2055
  }
1873
2056
  if (node.left.type === import_utils16.AST_NODE_TYPES.ObjectPattern || node.left.type === import_utils16.AST_NODE_TYPES.ArrayPattern) {
1874
- if (isJsonCall(node.right)) {
2057
+ if (isRawPayloadSource(node.right)) {
1875
2058
  context.report({
1876
2059
  node: node.left,
1877
2060
  messageId: "unparsedJsonAccess"
@@ -1905,7 +2088,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
1905
2088
  MemberExpression(node) {
1906
2089
  const scope = context.sourceCode.getScope(node);
1907
2090
  const obj = unwrap(node.object);
1908
- if (isJsonCall(obj)) {
2091
+ if (isRawPayloadSource(obj)) {
1909
2092
  const parent = node.parent;
1910
2093
  if (parent.type === import_utils16.AST_NODE_TYPES.CallExpression && parent.callee === node && node.property.type === import_utils16.AST_NODE_TYPES.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
1911
2094
  return;
@@ -2416,7 +2599,13 @@ var require_assert_never_default = import_utils20.ESLintUtils.RuleCreator(
2416
2599
 
2417
2600
  // src/rules/require-zod-form-validation.ts
2418
2601
  var import_utils21 = require("@typescript-eslint/utils");
2602
+
2603
+ // src/rules/_zod.ts
2604
+ var ZOD_PREFIX_RE = /^Z[A-Z]/;
2605
+ var ZOD_SUFFIX_RE = /Schema$/;
2419
2606
  var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
2607
+
2608
+ // src/rules/require-zod-form-validation.ts
2420
2609
  var looksLikeZodSchema = (node) => {
2421
2610
  let current = node;
2422
2611
  while (true) {
@@ -2494,14 +2683,38 @@ var require_zod_form_validation_default = import_utils21.ESLintUtils.RuleCreator
2494
2683
  }
2495
2684
  return isFormSourceIdentifier(callee.object);
2496
2685
  };
2686
+ const hasZodParseAncestor = (node) => {
2687
+ let parent = node.parent;
2688
+ while (parent !== null && parent !== void 0) {
2689
+ if (isZodParseCall(parent)) return true;
2690
+ parent = parent.parent;
2691
+ }
2692
+ return false;
2693
+ };
2694
+ const isInstanceofNarrowing = (node) => {
2695
+ const parent = node.parent;
2696
+ return parent !== null && parent !== void 0 && parent.type === import_utils21.AST_NODE_TYPES.BinaryExpression && parent.operator === "instanceof" && parent.left === node;
2697
+ };
2698
+ const boundDeclarator = (node) => {
2699
+ const parent = node.parent;
2700
+ if (parent.type === import_utils21.AST_NODE_TYPES.VariableDeclarator && parent.init === node && parent.id.type === import_utils21.AST_NODE_TYPES.Identifier) {
2701
+ return parent;
2702
+ }
2703
+ return null;
2704
+ };
2705
+ const bindingIsValidated = (declarator) => {
2706
+ const variable = context.sourceCode.getDeclaredVariables(declarator)[0];
2707
+ if (variable === void 0) return false;
2708
+ return variable.references.some(
2709
+ (ref) => hasZodParseAncestor(ref.identifier) || isInstanceofNarrowing(ref.identifier)
2710
+ );
2711
+ };
2497
2712
  return {
2498
2713
  CallExpression(node) {
2499
2714
  if (!isFormDataGetCall(node)) return;
2500
- let parent = node.parent;
2501
- while (parent !== null && parent !== void 0) {
2502
- if (isZodParseCall(parent)) return;
2503
- parent = parent.parent;
2504
- }
2715
+ if (hasZodParseAncestor(node) || isInstanceofNarrowing(node)) return;
2716
+ const declarator = boundDeclarator(node);
2717
+ if (declarator !== null && bindingIsValidated(declarator)) return;
2505
2718
  context.report({
2506
2719
  node,
2507
2720
  messageId: "missingZodValidation"
@@ -2513,6 +2726,11 @@ var require_zod_form_validation_default = import_utils21.ESLintUtils.RuleCreator
2513
2726
 
2514
2727
  // src/rules/zod-naming-convention.ts
2515
2728
  var import_utils22 = require("@typescript-eslint/utils");
2729
+ var CONVENTIONS = {
2730
+ prefix: { test: ZOD_PREFIX_RE, messageId: "zPrefix" },
2731
+ suffix: { test: ZOD_SUFFIX_RE, messageId: "schemaSuffix" },
2732
+ either: { test: ZOD_SCHEMA_NAME_RE, messageId: "zodSchemaName" }
2733
+ };
2516
2734
  var calleeChainStartsWithZ = (node) => {
2517
2735
  let current = node;
2518
2736
  while (current.type === import_utils22.AST_NODE_TYPES.MemberExpression) {
@@ -2535,15 +2753,29 @@ var zod_naming_convention_default = import_utils22.ESLintUtils.RuleCreator(
2535
2753
  meta: {
2536
2754
  type: "suggestion",
2537
2755
  docs: {
2538
- description: "Enforce Zod schemas to be named with a Z prefix (e.g. `ZUser = z.object({...})`)."
2756
+ description: "Enforce a consistent Zod schema naming convention \u2014 a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default."
2539
2757
  },
2540
- schema: [],
2758
+ schema: [
2759
+ {
2760
+ type: "object",
2761
+ additionalProperties: false,
2762
+ properties: {
2763
+ convention: {
2764
+ type: "string",
2765
+ enum: ["prefix", "suffix", "either"]
2766
+ }
2767
+ }
2768
+ }
2769
+ ],
2541
2770
  messages: {
2542
- zPrefix: "Zod schema names should start with Z"
2771
+ zPrefix: "Zod schema names should start with Z (e.g. `ZUser`)",
2772
+ schemaSuffix: "Zod schema names should end with Schema (e.g. `userSchema`)",
2773
+ zodSchemaName: "Zod schema names should start with Z (`ZUser`) or end with Schema (`userSchema`)"
2543
2774
  }
2544
2775
  },
2545
- defaultOptions: [],
2546
- create(context) {
2776
+ defaultOptions: [{}],
2777
+ create(context, [optionsArg]) {
2778
+ const { test, messageId } = CONVENTIONS[optionsArg?.convention ?? "either"];
2547
2779
  return {
2548
2780
  VariableDeclarator(node) {
2549
2781
  const init = node.init;
@@ -2553,11 +2785,10 @@ var zod_naming_convention_default = import_utils22.ESLintUtils.RuleCreator(
2553
2785
  if (callee.type !== import_utils22.AST_NODE_TYPES.MemberExpression) return;
2554
2786
  if (!calleeChainStartsWithZ(callee)) return;
2555
2787
  if (node.id.type !== import_utils22.AST_NODE_TYPES.Identifier) return;
2556
- const variableName = node.id.name;
2557
- if (variableName.startsWith("Z")) return;
2788
+ if (test.test(node.id.name)) return;
2558
2789
  context.report({
2559
2790
  node: node.id,
2560
- messageId: "zPrefix"
2791
+ messageId
2561
2792
  });
2562
2793
  }
2563
2794
  };
@@ -2778,10 +3009,14 @@ var no_cors_wildcard_with_credentials_default = import_utils23.ESLintUtils.RuleC
2778
3009
  var import_utils24 = require("@typescript-eslint/utils");
2779
3010
 
2780
3011
  // src/rules/_paths.ts
2781
- var TEST_FILE_RE = /(\.(test|spec)\.)|([\\/]__tests__[\\/])/;
2782
3012
  var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
2783
3013
  function isTestFile(filename) {
2784
- return TEST_FILE_RE.test(filename);
3014
+ const normalized = filename.replaceAll("\\", "/");
3015
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
3016
+ if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(base)) {
3017
+ return true;
3018
+ }
3019
+ return /(^|\/)(tests?|__tests__|__mocks__|fixtures)\//.test(normalized);
2785
3020
  }
2786
3021
  function isScriptFile(filename) {
2787
3022
  return SCRIPT_FILE_RE.test(filename);
@@ -2943,7 +3178,7 @@ var require_fetch_timeout_default = import_utils25.ESLintUtils.RuleCreator(
2943
3178
  const variable = import_utils25.ASTUtils.findVariable(scope, identifier.name);
2944
3179
  return variable === null || variable.defs.length === 0;
2945
3180
  }
2946
- function isGlobalFetchCall(callee) {
3181
+ function isGlobalFetchCall2(callee) {
2947
3182
  if (callee.type === import_utils25.AST_NODE_TYPES.Identifier) {
2948
3183
  return callee.name === "fetch" && resolvesToGlobal(callee);
2949
3184
  }
@@ -2951,7 +3186,7 @@ var require_fetch_timeout_default = import_utils25.ESLintUtils.RuleCreator(
2951
3186
  }
2952
3187
  return {
2953
3188
  CallExpression(node) {
2954
- if (!isGlobalFetchCall(node.callee)) {
3189
+ if (!isGlobalFetchCall2(node.callee)) {
2955
3190
  return;
2956
3191
  }
2957
3192
  const [first, init] = node.arguments;
@@ -3207,9 +3442,9 @@ function subtreeMatches(stmt, predicate) {
3207
3442
  visit(stmt);
3208
3443
  return found;
3209
3444
  }
3210
- var hasAwait = (stmt) => subtreeMatches(stmt, (n) => n.type === import_utils27.AST_NODE_TYPES.AwaitExpression);
3211
- var hasThrowingCallOrNew = (stmt) => subtreeMatches(
3212
- stmt,
3445
+ var hasAwait = (node) => subtreeMatches(node, (n) => n.type === import_utils27.AST_NODE_TYPES.AwaitExpression);
3446
+ var hasThrowingCallOrNew = (node) => subtreeMatches(
3447
+ node,
3213
3448
  (n) => n.type === import_utils27.AST_NODE_TYPES.CallExpression && !isPureCall(n) || n.type === import_utils27.AST_NODE_TYPES.NewExpression && !isPureNew(n)
3214
3449
  );
3215
3450
  function unwrap2(expr) {
@@ -3219,13 +3454,22 @@ function unwrap2(expr) {
3219
3454
  }
3220
3455
  return current;
3221
3456
  }
3457
+ function isBareCallStatement(stmt) {
3458
+ return stmt.type === import_utils27.AST_NODE_TYPES.ExpressionStatement && unwrap2(stmt.expression).type === import_utils27.AST_NODE_TYPES.CallExpression;
3459
+ }
3222
3460
  function canThrow(stmt) {
3223
3461
  if (hasAwait(stmt)) {
3224
3462
  return true;
3225
3463
  }
3226
- if (stmt.type === import_utils27.AST_NODE_TYPES.ExpressionStatement && unwrap2(stmt.expression).type === import_utils27.AST_NODE_TYPES.CallExpression) {
3464
+ if (isBareCallStatement(stmt)) {
3227
3465
  return false;
3228
3466
  }
3467
+ if (stmt.type === import_utils27.AST_NODE_TYPES.BlockStatement) {
3468
+ return stmt.body.some(canThrow);
3469
+ }
3470
+ if (stmt.type === import_utils27.AST_NODE_TYPES.IfStatement) {
3471
+ return hasThrowingCallOrNew(stmt.test) || canThrow(stmt.consequent) || stmt.alternate !== null && canThrow(stmt.alternate);
3472
+ }
3229
3473
  return hasThrowingCallOrNew(stmt);
3230
3474
  }
3231
3475
  function handlerRethrows(handler) {
@@ -3278,29 +3522,8 @@ var no_fat_try_blocks_default = import_utils27.ESLintUtils.RuleCreator(
3278
3522
 
3279
3523
  // src/rules/no-secret-in-log.ts
3280
3524
  var import_utils28 = require("@typescript-eslint/utils");
3281
- var LOG_METHODS2 = /* @__PURE__ */ new Set([
3282
- "debug",
3283
- "info",
3284
- "warn",
3285
- "warning",
3286
- "error",
3287
- "exception",
3288
- "critical",
3289
- "trace",
3290
- "log",
3291
- "fatal",
3292
- "success"
3293
- ]);
3294
- var LOGGER_NAMES2 = /* @__PURE__ */ new Set([
3295
- "logger",
3296
- "log",
3297
- "logging",
3298
- "loguru",
3299
- "console",
3300
- "_logger",
3301
- "_log"
3302
- ]);
3303
- var LOGGER_FACTORIES = /* @__PURE__ */ new Set(["getlogger", "get_logger"]);
3525
+
3526
+ // src/rules/_secret_names.ts
3304
3527
  var SECRET_WORDS = /* @__PURE__ */ new Set([
3305
3528
  "token",
3306
3529
  "secret",
@@ -3316,7 +3539,8 @@ var SECRET_WORDS = /* @__PURE__ */ new Set([
3316
3539
  "hmac",
3317
3540
  "digest",
3318
3541
  "hash",
3319
- "apikey"
3542
+ "apikey",
3543
+ "bearer"
3320
3544
  ]);
3321
3545
  var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
3322
3546
  "count",
@@ -3339,53 +3563,44 @@ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
3339
3563
  "valid",
3340
3564
  "invalid",
3341
3565
  "exists",
3566
+ "type",
3567
+ "types"
3568
+ ]);
3569
+ var DESCRIPTOR_WORDS = /* @__PURE__ */ new Set([
3342
3570
  "type",
3343
3571
  "types",
3344
3572
  "name",
3345
3573
  "names",
3346
- "label",
3347
- "labels",
3348
- "title",
3349
- "expiry",
3350
- "expiration",
3351
- "expires",
3352
- "ttl",
3353
- "version",
3354
- "versions",
3355
- "policy",
3356
- "rotation",
3357
- "arn",
3358
- "path",
3359
- "paths",
3360
- "issuer",
3361
- "audience",
3362
- "strength",
3363
- "manager",
3364
- "service",
3365
- "services",
3366
- "repository",
3367
- "provider",
3368
- "providers",
3369
- "store",
3370
- "factory",
3371
- "handler",
3372
- "controller",
3373
- "bucket",
3374
- "url",
3375
- "uri",
3376
- "endpoint",
3377
- "endpoints",
3378
- "scope",
3379
- "scopes",
3380
- "event",
3381
- "events",
3382
- "format",
3383
- "at",
3384
- "len",
3385
- "length"
3574
+ "id",
3575
+ "ids",
3576
+ "kind",
3577
+ "kinds"
3578
+ ]);
3579
+ var CATEGORY_WORDS = /* @__PURE__ */ new Set(["type", "types", "kind", "kinds"]);
3580
+ var FLAG_PREFIXES = /* @__PURE__ */ new Set([
3581
+ "is",
3582
+ "has",
3583
+ "was",
3584
+ "are",
3585
+ "can",
3586
+ "should"
3587
+ ]);
3588
+ var AUTH_WORDS = /* @__PURE__ */ new Set([
3589
+ "token",
3590
+ "secret",
3591
+ "secrets",
3592
+ "password",
3593
+ "passwd",
3594
+ "passwords",
3595
+ "jwt",
3596
+ "credential",
3597
+ "credentials",
3598
+ "authorization",
3599
+ "signature",
3600
+ "hmac",
3601
+ "apikey",
3602
+ "bearer"
3386
3603
  ]);
3387
- var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
3388
- var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
3389
3604
  var CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
3390
3605
  var SEGMENT_RE = /[^A-Za-z0-9]+/;
3391
3606
  function tokenize(identifier) {
@@ -3401,6 +3616,14 @@ function tokenize(identifier) {
3401
3616
  }
3402
3617
  return tokens;
3403
3618
  }
3619
+ function leadingWord(identifier) {
3620
+ for (const segment of identifier.split(SEGMENT_RE)) {
3621
+ if (segment) {
3622
+ return (segment.match(CAMEL_RE) ?? [segment])[0]?.toLowerCase();
3623
+ }
3624
+ }
3625
+ return void 0;
3626
+ }
3404
3627
  function hasApiKey(tokens) {
3405
3628
  for (let i = 0; i + 1 < tokens.length; i++) {
3406
3629
  if (tokens[i] === "api" && tokens[i + 1] === "key") {
@@ -3409,10 +3632,10 @@ function hasApiKey(tokens) {
3409
3632
  }
3410
3633
  return false;
3411
3634
  }
3412
- function isSecretName(identifier) {
3635
+ function isSecretName(identifier, innocuous = INNOCUOUS_WORDS) {
3413
3636
  const tokens = tokenize(identifier);
3414
3637
  const last = tokens.at(-1);
3415
- if (last !== void 0 && INNOCUOUS_WORDS.has(last)) {
3638
+ if (last !== void 0 && innocuous.has(last)) {
3416
3639
  return false;
3417
3640
  }
3418
3641
  if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
@@ -3420,6 +3643,76 @@ function isSecretName(identifier) {
3420
3643
  }
3421
3644
  return hasApiKey(tokens);
3422
3645
  }
3646
+ function isAuthSecretName(identifier) {
3647
+ if (!isSecretName(identifier)) {
3648
+ return false;
3649
+ }
3650
+ const tokens = tokenize(identifier);
3651
+ const first = leadingWord(identifier);
3652
+ if (first !== void 0 && FLAG_PREFIXES.has(first)) {
3653
+ return false;
3654
+ }
3655
+ const last = tokens.at(-1);
3656
+ if (last !== void 0 && DESCRIPTOR_WORDS.has(last)) {
3657
+ return false;
3658
+ }
3659
+ if (tokens.some((tok) => CATEGORY_WORDS.has(tok))) {
3660
+ return false;
3661
+ }
3662
+ if (tokens.some((tok) => AUTH_WORDS.has(tok))) {
3663
+ return true;
3664
+ }
3665
+ return hasApiKey(tokens);
3666
+ }
3667
+
3668
+ // src/rules/no-secret-in-log.ts
3669
+ var LOG_INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
3670
+ ...INNOCUOUS_WORDS,
3671
+ "name",
3672
+ "names",
3673
+ "label",
3674
+ "labels",
3675
+ "title",
3676
+ "expiry",
3677
+ "expiration",
3678
+ "expires",
3679
+ "ttl",
3680
+ "version",
3681
+ "versions",
3682
+ "policy",
3683
+ "rotation",
3684
+ "arn",
3685
+ "path",
3686
+ "paths",
3687
+ "issuer",
3688
+ "audience",
3689
+ "strength",
3690
+ "manager",
3691
+ "service",
3692
+ "services",
3693
+ "repository",
3694
+ "provider",
3695
+ "providers",
3696
+ "store",
3697
+ "factory",
3698
+ "handler",
3699
+ "controller",
3700
+ "bucket",
3701
+ "url",
3702
+ "uri",
3703
+ "endpoint",
3704
+ "endpoints",
3705
+ "scope",
3706
+ "scopes",
3707
+ "event",
3708
+ "events",
3709
+ "format",
3710
+ "at",
3711
+ "len",
3712
+ "length"
3713
+ ]);
3714
+ var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
3715
+ var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
3423
3716
  function isSecretKeyword(name) {
3424
3717
  if (REDACTION_RE.test(name)) {
3425
3718
  return false;
@@ -3427,35 +3720,7 @@ function isSecretKeyword(name) {
3427
3720
  if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
3428
3721
  return false;
3429
3722
  }
3430
- return isSecretName(name);
3431
- }
3432
- function isLoggerExpr(expr) {
3433
- switch (expr.type) {
3434
- case "Identifier":
3435
- return LOGGER_NAMES2.has(expr.name.toLowerCase());
3436
- case "MemberExpression": {
3437
- const { property, object } = expr;
3438
- if (!expr.computed && property.type === "Identifier") {
3439
- const lowered = property.name.toLowerCase();
3440
- if (LOGGER_NAMES2.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
3441
- return true;
3442
- }
3443
- }
3444
- return isLoggerExpr(object);
3445
- }
3446
- case "CallExpression": {
3447
- const callee = expr.callee;
3448
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
3449
- return true;
3450
- }
3451
- if (callee.type !== "Super") {
3452
- return isLoggerExpr(callee);
3453
- }
3454
- return false;
3455
- }
3456
- default:
3457
- return false;
3458
- }
3723
+ return isSecretName(name, LOG_INNOCUOUS_WORDS);
3459
3724
  }
3460
3725
  function isRawSecretValue(prop) {
3461
3726
  if (prop.shorthand) {
@@ -3484,20 +3749,23 @@ var no_secret_in_log_default = import_utils28.ESLintUtils.RuleCreator(
3484
3749
  docs: {
3485
3750
  description: "Disallow passing a secret-named value to a logging call; it leaks to log sinks. Redact or omit it."
3486
3751
  },
3487
- schema: [],
3752
+ schema: [
3753
+ {
3754
+ type: "object",
3755
+ additionalProperties: false,
3756
+ properties: { ...LOGGING_OPTION_PROPERTIES }
3757
+ }
3758
+ ],
3488
3759
  messages: {
3489
3760
  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."
3490
3761
  }
3491
3762
  },
3492
- defaultOptions: [],
3493
- create(context) {
3763
+ defaultOptions: [{}],
3764
+ create(context, [loggingOptions]) {
3765
+ const matcher = createLogMatcher(loggingOptions);
3494
3766
  return {
3495
3767
  CallExpression(node) {
3496
- const callee = node.callee;
3497
- if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !LOG_METHODS2.has(callee.property.name)) {
3498
- return;
3499
- }
3500
- if (!isLoggerExpr(callee.object)) {
3768
+ if (!matcher.isLoggingCall(node)) {
3501
3769
  return;
3502
3770
  }
3503
3771
  for (const arg of node.arguments) {
@@ -3700,19 +3968,33 @@ var prefer_string_literal_union_default = import_utils31.ESLintUtils.RuleCreator
3700
3968
  docs: {
3701
3969
  description: "Flag raw `string` choice fields and string-literal comparison clusters; prefer a string-literal union type."
3702
3970
  },
3703
- schema: [],
3971
+ schema: [
3972
+ {
3973
+ type: "object",
3974
+ additionalProperties: false,
3975
+ properties: {
3976
+ ignoreFields: {
3977
+ type: "array",
3978
+ items: { type: "string" }
3979
+ }
3980
+ }
3981
+ }
3982
+ ],
3704
3983
  messages: {
3705
3984
  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.',
3706
3985
  comparisonCluster: '`{{key}}` is compared against a closed set of string literals \u2014 define a string-literal union type (e.g. `type X = "a" | "b"`).'
3707
3986
  }
3708
3987
  },
3709
- defaultOptions: [],
3710
- create(context) {
3988
+ defaultOptions: [{}],
3989
+ create(context, [optionsArg]) {
3711
3990
  const filename = context.filename;
3712
3991
  const sourceText = context.sourceCode.getText();
3713
3992
  if (isIgnoredFile(filename, sourceText)) {
3714
3993
  return {};
3715
3994
  }
3995
+ const ignoredFields = new Set(
3996
+ (optionsArg?.ignoreFields ?? []).map((name) => name.toLowerCase())
3997
+ );
3716
3998
  let services;
3717
3999
  try {
3718
4000
  services = import_utils31.ESLintUtils.getParserServices(context);
@@ -3760,18 +4042,46 @@ var prefer_string_literal_union_default = import_utils31.ESLintUtils.RuleCreator
3760
4042
  function operandIsFlaggable(node) {
3761
4043
  return operandIsRawString(node) && !originIsExternal(services?.esTreeNodeToTSNodeMap.get(node), 0);
3762
4044
  }
3763
- function pushScope() {
3764
- scopeStack.push({ clusters: /* @__PURE__ */ new Map() });
4045
+ function declaredReturnLiterals(fn) {
4046
+ const annotation = fn.returnType?.typeAnnotation;
4047
+ if (annotation === void 0 || services === null) {
4048
+ return null;
4049
+ }
4050
+ const tsNode = services.esTreeNodeToTSNodeMap.get(annotation);
4051
+ if (!ts.isTypeNode(tsNode)) {
4052
+ return null;
4053
+ }
4054
+ const checker = services.program.getTypeChecker();
4055
+ const declared = checker.getTypeFromTypeNode(tsNode);
4056
+ const type = checker.getAwaitedType(declared) ?? declared;
4057
+ const literals = /* @__PURE__ */ new Set();
4058
+ for (const part of type.isUnion() ? type.types : [type]) {
4059
+ if (part.isStringLiteral()) {
4060
+ literals.add(part.value);
4061
+ }
4062
+ }
4063
+ return literals.size >= MIN_CLUSTER_SIZE ? literals : null;
4064
+ }
4065
+ function pushScope(node) {
4066
+ scopeStack.push({ clusters: /* @__PURE__ */ new Map(), fn: node });
3765
4067
  }
3766
4068
  function popScope() {
3767
4069
  const scope = scopeStack.pop();
3768
4070
  if (scope === void 0) {
3769
4071
  return;
3770
4072
  }
3771
- for (const entry of scope.clusters.values()) {
3772
- if (entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE) {
3773
- validClusters.push(entry.node);
4073
+ const candidates = [...scope.clusters.values()].filter(
4074
+ (entry) => entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE
4075
+ );
4076
+ if (candidates.length === 0) {
4077
+ return;
4078
+ }
4079
+ const returnLiterals = scope.fn === null ? null : declaredReturnLiterals(scope.fn);
4080
+ for (const entry of candidates) {
4081
+ if (returnLiterals !== null && [...entry.literals].every((lit) => returnLiterals.has(lit))) {
4082
+ continue;
3774
4083
  }
4084
+ validClusters.push(entry.node);
3775
4085
  }
3776
4086
  }
3777
4087
  function accumulate(key, literals, node) {
@@ -3803,7 +4113,7 @@ var prefer_string_literal_union_default = import_utils31.ESLintUtils.RuleCreator
3803
4113
  return;
3804
4114
  }
3805
4115
  const name = keyName(key);
3806
- if (name === null || !isChoiceLikeName(name)) {
4116
+ if (name === null || !isChoiceLikeName(name) || ignoredFields.has(name.toLowerCase())) {
3807
4117
  return;
3808
4118
  }
3809
4119
  bareChoiceProps.push({ name, container, node });
@@ -3922,7 +4232,7 @@ var ACRONYM_OVERRIDES = [
3922
4232
  [/gRPC/g, "Grpc"]
3923
4233
  ];
3924
4234
  var CAMEL_BOUNDARY_RE = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g;
3925
- var TEST_FILE_RE2 = /\.(test|spec)\.[cm]?[jt]sx?$/i;
4235
+ var TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
3926
4236
  var SCRIPT_EXT_RE = /\.[cm]?[jt]sx?$/i;
3927
4237
  var basename = (filename) => filename.split(/[/\\]/).pop() ?? filename;
3928
4238
  var stemOf = (base) => base.replace(SCRIPT_EXT_RE, "");
@@ -4022,7 +4332,7 @@ var single_public_export_default = import_utils32.ESLintUtils.RuleCreator(
4022
4332
  create(context) {
4023
4333
  const base = basename(context.filename);
4024
4334
  if (base.endsWith(".d.ts")) return {};
4025
- if (TEST_FILE_RE2.test(base)) return {};
4335
+ if (TEST_FILE_RE.test(base)) return {};
4026
4336
  const stem = stemOf(base);
4027
4337
  if (!JUNK_DRAWER_STEMS.has(stem.toLowerCase())) return {};
4028
4338
  return {
@@ -4043,6 +4353,975 @@ var single_public_export_default = import_utils32.ESLintUtils.RuleCreator(
4043
4353
  }
4044
4354
  });
4045
4355
 
4356
+ // src/rules/no-offset-pagination.ts
4357
+ var import_utils34 = require("@typescript-eslint/utils");
4358
+
4359
+ // src/rules/_sql.ts
4360
+ var import_utils33 = require("@typescript-eslint/utils");
4361
+ function stripSqlNoise(text) {
4362
+ const out = [...text];
4363
+ const n = text.length;
4364
+ let i = 0;
4365
+ while (i < n) {
4366
+ const ch = text[i];
4367
+ if (ch === "'" || ch === '"') {
4368
+ out[i] = " ";
4369
+ i += 1;
4370
+ while (i < n) {
4371
+ const c = text[i];
4372
+ if (c === ch) {
4373
+ if (i + 1 < n && text[i + 1] === ch) {
4374
+ out[i] = " ";
4375
+ out[i + 1] = " ";
4376
+ i += 2;
4377
+ continue;
4378
+ }
4379
+ out[i] = " ";
4380
+ i += 1;
4381
+ break;
4382
+ }
4383
+ if (c !== "\n") {
4384
+ out[i] = " ";
4385
+ }
4386
+ i += 1;
4387
+ }
4388
+ continue;
4389
+ }
4390
+ if (ch === "-" && text[i + 1] === "-") {
4391
+ while (i < n && text[i] !== "\n") {
4392
+ out[i] = " ";
4393
+ i += 1;
4394
+ }
4395
+ continue;
4396
+ }
4397
+ if (ch === "/" && text[i + 1] === "*") {
4398
+ out[i] = " ";
4399
+ out[i + 1] = " ";
4400
+ i += 2;
4401
+ while (i < n && !(text[i] === "*" && text[i + 1] === "/")) {
4402
+ if (text[i] !== "\n") {
4403
+ out[i] = " ";
4404
+ }
4405
+ i += 1;
4406
+ }
4407
+ if (i < n) {
4408
+ out[i] = " ";
4409
+ out[i + 1] = " ";
4410
+ i += 2;
4411
+ }
4412
+ continue;
4413
+ }
4414
+ i += 1;
4415
+ }
4416
+ return out.join("");
4417
+ }
4418
+ var SUBSTITUTION_MARKER = "?";
4419
+ function sqlTextOf(node) {
4420
+ switch (node.type) {
4421
+ case import_utils33.AST_NODE_TYPES.Literal:
4422
+ return typeof node.value === "string" ? node.value : null;
4423
+ case import_utils33.AST_NODE_TYPES.TemplateLiteral:
4424
+ return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join(SUBSTITUTION_MARKER);
4425
+ case import_utils33.AST_NODE_TYPES.TaggedTemplateExpression:
4426
+ return sqlTextOf(node.quasi);
4427
+ case import_utils33.AST_NODE_TYPES.BinaryExpression: {
4428
+ if (node.operator !== "+") {
4429
+ return null;
4430
+ }
4431
+ const left = sqlTextOf(node.left);
4432
+ const right = sqlTextOf(node.right);
4433
+ return left !== null && right !== null ? left + right : null;
4434
+ }
4435
+ case import_utils33.AST_NODE_TYPES.ArrayExpression: {
4436
+ const parts = [];
4437
+ for (const element of node.elements) {
4438
+ if (element === null) {
4439
+ return null;
4440
+ }
4441
+ const part = sqlTextOf(element);
4442
+ if (part === null) {
4443
+ return null;
4444
+ }
4445
+ parts.push(part);
4446
+ }
4447
+ return parts.length > 0 ? parts.join(" ") : null;
4448
+ }
4449
+ default:
4450
+ return null;
4451
+ }
4452
+ }
4453
+ function isJoinedFragmentArray(node) {
4454
+ const parent = node.parent;
4455
+ return parent?.type === import_utils33.AST_NODE_TYPES.MemberExpression && parent.object === node && !parent.computed && parent.property.type === import_utils33.AST_NODE_TYPES.Identifier && parent.property.name === "join" && parent.parent?.type === import_utils33.AST_NODE_TYPES.CallExpression;
4456
+ }
4457
+ function markConsumed(node, consumed) {
4458
+ consumed.add(node);
4459
+ for (const key of Object.keys(node)) {
4460
+ if (key === "parent") {
4461
+ continue;
4462
+ }
4463
+ const value = node[key];
4464
+ for (const child of Array.isArray(value) ? value : [value]) {
4465
+ if (child !== null && typeof child === "object" && "type" in child) {
4466
+ markConsumed(child, consumed);
4467
+ }
4468
+ }
4469
+ }
4470
+ }
4471
+ function createSqlListener(handler) {
4472
+ const consumed = /* @__PURE__ */ new WeakSet();
4473
+ const visit = (node) => {
4474
+ if (consumed.has(node)) {
4475
+ return;
4476
+ }
4477
+ const text = sqlTextOf(node);
4478
+ if (text === null) {
4479
+ return;
4480
+ }
4481
+ markConsumed(node, consumed);
4482
+ handler(stripSqlNoise(text), node);
4483
+ };
4484
+ return {
4485
+ BinaryExpression: (node) => {
4486
+ visit(node);
4487
+ },
4488
+ ArrayExpression: (node) => {
4489
+ if (isJoinedFragmentArray(node)) {
4490
+ visit(node);
4491
+ }
4492
+ },
4493
+ TemplateLiteral: (node) => {
4494
+ visit(node);
4495
+ },
4496
+ Literal: (node) => {
4497
+ visit(node);
4498
+ }
4499
+ };
4500
+ }
4501
+
4502
+ // src/rules/no-offset-pagination.ts
4503
+ var OFFSET_PAGINATION = /\bOFFSET\s+(?:\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
4504
+ var OFFSET_GATE = /offset/i;
4505
+ var no_offset_pagination_default = import_utils34.ESLintUtils.RuleCreator(
4506
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4507
+ )({
4508
+ name: "no-offset-pagination",
4509
+ meta: {
4510
+ type: "problem",
4511
+ docs: {
4512
+ 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."
4513
+ },
4514
+ schema: [],
4515
+ messages: {
4516
+ 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 ?`."
4517
+ }
4518
+ },
4519
+ defaultOptions: [],
4520
+ create(context) {
4521
+ if (isTestFile(context.filename) || !OFFSET_GATE.test(context.sourceCode.text)) {
4522
+ return {};
4523
+ }
4524
+ return createSqlListener((sql, node) => {
4525
+ if (!OFFSET_PAGINATION.test(sql)) {
4526
+ return;
4527
+ }
4528
+ context.report({ node, messageId: "noOffsetPagination" });
4529
+ });
4530
+ }
4531
+ });
4532
+
4533
+ // src/rules/no-positional-tuple-return.ts
4534
+ var import_utils35 = require("@typescript-eslint/utils");
4535
+ var MIN_ELEMENTS = 2;
4536
+ var AWAITABLE_TYPES = /* @__PURE__ */ new Set(["Promise", "PromiseLike", "Awaited"]);
4537
+ function tupleReturnType(node) {
4538
+ if (node.type === import_utils35.AST_NODE_TYPES.TSTupleType) {
4539
+ return node;
4540
+ }
4541
+ if (node.type === import_utils35.AST_NODE_TYPES.TSTypeReference && node.typeName.type === import_utils35.AST_NODE_TYPES.Identifier && AWAITABLE_TYPES.has(node.typeName.name)) {
4542
+ const argument = node.typeArguments?.params[0];
4543
+ return argument === void 0 ? null : tupleReturnType(argument);
4544
+ }
4545
+ return null;
4546
+ }
4547
+ function normalizedText(sourceCode, node) {
4548
+ return sourceCode.getText(node).replaceAll(/\s+/g, " ").trim();
4549
+ }
4550
+ function isPermittedTuple(tuple, sourceCode) {
4551
+ const elements = tuple.elementTypes;
4552
+ if (elements.length < MIN_ELEMENTS) {
4553
+ return true;
4554
+ }
4555
+ if (elements.some((element) => element.type === import_utils35.AST_NODE_TYPES.TSRestType)) {
4556
+ return true;
4557
+ }
4558
+ if (elements.some((element) => element.type === import_utils35.AST_NODE_TYPES.TSNamedTupleMember)) {
4559
+ return true;
4560
+ }
4561
+ if (elements[0]?.type === import_utils35.AST_NODE_TYPES.TSLiteralType) {
4562
+ return true;
4563
+ }
4564
+ const texts = new Set(elements.map((element) => normalizedText(sourceCode, element)));
4565
+ return texts.size === 1;
4566
+ }
4567
+ function functionName(node) {
4568
+ if (node.type === import_utils35.AST_NODE_TYPES.FunctionDeclaration) {
4569
+ return node.id?.name ?? null;
4570
+ }
4571
+ const parent = node.parent;
4572
+ if (parent?.type === import_utils35.AST_NODE_TYPES.VariableDeclarator && parent.id.type === import_utils35.AST_NODE_TYPES.Identifier) {
4573
+ return parent.id.name;
4574
+ }
4575
+ if ((parent?.type === import_utils35.AST_NODE_TYPES.MethodDefinition || parent?.type === import_utils35.AST_NODE_TYPES.PropertyDefinition || parent?.type === import_utils35.AST_NODE_TYPES.Property) && parent.key.type === import_utils35.AST_NODE_TYPES.Identifier) {
4576
+ return parent.key.name;
4577
+ }
4578
+ return null;
4579
+ }
4580
+ function isExported(node) {
4581
+ for (let current = node; current != null; current = current.parent) {
4582
+ const parent = current.parent;
4583
+ if (parent?.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent?.type === import_utils35.AST_NODE_TYPES.ExportDefaultDeclaration) {
4584
+ return true;
4585
+ }
4586
+ }
4587
+ return false;
4588
+ }
4589
+ var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
4590
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4591
+ )({
4592
+ name: "no-positional-tuple-return",
4593
+ meta: {
4594
+ type: "suggestion",
4595
+ docs: {
4596
+ description: "Disallow returning a positional tuple of distinct fields from an exported function; return a named object so call sites cannot mismatch slots."
4597
+ },
4598
+ schema: [],
4599
+ messages: {
4600
+ 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."
4601
+ }
4602
+ },
4603
+ defaultOptions: [],
4604
+ create(context) {
4605
+ const check = (node) => {
4606
+ const annotation = node.returnType?.typeAnnotation;
4607
+ if (annotation === void 0) {
4608
+ return;
4609
+ }
4610
+ const tuple = tupleReturnType(annotation);
4611
+ if (tuple === null || isPermittedTuple(tuple, context.sourceCode)) {
4612
+ return;
4613
+ }
4614
+ const name = functionName(node);
4615
+ if (name === null || name.startsWith("_") || /^use[A-Z]/.test(name)) {
4616
+ return;
4617
+ }
4618
+ if (!isExported(node)) {
4619
+ return;
4620
+ }
4621
+ context.report({
4622
+ node: tuple,
4623
+ messageId: "noPositionalTupleReturn",
4624
+ data: { name, count: String(tuple.elementTypes.length) }
4625
+ });
4626
+ };
4627
+ return {
4628
+ FunctionDeclaration: check,
4629
+ FunctionExpression: check,
4630
+ ArrowFunctionExpression: check
4631
+ };
4632
+ }
4633
+ });
4634
+
4635
+ // src/rules/no-repeated-string-literal.ts
4636
+ var import_utils36 = require("@typescript-eslint/utils");
4637
+ var MIN_LENGTH = 40;
4638
+ var MIN_OCCURRENCES = 3;
4639
+ var MIN_DISTINCT_SCOPES = 2;
4640
+ var PREVIEW_LENGTH = 40;
4641
+ var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
4642
+ var IDENTIFIER_RE = /^[a-z_][a-z0-9_.]*$/;
4643
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
4644
+ import_utils36.AST_NODE_TYPES.FunctionDeclaration,
4645
+ import_utils36.AST_NODE_TYPES.FunctionExpression,
4646
+ import_utils36.AST_NODE_TYPES.ArrowFunctionExpression
4647
+ ]);
4648
+ function isStructured(value) {
4649
+ return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value);
4650
+ }
4651
+ function preview(value) {
4652
+ const oneLine = value.replaceAll("\n", " ").trim();
4653
+ return oneLine.length <= PREVIEW_LENGTH ? oneLine : `${oneLine.slice(0, PREVIEW_LENGTH)}...`;
4654
+ }
4655
+ function enclosingFunction(node) {
4656
+ for (let current = node.parent; current != null; current = current.parent) {
4657
+ if (FUNCTION_TYPES.has(current.type)) {
4658
+ return current;
4659
+ }
4660
+ }
4661
+ return null;
4662
+ }
4663
+ function isScaffolding(node) {
4664
+ const parent = node.parent;
4665
+ if (parent === void 0) {
4666
+ return true;
4667
+ }
4668
+ return parent.type === import_utils36.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils36.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils36.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils36.AST_NODE_TYPES.TSImportType || parent.type === import_utils36.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils36.AST_NODE_TYPES.TSLiteralType;
4669
+ }
4670
+ var no_repeated_string_literal_default = import_utils36.ESLintUtils.RuleCreator(
4671
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4672
+ )({
4673
+ name: "no-repeated-string-literal",
4674
+ meta: {
4675
+ type: "suggestion",
4676
+ docs: {
4677
+ description: "Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant."
4678
+ },
4679
+ schema: [],
4680
+ messages: {
4681
+ 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.'
4682
+ }
4683
+ },
4684
+ defaultOptions: [],
4685
+ create(context) {
4686
+ if (isTestFile(context.filename)) {
4687
+ return {};
4688
+ }
4689
+ const occurrences = /* @__PURE__ */ new Map();
4690
+ const scopes = /* @__PURE__ */ new WeakMap();
4691
+ const record = (value, node) => {
4692
+ if (value.length < MIN_LENGTH || !isStructured(value) || isScaffolding(node)) {
4693
+ return;
4694
+ }
4695
+ const existing = occurrences.get(value);
4696
+ if (existing === void 0) {
4697
+ occurrences.set(value, [node]);
4698
+ } else {
4699
+ existing.push(node);
4700
+ }
4701
+ scopes.set(node, enclosingFunction(node));
4702
+ };
4703
+ return {
4704
+ Literal(node) {
4705
+ if (typeof node.value === "string") {
4706
+ record(node.value, node);
4707
+ }
4708
+ },
4709
+ TemplateLiteral(node) {
4710
+ const [only] = node.quasis;
4711
+ if (node.expressions.length === 0 && only !== void 0) {
4712
+ record(only.value.cooked ?? only.value.raw, node);
4713
+ }
4714
+ },
4715
+ "Program:exit": () => {
4716
+ for (const [value, nodes] of occurrences) {
4717
+ if (nodes.length < MIN_OCCURRENCES) {
4718
+ continue;
4719
+ }
4720
+ const distinctScopes = new Set(
4721
+ nodes.map((node) => scopes.get(node)).filter((scope) => scope != null)
4722
+ );
4723
+ if (distinctScopes.size < MIN_DISTINCT_SCOPES) {
4724
+ continue;
4725
+ }
4726
+ const [first, ...repeats] = nodes;
4727
+ if (first === void 0) {
4728
+ continue;
4729
+ }
4730
+ for (const node of repeats) {
4731
+ context.report({
4732
+ node,
4733
+ messageId: "noRepeatedStringLiteral",
4734
+ data: { preview: preview(value), line: String(first.loc.start.line) }
4735
+ });
4736
+ }
4737
+ }
4738
+ }
4739
+ };
4740
+ }
4741
+ });
4742
+
4743
+ // src/rules/no-select-star.ts
4744
+ var import_utils37 = require("@typescript-eslint/utils");
4745
+ var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
4746
+ var SELECT_KEYWORD = /\bSELECT\b/gi;
4747
+ var FROM_KEYWORD = /^FROM\b/i;
4748
+ var EXISTS_BEFORE = /\bEXISTS\s*\(\s*$/i;
4749
+ var QUALIFIED_PREFIX = /\w\.$/;
4750
+ var SELECT_GATE = /select/i;
4751
+ function isProjectionStar(sql, pos) {
4752
+ if (QUALIFIED_PREFIX.test(sql.slice(0, pos))) {
4753
+ return true;
4754
+ }
4755
+ let before = pos - 1;
4756
+ while (before >= 0 && /\s/.test(sql[before] ?? "")) {
4757
+ before -= 1;
4758
+ }
4759
+ let after = pos + 1;
4760
+ while (after < sql.length && /\s/.test(sql[after] ?? "")) {
4761
+ after += 1;
4762
+ }
4763
+ const beforeChar = before >= 0 ? sql[before] ?? "" : "";
4764
+ const afterChar = after < sql.length ? sql[after] ?? "" : "";
4765
+ const terminates = afterChar === "" || afterChar === "," || afterChar === ")" || FROM_KEYWORD.test(sql.slice(after));
4766
+ if (!terminates) {
4767
+ return false;
4768
+ }
4769
+ return !(beforeChar === "(" && afterChar === ")");
4770
+ }
4771
+ function hasRealSelectStar(sql) {
4772
+ const selects = [...sql.matchAll(SELECT_KEYWORD)].map((m) => m.index);
4773
+ for (let pos = 0; pos < sql.length; pos++) {
4774
+ if (sql[pos] !== "*" || !isProjectionStar(sql, pos)) {
4775
+ continue;
4776
+ }
4777
+ const owning = selects.filter((start) => start < pos).at(-1);
4778
+ if (owning !== void 0 && !EXISTS_BEFORE.test(sql.slice(0, owning))) {
4779
+ return true;
4780
+ }
4781
+ }
4782
+ return false;
4783
+ }
4784
+ var no_select_star_default = import_utils37.ESLintUtils.RuleCreator(
4785
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4786
+ )({
4787
+ name: "no-select-star",
4788
+ meta: {
4789
+ type: "problem",
4790
+ docs: {
4791
+ description: "Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently."
4792
+ },
4793
+ schema: [],
4794
+ messages: {
4795
+ 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."
4796
+ }
4797
+ },
4798
+ defaultOptions: [],
4799
+ create(context) {
4800
+ if (isTestFile(context.filename) || !SELECT_GATE.test(context.sourceCode.text)) {
4801
+ return {};
4802
+ }
4803
+ return createSqlListener((sql, node) => {
4804
+ if (!QUERY_SHAPE.test(sql) || !hasRealSelectStar(sql)) {
4805
+ return;
4806
+ }
4807
+ context.report({ node, messageId: "noSelectStar" });
4808
+ });
4809
+ }
4810
+ });
4811
+
4812
+ // src/rules/no-sleep-in-test-body.ts
4813
+ var import_utils38 = require("@typescript-eslint/utils");
4814
+ var SLEEP_HELPERS = /* @__PURE__ */ new Set(["sleep", "delay", "wait", "pause"]);
4815
+ var TEST_CALLERS = /* @__PURE__ */ new Set([
4816
+ "it",
4817
+ "test",
4818
+ "beforeEach",
4819
+ "afterEach"
4820
+ ]);
4821
+ var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
4822
+ import_utils38.AST_NODE_TYPES.FunctionDeclaration,
4823
+ import_utils38.AST_NODE_TYPES.FunctionExpression,
4824
+ import_utils38.AST_NODE_TYPES.ArrowFunctionExpression
4825
+ ]);
4826
+ function isNonzeroNumericLiteral(node) {
4827
+ return node?.type === import_utils38.AST_NODE_TYPES.Literal && typeof node.value === "number" && node.value !== 0;
4828
+ }
4829
+ function isTimedSetTimeout(node) {
4830
+ return node.type === import_utils38.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils38.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
4831
+ }
4832
+ function isPromiseSleep(node) {
4833
+ if (node.callee.type !== import_utils38.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
4834
+ return false;
4835
+ }
4836
+ const executor = node.arguments[0];
4837
+ if (executor?.type !== import_utils38.AST_NODE_TYPES.ArrowFunctionExpression && executor?.type !== import_utils38.AST_NODE_TYPES.FunctionExpression) {
4838
+ return false;
4839
+ }
4840
+ const body = executor.body;
4841
+ if (body.type !== import_utils38.AST_NODE_TYPES.BlockStatement) {
4842
+ return isTimedSetTimeout(body);
4843
+ }
4844
+ return body.body.some(
4845
+ (stmt) => stmt.type === import_utils38.AST_NODE_TYPES.ExpressionStatement && isTimedSetTimeout(stmt.expression)
4846
+ );
4847
+ }
4848
+ function isHelperSleep(node) {
4849
+ return node.callee.type === import_utils38.AST_NODE_TYPES.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
4850
+ }
4851
+ function nearestEnclosingFunction(node) {
4852
+ for (let current = node.parent; current != null; current = current.parent) {
4853
+ if (!FUNCTION_TYPES2.has(current.type)) {
4854
+ continue;
4855
+ }
4856
+ const grandparent = current.parent;
4857
+ const isPromiseExecutor = grandparent?.type === import_utils38.AST_NODE_TYPES.NewExpression && isPromiseSleep(grandparent);
4858
+ if (!isPromiseExecutor) {
4859
+ return current;
4860
+ }
4861
+ }
4862
+ return null;
4863
+ }
4864
+ function testCallerName(callee) {
4865
+ if (callee.type === import_utils38.AST_NODE_TYPES.Identifier) {
4866
+ return callee.name;
4867
+ }
4868
+ if (callee.type === import_utils38.AST_NODE_TYPES.MemberExpression) {
4869
+ return testCallerName(callee.object);
4870
+ }
4871
+ if (callee.type === import_utils38.AST_NODE_TYPES.CallExpression) {
4872
+ return testCallerName(callee.callee);
4873
+ }
4874
+ if (callee.type === import_utils38.AST_NODE_TYPES.TaggedTemplateExpression) {
4875
+ return testCallerName(callee.tag);
4876
+ }
4877
+ return null;
4878
+ }
4879
+ function isTestBody(fn) {
4880
+ const call = fn.parent;
4881
+ if (call?.type !== import_utils38.AST_NODE_TYPES.CallExpression || !call.arguments.some((argument) => argument === fn)) {
4882
+ return false;
4883
+ }
4884
+ const name = testCallerName(call.callee);
4885
+ return name !== null && TEST_CALLERS.has(name);
4886
+ }
4887
+ var no_sleep_in_test_body_default = import_utils38.ESLintUtils.RuleCreator(
4888
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4889
+ )({
4890
+ name: "no-sleep-in-test-body",
4891
+ meta: {
4892
+ type: "problem",
4893
+ docs: {
4894
+ description: "Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers."
4895
+ },
4896
+ schema: [],
4897
+ messages: {
4898
+ 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)`."
4899
+ }
4900
+ },
4901
+ defaultOptions: [],
4902
+ create(context) {
4903
+ if (!isTestFile(context.filename)) {
4904
+ return {};
4905
+ }
4906
+ const report = (node) => {
4907
+ const enclosing = nearestEnclosingFunction(node);
4908
+ if (enclosing === null || !isTestBody(enclosing)) {
4909
+ return;
4910
+ }
4911
+ context.report({ node, messageId: "noSleepInTestBody" });
4912
+ };
4913
+ return {
4914
+ NewExpression(node) {
4915
+ if (isPromiseSleep(node)) {
4916
+ report(node);
4917
+ }
4918
+ },
4919
+ CallExpression(node) {
4920
+ if (isHelperSleep(node)) {
4921
+ report(node);
4922
+ }
4923
+ }
4924
+ };
4925
+ }
4926
+ });
4927
+
4928
+ // src/rules/prefer-constant-time-secret-compare.ts
4929
+ var import_utils39 = require("@typescript-eslint/utils");
4930
+ var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
4931
+ var SENTINEL_IDENTIFIERS = /* @__PURE__ */ new Set(["undefined", "NaN"]);
4932
+ var SENTINEL_WORDS = /(^|_)(SENTINEL|EMPTY|NONE|NULL|UNSET|MISSING|PLACEHOLDER|DUMMY|FAKE|EXAMPLE)(_|$)/;
4933
+ function isConstantReference(identifier) {
4934
+ if (isAuthSecretName(identifier) && !SENTINEL_WORDS.test(identifier)) return false;
4935
+ return identifier === identifier.toUpperCase() && /[A-Za-z]/.test(identifier);
4936
+ }
4937
+ function isExcludedOperand(node) {
4938
+ switch (node.type) {
4939
+ case import_utils39.AST_NODE_TYPES.Literal:
4940
+ return true;
4941
+ case import_utils39.AST_NODE_TYPES.TemplateLiteral:
4942
+ return node.expressions.length === 0;
4943
+ case import_utils39.AST_NODE_TYPES.Identifier:
4944
+ return SENTINEL_IDENTIFIERS.has(node.name) || isConstantReference(node.name);
4945
+ case import_utils39.AST_NODE_TYPES.MemberExpression:
4946
+ return !node.computed && node.property.type === import_utils39.AST_NODE_TYPES.Identifier && isConstantReference(node.property.name);
4947
+ default:
4948
+ return false;
4949
+ }
4950
+ }
4951
+ function operandName(node) {
4952
+ if (node.type === import_utils39.AST_NODE_TYPES.Identifier) {
4953
+ return node.name;
4954
+ }
4955
+ if (node.type === import_utils39.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils39.AST_NODE_TYPES.Identifier) {
4956
+ return node.property.name;
4957
+ }
4958
+ return null;
4959
+ }
4960
+ function isSecretOperand(node) {
4961
+ if (node.type === import_utils39.AST_NODE_TYPES.TemplateLiteral) {
4962
+ return node.expressions.some((expression) => isSecretOperand(expression));
4963
+ }
4964
+ const name = operandName(node);
4965
+ return name !== null && isAuthSecretName(name);
4966
+ }
4967
+ function secretNameOf(node) {
4968
+ if (node.type === import_utils39.AST_NODE_TYPES.TemplateLiteral) {
4969
+ for (const expression of node.expressions) {
4970
+ const nested = secretNameOf(expression);
4971
+ if (nested !== null) {
4972
+ return nested;
4973
+ }
4974
+ }
4975
+ return null;
4976
+ }
4977
+ return operandName(node);
4978
+ }
4979
+ var prefer_constant_time_secret_compare_default = import_utils39.ESLintUtils.RuleCreator(
4980
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4981
+ )({
4982
+ name: "prefer-constant-time-secret-compare",
4983
+ meta: {
4984
+ type: "problem",
4985
+ docs: {
4986
+ description: "Disallow `===`/`!==` on a secret-like value; short-circuiting comparison leaks the secret through timing. Use a constant-time compare."
4987
+ },
4988
+ schema: [],
4989
+ messages: {
4990
+ 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)."
4991
+ }
4992
+ },
4993
+ defaultOptions: [],
4994
+ create(context) {
4995
+ if (isTestFile(context.filename)) {
4996
+ return {};
4997
+ }
4998
+ return {
4999
+ BinaryExpression(node) {
5000
+ if (!EQUALITY_OPERATORS.has(node.operator)) {
5001
+ return;
5002
+ }
5003
+ const { left, right } = node;
5004
+ if (isExcludedOperand(left) || isExcludedOperand(right)) {
5005
+ return;
5006
+ }
5007
+ const secret = [left, right].find((operand) => isSecretOperand(operand));
5008
+ if (secret === void 0) {
5009
+ return;
5010
+ }
5011
+ context.report({
5012
+ node,
5013
+ messageId: "preferConstantTimeSecretCompare",
5014
+ data: { operator: node.operator, name: secretNameOf(secret) ?? "" }
5015
+ });
5016
+ }
5017
+ };
5018
+ }
5019
+ });
5020
+
5021
+ // src/rules/store-insert-requires-on-conflict.ts
5022
+ var import_utils40 = require("@typescript-eslint/utils");
5023
+ var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
5024
+ var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/i;
5025
+ var INSERT_GATE = /insert/i;
5026
+ var store_insert_requires_on_conflict_default = import_utils40.ESLintUtils.RuleCreator(
5027
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5028
+ )({
5029
+ name: "store-insert-requires-on-conflict",
5030
+ meta: {
5031
+ type: "problem",
5032
+ docs: {
5033
+ 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."
5034
+ },
5035
+ schema: [],
5036
+ messages: {
5037
+ 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`)."
5038
+ }
5039
+ },
5040
+ defaultOptions: [],
5041
+ create(context) {
5042
+ if (isTestFile(context.filename) || !INSERT_GATE.test(context.sourceCode.text)) {
5043
+ return {};
5044
+ }
5045
+ return createSqlListener((sql, node) => {
5046
+ if (!INSERT_WRITE.test(sql) || CONFLICT_HANDLED.test(sql)) {
5047
+ return;
5048
+ }
5049
+ context.report({ node, messageId: "storeInsertRequiresOnConflict" });
5050
+ });
5051
+ }
5052
+ });
5053
+
5054
+ // src/rules/no-dynamic-sql.ts
5055
+ var import_utils41 = require("@typescript-eslint/utils");
5056
+ var DEFAULT_METHODS = ["prepare", "exec", "query"];
5057
+ var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
5058
+ function isStaticFragment(expression) {
5059
+ if (expression.type === import_utils41.AST_NODE_TYPES.Identifier) {
5060
+ return CONSTANT_CASE_RE.test(expression.name);
5061
+ }
5062
+ if (expression.type === import_utils41.AST_NODE_TYPES.MemberExpression && !expression.computed && expression.property.type === import_utils41.AST_NODE_TYPES.Identifier) {
5063
+ return CONSTANT_CASE_RE.test(expression.property.name);
5064
+ }
5065
+ if (expression.type === import_utils41.AST_NODE_TYPES.Literal) {
5066
+ return typeof expression.value === "string";
5067
+ }
5068
+ return false;
5069
+ }
5070
+ function runtimeInterpolations(template) {
5071
+ return template.expressions.filter(
5072
+ (expression) => !isStaticFragment(expression)
5073
+ );
5074
+ }
5075
+ function concatOperands(node) {
5076
+ if (node.type === import_utils41.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
5077
+ return [...concatOperands(node.left), ...concatOperands(node.right)];
5078
+ }
5079
+ return [node];
5080
+ }
5081
+ function runtimeConcatOperands(node) {
5082
+ if (node.type !== import_utils41.AST_NODE_TYPES.BinaryExpression || node.operator !== "+") {
5083
+ return [];
5084
+ }
5085
+ const operands = concatOperands(node);
5086
+ const hasStringLiteral = operands.some(
5087
+ (operand) => operand.type === import_utils41.AST_NODE_TYPES.Literal && typeof operand.value === "string"
5088
+ );
5089
+ if (!hasStringLiteral) {
5090
+ return [];
5091
+ }
5092
+ return operands.filter(
5093
+ (operand) => operand.type !== import_utils41.AST_NODE_TYPES.Literal && !isStaticFragment(operand)
5094
+ );
5095
+ }
5096
+ function statementMethodName(node, methods) {
5097
+ const callee = node.callee;
5098
+ if (callee.type !== import_utils41.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils41.AST_NODE_TYPES.Identifier) {
5099
+ return null;
5100
+ }
5101
+ const name = callee.property.name;
5102
+ return methods.has(name) ? name : null;
5103
+ }
5104
+ var no_dynamic_sql_default = import_utils41.ESLintUtils.RuleCreator(
5105
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5106
+ )({
5107
+ name: "no-dynamic-sql",
5108
+ meta: {
5109
+ type: "problem",
5110
+ docs: {
5111
+ description: "Disallow interpolating or concatenating a runtime value into a SQL statement passed to `prepare`/`exec`/`query`; use a placeholder and bind the value."
5112
+ },
5113
+ schema: [
5114
+ {
5115
+ type: "object",
5116
+ properties: {
5117
+ methods: {
5118
+ type: "array",
5119
+ items: { type: "string" },
5120
+ description: "Statement-taking method names to inspect. Replaces the defaults."
5121
+ }
5122
+ },
5123
+ additionalProperties: false
5124
+ }
5125
+ ],
5126
+ messages: {
5127
+ 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."
5128
+ }
5129
+ },
5130
+ defaultOptions: [{}],
5131
+ create(context, [options]) {
5132
+ const methods = new Set(options?.methods ?? DEFAULT_METHODS);
5133
+ return {
5134
+ CallExpression(node) {
5135
+ const method = statementMethodName(node, methods);
5136
+ if (method === null) {
5137
+ return;
5138
+ }
5139
+ const statement = node.arguments[0];
5140
+ if (statement === void 0) {
5141
+ return;
5142
+ }
5143
+ const offenders = statement.type === import_utils41.AST_NODE_TYPES.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
5144
+ for (const offender of offenders) {
5145
+ context.report({
5146
+ node: offender,
5147
+ messageId: "dynamicSql",
5148
+ data: { method }
5149
+ });
5150
+ }
5151
+ }
5152
+ };
5153
+ }
5154
+ });
5155
+
5156
+ // src/rules/no-raw-fetch-outside-clients.ts
5157
+ var import_utils42 = require("@typescript-eslint/utils");
5158
+ var DEFAULT_ALLOW = [
5159
+ "[\\\\/]clients?[\\\\/]",
5160
+ "-client\\.[cm]?[jt]sx?$",
5161
+ "[\\\\/]http-client\\.[cm]?[jt]sx?$",
5162
+ "\\.test\\.",
5163
+ "\\.spec\\.",
5164
+ "[\\\\/]__tests__[\\\\/]",
5165
+ "[\\\\/]__mocks__[\\\\/]"
5166
+ ];
5167
+ var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
5168
+ "globalThis",
5169
+ "window",
5170
+ "self"
5171
+ ]);
5172
+ function isGlobalFetchCall(node) {
5173
+ const callee = node.callee;
5174
+ if (callee.type === "Identifier") {
5175
+ return callee.name === "fetch";
5176
+ }
5177
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "fetch" && callee.object.type === "Identifier") {
5178
+ return GLOBAL_RECEIVERS.has(callee.object.name);
5179
+ }
5180
+ return false;
5181
+ }
5182
+ function compile(patterns) {
5183
+ const compiled = [];
5184
+ for (const pattern of patterns) {
5185
+ try {
5186
+ compiled.push(new RegExp(pattern));
5187
+ } catch {
5188
+ }
5189
+ }
5190
+ return compiled;
5191
+ }
5192
+ var no_raw_fetch_outside_clients_default = import_utils42.ESLintUtils.RuleCreator(
5193
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5194
+ )({
5195
+ name: "no-raw-fetch-outside-clients",
5196
+ meta: {
5197
+ type: "problem",
5198
+ docs: {
5199
+ description: "Disallow calling the global `fetch` outside the client layer; route outbound HTTP through a client module that owns retry, timeout and status handling."
5200
+ },
5201
+ schema: [
5202
+ {
5203
+ type: "object",
5204
+ properties: {
5205
+ allow: {
5206
+ type: "array",
5207
+ items: { type: "string" },
5208
+ description: "Regular-expression sources matched against the filename. Replaces the defaults."
5209
+ }
5210
+ },
5211
+ additionalProperties: false
5212
+ }
5213
+ ],
5214
+ messages: {
5215
+ 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."
5216
+ }
5217
+ },
5218
+ defaultOptions: [{}],
5219
+ create(context, [options]) {
5220
+ const patterns = options?.allow ?? DEFAULT_ALLOW;
5221
+ const allowed = compile(patterns);
5222
+ const filename = context.filename;
5223
+ if (allowed.some((re) => re.test(filename))) {
5224
+ return {};
5225
+ }
5226
+ return {
5227
+ CallExpression(node) {
5228
+ if (isGlobalFetchCall(node)) {
5229
+ context.report({ node, messageId: "rawFetch" });
5230
+ }
5231
+ }
5232
+ };
5233
+ }
5234
+ });
5235
+
5236
+ // src/rules/no-storage-in-stateless-modules.ts
5237
+ var import_utils43 = require("@typescript-eslint/utils");
5238
+ var DEFAULT_METHODS2 = [
5239
+ "prepare",
5240
+ "put",
5241
+ "getWithMetadata"
5242
+ ];
5243
+ var MIN_ARGUMENTS = /* @__PURE__ */ new Map([["put", 2]]);
5244
+ function compile2(patterns) {
5245
+ const compiled = [];
5246
+ for (const pattern of patterns) {
5247
+ try {
5248
+ compiled.push(new RegExp(pattern));
5249
+ } catch {
5250
+ }
5251
+ }
5252
+ return compiled;
5253
+ }
5254
+ function storageMethodName(node, methods) {
5255
+ const callee = node.callee;
5256
+ if (callee.type !== import_utils43.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils43.AST_NODE_TYPES.Identifier) {
5257
+ return null;
5258
+ }
5259
+ const name = callee.property.name;
5260
+ if (!methods.has(name)) {
5261
+ return null;
5262
+ }
5263
+ if (node.arguments.length < (MIN_ARGUMENTS.get(name) ?? 1)) {
5264
+ return null;
5265
+ }
5266
+ return name;
5267
+ }
5268
+ var no_storage_in_stateless_modules_default = import_utils43.ESLintUtils.RuleCreator(
5269
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
5270
+ )({
5271
+ name: "no-storage-in-stateless-modules",
5272
+ meta: {
5273
+ type: "problem",
5274
+ docs: {
5275
+ 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."
5276
+ },
5277
+ schema: [
5278
+ {
5279
+ type: "object",
5280
+ properties: {
5281
+ modules: {
5282
+ type: "array",
5283
+ items: { type: "string" },
5284
+ description: "Regex sources matched against the filename. Empty (the default) disables the rule."
5285
+ },
5286
+ methods: {
5287
+ type: "array",
5288
+ items: { type: "string" },
5289
+ description: "Storage method names to flag. Replaces the defaults."
5290
+ }
5291
+ },
5292
+ additionalProperties: false
5293
+ }
5294
+ ],
5295
+ messages: {
5296
+ 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."
5297
+ }
5298
+ },
5299
+ defaultOptions: [{}],
5300
+ create(context, [options]) {
5301
+ const modules = options?.modules ?? [];
5302
+ if (modules.length === 0) {
5303
+ return {};
5304
+ }
5305
+ const scoped = compile2(modules);
5306
+ if (!scoped.some((re) => re.test(context.filename))) {
5307
+ return {};
5308
+ }
5309
+ const methods = new Set(options?.methods ?? DEFAULT_METHODS2);
5310
+ return {
5311
+ CallExpression(node) {
5312
+ const method = storageMethodName(node, methods);
5313
+ if (method !== null) {
5314
+ context.report({
5315
+ node,
5316
+ messageId: "storageInStatelessModule",
5317
+ data: { method }
5318
+ });
5319
+ }
5320
+ }
5321
+ };
5322
+ }
5323
+ });
5324
+
4046
5325
  // src/index.ts
4047
5326
  var rules = {
4048
5327
  "enforce-file-structure": enforce_file_structure_default,
@@ -4073,12 +5352,22 @@ var rules = {
4073
5352
  "single-public-export": single_public_export_default,
4074
5353
  "no-silent-promise-catch": no_silent_promise_catch_default,
4075
5354
  "require-fetch-timeout": require_fetch_timeout_default,
4076
- "require-schema-validate-search": require_schema_validate_search_default
5355
+ "require-schema-validate-search": require_schema_validate_search_default,
5356
+ "no-offset-pagination": no_offset_pagination_default,
5357
+ "no-positional-tuple-return": no_positional_tuple_return_default,
5358
+ "no-repeated-string-literal": no_repeated_string_literal_default,
5359
+ "no-select-star": no_select_star_default,
5360
+ "no-sleep-in-test-body": no_sleep_in_test_body_default,
5361
+ "prefer-constant-time-secret-compare": prefer_constant_time_secret_compare_default,
5362
+ "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
5363
+ "no-dynamic-sql": no_dynamic_sql_default,
5364
+ "no-raw-fetch-outside-clients": no_raw_fetch_outside_clients_default,
5365
+ "no-storage-in-stateless-modules": no_storage_in_stateless_modules_default
4077
5366
  };
4078
5367
  var plugin = {
4079
5368
  meta: {
4080
5369
  name: "@sarj/eslint-plugin",
4081
- version: "2.7.0"
5370
+ version: "2.8.0"
4082
5371
  },
4083
5372
  rules,
4084
5373
  configs: {
@@ -4114,7 +5403,20 @@ var plugin = {
4114
5403
  // Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
4115
5404
  "@sarj/require-fetch-timeout": "warn",
4116
5405
  "@sarj/no-silent-promise-catch": "warn",
4117
- "@sarj/require-schema-validate-search": "warn"
5406
+ "@sarj/require-schema-validate-search": "warn",
5407
+ // Second SARJ port wave — the TS/Python parity gap. Each targets a
5408
+ // defect class seen in production Workers code: timing-leaky secret
5409
+ // compares, non-idempotent store writes under queue redelivery,
5410
+ // O(N) pagination, implicit row contracts, flaky timed tests.
5411
+ "@sarj/prefer-constant-time-secret-compare": "error",
5412
+ "@sarj/store-insert-requires-on-conflict": "warn",
5413
+ "@sarj/no-offset-pagination": "warn",
5414
+ "@sarj/no-select-star": "warn",
5415
+ "@sarj/no-sleep-in-test-body": "warn",
5416
+ "@sarj/no-repeated-string-literal": "warn",
5417
+ "@sarj/no-positional-tuple-return": "warn",
5418
+ // Injection guard — low FP, applies to any repo touching SQL.
5419
+ "@sarj/no-dynamic-sql": "warn"
4118
5420
  }
4119
5421
  },
4120
5422
  strict: {
@@ -4154,7 +5456,27 @@ var plugin = {
4154
5456
  // Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
4155
5457
  "@sarj/require-fetch-timeout": "error",
4156
5458
  "@sarj/no-silent-promise-catch": "error",
4157
- "@sarj/require-schema-validate-search": "error"
5459
+ "@sarj/require-schema-validate-search": "error",
5460
+ // Second SARJ port wave — the TS/Python parity gap.
5461
+ "@sarj/prefer-constant-time-secret-compare": "error",
5462
+ "@sarj/store-insert-requires-on-conflict": "error",
5463
+ "@sarj/no-offset-pagination": "error",
5464
+ "@sarj/no-select-star": "error",
5465
+ "@sarj/no-sleep-in-test-body": "error",
5466
+ "@sarj/no-repeated-string-literal": "error",
5467
+ // API-shape advice rather than a runtime defect — a corpus sweep found its
5468
+ // only hits are parser `[value, cursor]` returns, which are conventional.
5469
+ // Warn even in strict until a rollout justifies more.
5470
+ "@sarj/no-positional-tuple-return": "warn",
5471
+ "@sarj/no-dynamic-sql": "error",
5472
+ // Architectural: both need per-repo config to be meaningful, so they
5473
+ // are strict-only. `no-storage-in-stateless-modules` is a no-op until
5474
+ // its `modules` option names the directories a team declared stateless;
5475
+ // `no-raw-fetch-outside-clients` defaults to the `clients/` convention
5476
+ // and takes an `allow` list for repos that lay their client layer out
5477
+ // differently.
5478
+ "@sarj/no-raw-fetch-outside-clients": "error",
5479
+ "@sarj/no-storage-in-stateless-modules": "error"
4158
5480
  }
4159
5481
  }
4160
5482
  }