@noctcore/eslint-plugin-contracts 0.2.0 → 0.3.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
@@ -7,6 +7,8 @@ var recommended = {
7
7
  "noctcore-contracts/money-must-be-decimal": "error",
8
8
  "noctcore-contracts/require-error-cause": "error",
9
9
  "noctcore-contracts/restrict-throw-to-taxonomy": "error",
10
+ "noctcore-contracts/schema-enum-field-consistency": "error",
11
+ "noctcore-contracts/fetch-must-check-ok": "error",
10
12
  // Config-required / heuristic rules ship inert. `require-registered-keys` and
11
13
  // `env-var-schema-parity` do nothing until their `sinks` / `schema` options are
12
14
  // set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
@@ -113,9 +115,476 @@ var envVarSchemaParityRule = createRule({
113
115
  }
114
116
  });
115
117
 
116
- // src/rules/money-must-be-decimal.ts
118
+ // src/rules/fetch-must-check-ok.ts
117
119
  import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
118
- var RULE_NAME2 = "money-must-be-decimal";
120
+ var RULE_NAME2 = "fetch-must-check-ok";
121
+ var optionSchema2 = {
122
+ type: "object",
123
+ additionalProperties: false,
124
+ properties: {
125
+ fetchFunctions: {
126
+ type: "array",
127
+ items: { type: "string", minLength: 1 },
128
+ uniqueItems: true
129
+ }
130
+ }
131
+ };
132
+ function isNode(value) {
133
+ return typeof value === "object" && value !== null && "type" in value;
134
+ }
135
+ function walkSome(root, keys, predicate) {
136
+ const stack = [root];
137
+ for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
138
+ if (predicate(node)) {
139
+ return true;
140
+ }
141
+ for (const key of keys[node.type] ?? []) {
142
+ const value = Reflect.get(node, key);
143
+ if (Array.isArray(value)) {
144
+ for (const child of value) {
145
+ if (isNode(child)) {
146
+ stack.push(child);
147
+ }
148
+ }
149
+ } else if (isNode(value)) {
150
+ stack.push(value);
151
+ }
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+ function calleePath(node) {
157
+ if (node.type === AST_NODE_TYPES2.Identifier) {
158
+ return node.name;
159
+ }
160
+ if (node.type === AST_NODE_TYPES2.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES2.Identifier) {
161
+ const object = calleePath(node.object);
162
+ return object === null ? null : `${object}.${node.property.name}`;
163
+ }
164
+ return null;
165
+ }
166
+ var OK_PROP = "ok";
167
+ var OK_PROPS = /* @__PURE__ */ new Set([OK_PROP, "status"]);
168
+ var ASSERTION_NAMES = /^(?:[Aa]ssert|[Ii]nvariant|[Ee]nsure|[Ee]xpect)(?:[A-Z_]\w*)?$/u;
169
+ var COMPARISONS = /* @__PURE__ */ new Set(["===", "!==", "==", "!=", "<", "<=", ">", ">="]);
170
+ var FIRST_ERROR_STATUS = 400;
171
+ function literalValue(node) {
172
+ if (node.type !== AST_NODE_TYPES2.Literal) {
173
+ return void 0;
174
+ }
175
+ return typeof node.value === "number" || typeof node.value === "boolean" ? node.value : void 0;
176
+ }
177
+ function mirror(operator) {
178
+ switch (operator) {
179
+ case "<":
180
+ return ">";
181
+ case "<=":
182
+ return ">=";
183
+ case ">":
184
+ return "<";
185
+ case ">=":
186
+ return "<=";
187
+ default:
188
+ return operator;
189
+ }
190
+ }
191
+ function booleanPolarity(operator, value) {
192
+ if (operator === "===" || operator === "==") {
193
+ return value ? "positive" : "negative";
194
+ }
195
+ if (operator === "!==" || operator === "!=") {
196
+ return value ? "negative" : "positive";
197
+ }
198
+ return "opaque";
199
+ }
200
+ function statusPolarity(operator, value) {
201
+ const isSuccessCode = value >= 200 && value < 300;
202
+ switch (operator) {
203
+ case "===":
204
+ case "==":
205
+ return isSuccessCode ? "positive" : "opaque";
206
+ case "!==":
207
+ case "!=":
208
+ return isSuccessCode ? "negative" : "opaque";
209
+ case "<":
210
+ return value <= FIRST_ERROR_STATUS ? "positive" : "opaque";
211
+ case "<=":
212
+ return value < FIRST_ERROR_STATUS ? "positive" : "opaque";
213
+ // A failure test is only useful for what it says about the OTHER side, so
214
+ // what matters is that everything below the threshold is a success:
215
+ // `>= 300` and `>= 400` both leave only good responses behind, while
216
+ // `>= 500` leaves every 4xx there.
217
+ case ">=":
218
+ return value <= FIRST_ERROR_STATUS ? "negative" : "opaque";
219
+ case ">":
220
+ return value < FIRST_ERROR_STATUS ? "negative" : "opaque";
221
+ default:
222
+ return "opaque";
223
+ }
224
+ }
225
+ function comparisonPolarity(node, readIsLeft) {
226
+ const value = literalValue(readIsLeft ? node.right : node.left);
227
+ const operator = readIsLeft ? node.operator : mirror(node.operator);
228
+ if (typeof value === "boolean") {
229
+ return booleanPolarity(operator, value);
230
+ }
231
+ return typeof value === "number" ? statusPolarity(operator, value) : "opaque";
232
+ }
233
+ function propReadOn(node, objectName, props) {
234
+ if (node.type !== AST_NODE_TYPES2.MemberExpression || node.computed) {
235
+ return false;
236
+ }
237
+ if (node.object.type !== AST_NODE_TYPES2.Identifier || node.object.name !== objectName || node.property.type !== AST_NODE_TYPES2.Identifier) {
238
+ return false;
239
+ }
240
+ const name = node.property.name;
241
+ return typeof props === "string" ? name === props : props.has(name);
242
+ }
243
+ function findJsonReads(root, keys, name) {
244
+ const reads = [];
245
+ walkSome(root, keys, (node) => {
246
+ if (propReadOn(node, name, "json")) {
247
+ reads.push(node);
248
+ }
249
+ return false;
250
+ });
251
+ return reads;
252
+ }
253
+ function isAssertionName(node) {
254
+ return node.type === AST_NODE_TYPES2.Identifier && ASSERTION_NAMES.test(node.name);
255
+ }
256
+ function isAssertionCallee(callee) {
257
+ if (callee.type === AST_NODE_TYPES2.Identifier) {
258
+ return isAssertionName(callee);
259
+ }
260
+ return callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && (isAssertionName(callee.object) || isAssertionName(callee.property));
261
+ }
262
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
263
+ AST_NODE_TYPES2.IfStatement,
264
+ AST_NODE_TYPES2.WhileStatement,
265
+ AST_NODE_TYPES2.DoWhileStatement,
266
+ AST_NODE_TYPES2.ConditionalExpression,
267
+ AST_NODE_TYPES2.SwitchStatement,
268
+ AST_NODE_TYPES2.CallExpression
269
+ ]);
270
+ var ASSERTION_OPERATORS = /* @__PURE__ */ new Map([
271
+ ["equal", "==="],
272
+ ["equals", "==="],
273
+ ["strictEqual", "==="],
274
+ ["deepEqual", "==="],
275
+ ["deepStrictEqual", "==="],
276
+ ["toBe", "==="],
277
+ ["toEqual", "==="],
278
+ ["notEqual", "!=="],
279
+ ["notStrictEqual", "!=="],
280
+ ["notDeepEqual", "!=="]
281
+ ]);
282
+ function assertionOperator(callee) {
283
+ return callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES2.Identifier ? ASSERTION_OPERATORS.get(callee.property.name) : void 0;
284
+ }
285
+ function assertionCheck(parent, child, check) {
286
+ if (check.compared) {
287
+ return check;
288
+ }
289
+ const operator = assertionOperator(parent.callee);
290
+ if (operator === void 0) {
291
+ return check;
292
+ }
293
+ const other = parent.arguments.find((arg) => arg !== child);
294
+ const value = other === void 0 ? void 0 : literalValue(other);
295
+ if (typeof value === "boolean") {
296
+ return {
297
+ ...check,
298
+ compared: true,
299
+ polarity: booleanPolarity(operator, value)
300
+ };
301
+ }
302
+ return typeof value === "number" ? { ...check, compared: true, polarity: statusPolarity(operator, value) } : check;
303
+ }
304
+ function terminalCheck(parent, child, state) {
305
+ const check = { owner: parent, ...state };
306
+ switch (parent.type) {
307
+ case AST_NODE_TYPES2.IfStatement:
308
+ case AST_NODE_TYPES2.WhileStatement:
309
+ case AST_NODE_TYPES2.DoWhileStatement:
310
+ case AST_NODE_TYPES2.ConditionalExpression:
311
+ return parent.test === child ? check : null;
312
+ case AST_NODE_TYPES2.SwitchStatement:
313
+ return parent.discriminant === child ? { ...check, compared: true, polarity: "opaque" } : null;
314
+ // An assertion settles a status only when it COMPARES it.
315
+ // `assert.equal(res.status, 200)` does; `assert.ok(res.status)` asserts a
316
+ // number is truthy, which every response that arrived satisfies. Arity
317
+ // cannot tell them apart (`assert.ok(res.status, 'message')` also has two
318
+ // arguments), so the comparison is read from the assertion's own name.
319
+ case AST_NODE_TYPES2.CallExpression:
320
+ return isAssertionCallee(parent.callee) && parent.callee !== child ? assertionCheck(parent, child, check) : null;
321
+ default:
322
+ return null;
323
+ }
324
+ }
325
+ function combinatorStep(parent, child, state, parse) {
326
+ switch (parent.type) {
327
+ case AST_NODE_TYPES2.UnaryExpression:
328
+ if (parent.operator === "typeof") {
329
+ return "stop";
330
+ }
331
+ if (parent.operator === "!") {
332
+ state.polarity = state.polarity === "positive" ? "negative" : "positive";
333
+ }
334
+ return "continue";
335
+ case AST_NODE_TYPES2.BinaryExpression: {
336
+ if (!COMPARISONS.has(parent.operator)) {
337
+ return "stop";
338
+ }
339
+ const polarity = comparisonPolarity(parent, parent.left === child);
340
+ state.compared = true;
341
+ state.polarity = polarity;
342
+ return polarity === "opaque" ? "stop" : "continue";
343
+ }
344
+ case AST_NODE_TYPES2.LogicalExpression:
345
+ if (parent.operator === "&&") {
346
+ state.underAnd = true;
347
+ }
348
+ if (parent.operator === "||") {
349
+ state.underOr = true;
350
+ }
351
+ return parent.left === child && contains(parent.right, parse) ? { owner: parent, ...state } : "continue";
352
+ case AST_NODE_TYPES2.ChainExpression:
353
+ case AST_NODE_TYPES2.TSNonNullExpression:
354
+ return "continue";
355
+ default:
356
+ return "stop";
357
+ }
358
+ }
359
+ function climb(read, parse) {
360
+ const state = {
361
+ polarity: "positive",
362
+ compared: false,
363
+ underAnd: false,
364
+ underOr: false
365
+ };
366
+ let child = read;
367
+ let parent = read.parent;
368
+ while (parent !== void 0) {
369
+ if (TERMINAL_TYPES.has(parent.type)) {
370
+ return terminalCheck(parent, child, state);
371
+ }
372
+ const step = combinatorStep(parent, child, state, parse);
373
+ if (step === "stop") {
374
+ return null;
375
+ }
376
+ if (step !== "continue") {
377
+ return step;
378
+ }
379
+ child = parent;
380
+ parent = parent.parent;
381
+ }
382
+ return null;
383
+ }
384
+ function contains(outer, inner) {
385
+ return outer.range[0] <= inner.range[0] && outer.range[1] >= inner.range[1];
386
+ }
387
+ function alwaysExits(node) {
388
+ if (node.type === AST_NODE_TYPES2.ReturnStatement || node.type === AST_NODE_TYPES2.ThrowStatement) {
389
+ return true;
390
+ }
391
+ return node.type === AST_NODE_TYPES2.BlockStatement && node.body.some((stmt) => alwaysExits(stmt));
392
+ }
393
+ function scopeOfCheck(node) {
394
+ let current = node;
395
+ while (current.parent !== void 0 && !current.type.endsWith("Statement")) {
396
+ current = current.parent;
397
+ }
398
+ return current.parent ?? current;
399
+ }
400
+ function isSuccessCase(arm) {
401
+ if (arm.test === null) {
402
+ return false;
403
+ }
404
+ const value = literalValue(arm.test);
405
+ return typeof value === "number" && value >= 200 && value < 300;
406
+ }
407
+ function switchArmProtects(owner, parse) {
408
+ const index = owner.cases.findIndex((arm) => contains(arm, parse));
409
+ const own = owner.cases[index];
410
+ if (own === void 0 || !isSuccessCase(own)) {
411
+ return false;
412
+ }
413
+ for (let i = index - 1; i >= 0; i -= 1) {
414
+ const arm = owner.cases[i];
415
+ if (arm === void 0 || arm.consequent.length > 0) {
416
+ break;
417
+ }
418
+ if (!isSuccessCase(arm)) {
419
+ return false;
420
+ }
421
+ }
422
+ return true;
423
+ }
424
+ function branchProtects(check, parse) {
425
+ const { owner } = check;
426
+ if (owner.type === AST_NODE_TYPES2.SwitchStatement) {
427
+ return switchArmProtects(owner, parse);
428
+ }
429
+ if (owner.type === AST_NODE_TYPES2.LogicalExpression) {
430
+ if (!contains(owner.right, parse)) {
431
+ return false;
432
+ }
433
+ if (owner.operator === "&&") {
434
+ return check.polarity === "positive";
435
+ }
436
+ return owner.operator === "||" && check.polarity === "negative";
437
+ }
438
+ if (owner.type === AST_NODE_TYPES2.WhileStatement || owner.type === AST_NODE_TYPES2.DoWhileStatement) {
439
+ return contains(owner.body, parse) && entersOnSuccess(check);
440
+ }
441
+ if (owner.type !== AST_NODE_TYPES2.ConditionalExpression && owner.type !== AST_NODE_TYPES2.IfStatement) {
442
+ return false;
443
+ }
444
+ if (contains(owner.consequent, parse)) {
445
+ return entersOnSuccess(check);
446
+ }
447
+ return owner.alternate !== null && contains(owner.alternate, parse) && skipsOnSuccess(check);
448
+ }
449
+ function entersOnSuccess(check) {
450
+ return check.polarity === "positive" && !check.underOr;
451
+ }
452
+ function skipsOnSuccess(check) {
453
+ return check.polarity === "negative" && !check.underAnd;
454
+ }
455
+ function guardProtects(check, parse) {
456
+ const { owner } = check;
457
+ if (owner.type === AST_NODE_TYPES2.CallExpression) {
458
+ return entersOnSuccess(check) && owner.range[1] <= parse.range[0] && contains(scopeOfCheck(owner), parse);
459
+ }
460
+ if (owner.type !== AST_NODE_TYPES2.IfStatement) {
461
+ return false;
462
+ }
463
+ if (owner.range[1] > parse.range[0] || !contains(scopeOfCheck(owner), parse)) {
464
+ return false;
465
+ }
466
+ if (alwaysExits(owner.consequent)) {
467
+ return skipsOnSuccess(check);
468
+ }
469
+ return owner.alternate !== null && alwaysExits(owner.alternate) && entersOnSuccess(check);
470
+ }
471
+ function protects(check, parse) {
472
+ if (!check.compared) {
473
+ return false;
474
+ }
475
+ return branchProtects(check, parse) || guardProtects(check, parse);
476
+ }
477
+ function statusAliases(root, keys, name) {
478
+ const aliases = /* @__PURE__ */ new Map();
479
+ walkSome(root, keys, (node) => {
480
+ if (node.type === AST_NODE_TYPES2.VariableDeclarator && node.id.type === AST_NODE_TYPES2.Identifier && node.init !== null && propReadOn(node.init, name, OK_PROPS) && node.init.property.type === AST_NODE_TYPES2.Identifier) {
481
+ aliases.set(node.id.name, node.init.property.name);
482
+ }
483
+ return false;
484
+ });
485
+ return aliases;
486
+ }
487
+ function checkedProp(node, name, aliases) {
488
+ if (propReadOn(node, name, OK_PROPS)) {
489
+ return node.property.type === AST_NODE_TYPES2.Identifier ? node.property.name : void 0;
490
+ }
491
+ return node.type === AST_NODE_TYPES2.Identifier ? aliases.get(node.name) : void 0;
492
+ }
493
+ function isProtected(root, keys, name, aliases, parse) {
494
+ return walkSome(root, keys, (node) => {
495
+ const prop = checkedProp(node, name, aliases);
496
+ if (prop === void 0) {
497
+ return false;
498
+ }
499
+ const check = climb(node, parse);
500
+ if (check === null) {
501
+ return false;
502
+ }
503
+ return protects(
504
+ prop === OK_PROP ? { ...check, compared: true } : check,
505
+ parse
506
+ );
507
+ });
508
+ }
509
+ function scopeOf(node) {
510
+ let current = node.parent;
511
+ while (current !== void 0) {
512
+ if (current.type === AST_NODE_TYPES2.BlockStatement || current.type === AST_NODE_TYPES2.Program) {
513
+ return current;
514
+ }
515
+ current = current.parent;
516
+ }
517
+ return node;
518
+ }
519
+ function skipAwait(node) {
520
+ return node?.type === AST_NODE_TYPES2.AwaitExpression ? node.parent : node;
521
+ }
522
+ function thenCallbackParam(node) {
523
+ if (node.type !== AST_NODE_TYPES2.MemberExpression || node.computed || node.property.type !== AST_NODE_TYPES2.Identifier || node.property.name !== "then" || node.parent.type !== AST_NODE_TYPES2.CallExpression) {
524
+ return null;
525
+ }
526
+ const callback = node.parent.arguments[0];
527
+ if (callback === void 0 || callback.type !== AST_NODE_TYPES2.ArrowFunctionExpression && callback.type !== AST_NODE_TYPES2.FunctionExpression) {
528
+ return null;
529
+ }
530
+ const param = callback.params[0];
531
+ return param?.type === AST_NODE_TYPES2.Identifier ? { name: param.name, body: callback.body } : null;
532
+ }
533
+ var fetchMustCheckOkRule = createRule({
534
+ name: RULE_NAME2,
535
+ meta: {
536
+ type: "problem",
537
+ docs: {
538
+ description: "Require a fetch response to be checked with `.ok` or a status comparison before `.json()` parses its body."
539
+ },
540
+ schema: [optionSchema2],
541
+ messages: {
542
+ missingOkCheck: "`fetch` resolves on 4xx/5xx too, so `.json()` here can parse an error body as data. Check `response.ok` (or compare the status) and leave early before reading the body."
543
+ }
544
+ },
545
+ defaultOptions: [{ fetchFunctions: ["fetch"] }],
546
+ create(context, [options]) {
547
+ const fetchFunctions = new Set(options.fetchFunctions ?? ["fetch"]);
548
+ const keys = context.sourceCode.visitorKeys;
549
+ function reportUnprotected(root, name) {
550
+ const aliases = statusAliases(root, keys, name);
551
+ for (const read of findJsonReads(root, keys, name)) {
552
+ if (!isProtected(root, keys, name, aliases, read)) {
553
+ context.report({ node: read, messageId: "missingOkCheck" });
554
+ }
555
+ }
556
+ }
557
+ return {
558
+ CallExpression(node) {
559
+ const path2 = calleePath(node.callee);
560
+ if (path2 === null || !fetchFunctions.has(path2)) {
561
+ return;
562
+ }
563
+ const parent = skipAwait(node.parent);
564
+ if (parent === void 0) {
565
+ return;
566
+ }
567
+ if (parent.type === AST_NODE_TYPES2.MemberExpression && !parent.computed && parent.property.type === AST_NODE_TYPES2.Identifier && parent.property.name === "json") {
568
+ context.report({ node: parent, messageId: "missingOkCheck" });
569
+ return;
570
+ }
571
+ const callback = thenCallbackParam(parent);
572
+ if (callback !== null) {
573
+ reportUnprotected(callback.body, callback.name);
574
+ return;
575
+ }
576
+ if (parent.type !== AST_NODE_TYPES2.VariableDeclarator || parent.id.type !== AST_NODE_TYPES2.Identifier) {
577
+ return;
578
+ }
579
+ reportUnprotected(scopeOf(parent), parent.id.name);
580
+ }
581
+ };
582
+ }
583
+ });
584
+
585
+ // src/rules/money-must-be-decimal.ts
586
+ import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
587
+ var RULE_NAME3 = "money-must-be-decimal";
119
588
  var DEFAULT_DECIMAL_TYPE = "Decimal";
120
589
  var DEFAULT_FIELD_PATTERNS = [
121
590
  "amount",
@@ -125,7 +594,7 @@ var DEFAULT_FIELD_PATTERNS = [
125
594
  "balance"
126
595
  ];
127
596
  var DEFAULT_ALLOWED_FILES = [];
128
- var optionSchema2 = {
597
+ var optionSchema3 = {
129
598
  type: "object",
130
599
  additionalProperties: false,
131
600
  properties: {
@@ -154,22 +623,22 @@ function isAllowedFile(filename, patterns) {
154
623
  return patterns.some((pattern) => normalized.endsWith(toForwardSlash(pattern)));
155
624
  }
156
625
  function staticName(node) {
157
- if (node.type === AST_NODE_TYPES2.Identifier) {
626
+ if (node.type === AST_NODE_TYPES3.Identifier) {
158
627
  return node.name;
159
628
  }
160
629
  return void 0;
161
630
  }
162
631
  function isNumberAnnotation(annotation) {
163
- return annotation?.typeAnnotation.type === AST_NODE_TYPES2.TSNumberKeyword;
632
+ return annotation?.typeAnnotation.type === AST_NODE_TYPES3.TSNumberKeyword;
164
633
  }
165
634
  var moneyMustBeDecimalRule = createRule({
166
- name: RULE_NAME2,
635
+ name: RULE_NAME3,
167
636
  meta: {
168
637
  type: "problem",
169
638
  docs: {
170
639
  description: "Disallow monetary values typed as the JS primitive `number`. Money-named fields explicitly typed `: number` lose precision to float rounding; use a Decimal money type instead."
171
640
  },
172
- schema: [optionSchema2],
641
+ schema: [optionSchema3],
173
642
  messages: {
174
643
  moneyMustBeDecimal: "Monetary values must use {{decimalType}}, never the JS `number` primitive, to avoid float rounding errors. Rename or retype this field to a {{decimalType}} money type."
175
644
  }
@@ -205,7 +674,7 @@ var moneyMustBeDecimalRule = createRule({
205
674
  },
206
675
  // `const total: number = ...`: annotated variable declarator.
207
676
  VariableDeclarator(node) {
208
- if (node.id.type !== AST_NODE_TYPES2.Identifier) {
677
+ if (node.id.type !== AST_NODE_TYPES3.Identifier) {
209
678
  return;
210
679
  }
211
680
  const name = node.id.name;
@@ -218,15 +687,15 @@ var moneyMustBeDecimalRule = createRule({
218
687
  });
219
688
 
220
689
  // src/rules/no-direct-process-env.ts
221
- import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
222
- var RULE_NAME3 = "no-direct-process-env";
690
+ import { AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
691
+ var RULE_NAME4 = "no-direct-process-env";
223
692
  var DEFAULT_CONFIG_MODULE = "@/config";
224
693
  var DEFAULT_ALLOWED_FILES2 = [
225
694
  "**/*.config.{ts,js,mjs,cjs}",
226
695
  "**/*.{spec,test}.{ts,tsx}",
227
696
  "**/scripts/**"
228
697
  ];
229
- var optionSchema3 = {
698
+ var optionSchema4 = {
230
699
  type: "object",
231
700
  additionalProperties: false,
232
701
  properties: {
@@ -281,22 +750,22 @@ function isAllowedFile2(filename, patterns) {
281
750
  return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
282
751
  }
283
752
  function isProcessEnv2(node) {
284
- if (node.type !== AST_NODE_TYPES3.MemberExpression || node.object.type !== AST_NODE_TYPES3.Identifier || node.object.name !== "process") {
753
+ if (node.type !== AST_NODE_TYPES4.MemberExpression || node.object.type !== AST_NODE_TYPES4.Identifier || node.object.name !== "process") {
285
754
  return false;
286
755
  }
287
756
  if (node.computed) {
288
- return node.property.type === AST_NODE_TYPES3.Literal && node.property.value === "env";
757
+ return node.property.type === AST_NODE_TYPES4.Literal && node.property.value === "env";
289
758
  }
290
- return node.property.type === AST_NODE_TYPES3.Identifier && node.property.name === "env";
759
+ return node.property.type === AST_NODE_TYPES4.Identifier && node.property.name === "env";
291
760
  }
292
761
  var noDirectProcessEnvRule = createRule({
293
- name: RULE_NAME3,
762
+ name: RULE_NAME4,
294
763
  meta: {
295
764
  type: "problem",
296
765
  docs: {
297
766
  description: "Disallow direct `process.env` access. Force every consumer through a typed, validated config accessor so a missing variable fails at boot, not at use."
298
767
  },
299
- schema: [optionSchema3],
768
+ schema: [optionSchema4],
300
769
  messages: {
301
770
  directProcessEnv: "Read environment variables through your typed config accessor (import from `{{configModule}}`). Direct `process.env` access bypasses boot-time validation."
302
771
  }
@@ -331,10 +800,10 @@ var noDirectProcessEnvRule = createRule({
331
800
  });
332
801
 
333
802
  // src/rules/no-error-stringify.ts
334
- import { AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
335
- var RULE_NAME4 = "no-error-stringify";
803
+ import { AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
804
+ var RULE_NAME5 = "no-error-stringify";
336
805
  var DEFAULT_ERROR_NAMES = ["error", "err", "e", "cause"];
337
- var optionSchema4 = {
806
+ var optionSchema5 = {
338
807
  type: "object",
339
808
  additionalProperties: false,
340
809
  properties: {
@@ -347,19 +816,19 @@ var optionSchema4 = {
347
816
  }
348
817
  };
349
818
  function isEmptyStringLiteral(node) {
350
- return node.type === AST_NODE_TYPES4.Literal && node.value === "";
819
+ return node.type === AST_NODE_TYPES5.Literal && node.value === "";
351
820
  }
352
821
  function isErrorIdentifier(node, names) {
353
- return node.type === AST_NODE_TYPES4.Identifier && names.has(node.name);
822
+ return node.type === AST_NODE_TYPES5.Identifier && names.has(node.name);
354
823
  }
355
824
  var noErrorStringifyRule = createRule({
356
- name: RULE_NAME4,
825
+ name: RULE_NAME5,
357
826
  meta: {
358
827
  type: "problem",
359
828
  docs: {
360
829
  description: 'Disallow stringifying an error with bare `${error}` interpolation, `error.toString()`, or `error + ""`. These drop the cause chain. Use `error instanceof Error ? error.message : String(error)` instead.'
361
830
  },
362
- schema: [optionSchema4],
831
+ schema: [optionSchema5],
363
832
  messages: {
364
833
  noErrorStringify: "Stringifying an error this way drops its cause chain. Use `{{name}} instanceof Error ? {{name}}.message : String({{name}})` (or pass the Error object straight to the logger)."
365
834
  }
@@ -374,7 +843,7 @@ var noErrorStringifyRule = createRule({
374
843
  // `error.toString()`
375
844
  'CallExpression[callee.type="MemberExpression"]'(node) {
376
845
  const callee = node.callee;
377
- if (!callee.computed && callee.property.type === AST_NODE_TYPES4.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
846
+ if (!callee.computed && callee.property.type === AST_NODE_TYPES5.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
378
847
  report(node, callee.object.name);
379
848
  }
380
849
  },
@@ -407,14 +876,14 @@ var noErrorStringifyRule = createRule({
407
876
  });
408
877
 
409
878
  // src/rules/require-error-cause.ts
410
- import { AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
411
- var RULE_NAME5 = "require-error-cause";
879
+ import { AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
880
+ var RULE_NAME6 = "require-error-cause";
412
881
  function constructorSimpleName(node) {
413
882
  const callee = node.callee;
414
- if (callee.type === AST_NODE_TYPES5.Identifier) {
883
+ if (callee.type === AST_NODE_TYPES6.Identifier) {
415
884
  return callee.name;
416
885
  }
417
- if (callee.type === AST_NODE_TYPES5.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES5.Identifier) {
886
+ if (callee.type === AST_NODE_TYPES6.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES6.Identifier) {
418
887
  return callee.property.name;
419
888
  }
420
889
  return null;
@@ -424,16 +893,16 @@ function isErrorLikeName(name) {
424
893
  }
425
894
  function alreadyHasCause(node) {
426
895
  for (const arg of node.arguments) {
427
- if (arg.type === AST_NODE_TYPES5.SpreadElement) {
896
+ if (arg.type === AST_NODE_TYPES6.SpreadElement) {
428
897
  return true;
429
898
  }
430
- if (arg.type === AST_NODE_TYPES5.ObjectExpression) {
899
+ if (arg.type === AST_NODE_TYPES6.ObjectExpression) {
431
900
  for (const prop of arg.properties) {
432
- if (prop.type === AST_NODE_TYPES5.SpreadElement) {
901
+ if (prop.type === AST_NODE_TYPES6.SpreadElement) {
433
902
  return true;
434
903
  }
435
904
  const key = prop.key;
436
- const isCause = key.type === AST_NODE_TYPES5.Identifier && key.name === "cause" || key.type === AST_NODE_TYPES5.Literal && key.value === "cause";
905
+ const isCause = key.type === AST_NODE_TYPES6.Identifier && key.name === "cause" || key.type === AST_NODE_TYPES6.Literal && key.value === "cause";
437
906
  if (isCause) {
438
907
  return true;
439
908
  }
@@ -451,7 +920,7 @@ function buildFix(node, binding) {
451
920
  if (last === void 0) {
452
921
  return null;
453
922
  }
454
- if (last.type === AST_NODE_TYPES5.ObjectExpression) {
923
+ if (last.type === AST_NODE_TYPES6.ObjectExpression) {
455
924
  const props = last.properties;
456
925
  if (props.length === 0) {
457
926
  return (fixer) => fixer.replaceText(last, `{ cause: ${binding} }`);
@@ -465,7 +934,7 @@ function buildFix(node, binding) {
465
934
  return (fixer) => fixer.insertTextAfter(last, `, { cause: ${binding} }`);
466
935
  }
467
936
  var requireErrorCauseRule = createRule({
468
- name: RULE_NAME5,
937
+ name: RULE_NAME6,
469
938
  meta: {
470
939
  type: "problem",
471
940
  docs: {
@@ -484,7 +953,7 @@ var requireErrorCauseRule = createRule({
484
953
  CatchClause(node) {
485
954
  const param = node.param;
486
955
  catchBindings.push(
487
- param && param.type === AST_NODE_TYPES5.Identifier ? param.name : null
956
+ param && param.type === AST_NODE_TYPES6.Identifier ? param.name : null
488
957
  );
489
958
  },
490
959
  "CatchClause:exit"() {
@@ -496,7 +965,7 @@ var requireErrorCauseRule = createRule({
496
965
  return;
497
966
  }
498
967
  const arg = node.argument;
499
- if (arg.type !== AST_NODE_TYPES5.NewExpression) {
968
+ if (arg.type !== AST_NODE_TYPES6.NewExpression) {
500
969
  return;
501
970
  }
502
971
  const ctor = constructorSimpleName(arg);
@@ -519,9 +988,9 @@ var requireErrorCauseRule = createRule({
519
988
  });
520
989
 
521
990
  // src/rules/require-registered-keys.ts
522
- import { AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
523
- var RULE_NAME6 = "require-registered-keys";
524
- var optionSchema5 = {
991
+ import { AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
992
+ var RULE_NAME7 = "require-registered-keys";
993
+ var optionSchema6 = {
525
994
  type: "object",
526
995
  additionalProperties: false,
527
996
  properties: {
@@ -540,30 +1009,30 @@ var optionSchema5 = {
540
1009
  registry: { type: "string", minLength: 1 }
541
1010
  }
542
1011
  };
543
- function calleePath(callee) {
544
- if (callee.type === AST_NODE_TYPES6.Identifier) {
1012
+ function calleePath2(callee) {
1013
+ if (callee.type === AST_NODE_TYPES7.Identifier) {
545
1014
  return callee.name;
546
1015
  }
547
- if (callee.type === AST_NODE_TYPES6.MemberExpression && !callee.computed) {
548
- if (callee.property.type !== AST_NODE_TYPES6.Identifier) {
1016
+ if (callee.type === AST_NODE_TYPES7.MemberExpression && !callee.computed) {
1017
+ if (callee.property.type !== AST_NODE_TYPES7.Identifier) {
549
1018
  return null;
550
1019
  }
551
- const objectPath = calleePath(callee.object);
1020
+ const objectPath = calleePath2(callee.object);
552
1021
  return objectPath === null ? null : `${objectPath}.${callee.property.name}`;
553
1022
  }
554
1023
  return null;
555
1024
  }
556
1025
  function isStringLiteral(node) {
557
- return node.type === AST_NODE_TYPES6.Literal && typeof node.value === "string";
1026
+ return node.type === AST_NODE_TYPES7.Literal && typeof node.value === "string";
558
1027
  }
559
1028
  var requireRegisteredKeysRule = createRule({
560
- name: RULE_NAME6,
1029
+ name: RULE_NAME7,
561
1030
  meta: {
562
1031
  type: "suggestion",
563
1032
  docs: {
564
1033
  description: "Require the key/name argument of configured sink APIs (storage, event channels, cache keys) to be an imported constant from a registry module, not a raw string literal."
565
1034
  },
566
- schema: [optionSchema5],
1035
+ schema: [optionSchema6],
567
1036
  messages: {
568
1037
  unregisteredKey: "Pass an imported key constant to `{{callee}}`, not the raw string {{value}}{{registryHint}}. Raw string keys drift out of sync across call sites."
569
1038
  }
@@ -587,7 +1056,7 @@ var requireRegisteredKeysRule = createRule({
587
1056
  const registryHint = registry ? ` (import it from '${registry}')` : "";
588
1057
  return {
589
1058
  CallExpression(node) {
590
- const path2 = calleePath(node.callee);
1059
+ const path2 = calleePath2(node.callee);
591
1060
  if (path2 === null) {
592
1061
  return;
593
1062
  }
@@ -611,29 +1080,29 @@ var requireRegisteredKeysRule = createRule({
611
1080
  });
612
1081
 
613
1082
  // src/rules/require-schema-parse-at-boundary.ts
614
- import { AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
615
- var RULE_NAME7 = "require-schema-parse-at-boundary";
1083
+ import { AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
1084
+ var RULE_NAME8 = "require-schema-parse-at-boundary";
616
1085
  function isJsonParseCall(node) {
617
- return node.type === AST_NODE_TYPES7.CallExpression && node.callee.type === AST_NODE_TYPES7.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES7.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === AST_NODE_TYPES7.Identifier && node.callee.property.name === "parse";
1086
+ return node.type === AST_NODE_TYPES8.CallExpression && node.callee.type === AST_NODE_TYPES8.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES8.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === AST_NODE_TYPES8.Identifier && node.callee.property.name === "parse";
618
1087
  }
619
1088
  function isAwaitJsonCall(node) {
620
- if (node.type !== AST_NODE_TYPES7.AwaitExpression) {
1089
+ if (node.type !== AST_NODE_TYPES8.AwaitExpression) {
621
1090
  return false;
622
1091
  }
623
1092
  const call = node.argument;
624
- return call.type === AST_NODE_TYPES7.CallExpression && call.arguments.length === 0 && call.callee.type === AST_NODE_TYPES7.MemberExpression && !call.callee.computed && call.callee.property.type === AST_NODE_TYPES7.Identifier && call.callee.property.name === "json";
1093
+ return call.type === AST_NODE_TYPES8.CallExpression && call.arguments.length === 0 && call.callee.type === AST_NODE_TYPES8.MemberExpression && !call.callee.computed && call.callee.property.type === AST_NODE_TYPES8.Identifier && call.callee.property.name === "json";
625
1094
  }
626
1095
  function isShapeClaim(annotation) {
627
- if (annotation.type === AST_NODE_TYPES7.TSArrayType) {
1096
+ if (annotation.type === AST_NODE_TYPES8.TSArrayType) {
628
1097
  return true;
629
1098
  }
630
- if (annotation.type === AST_NODE_TYPES7.TSTypeReference) {
631
- return !(annotation.typeName.type === AST_NODE_TYPES7.Identifier && annotation.typeName.name === "const");
1099
+ if (annotation.type === AST_NODE_TYPES8.TSTypeReference) {
1100
+ return !(annotation.typeName.type === AST_NODE_TYPES8.Identifier && annotation.typeName.name === "const");
632
1101
  }
633
1102
  return false;
634
1103
  }
635
1104
  var requireSchemaParseAtBoundaryRule = createRule({
636
- name: RULE_NAME7,
1105
+ name: RULE_NAME8,
637
1106
  meta: {
638
1107
  type: "problem",
639
1108
  docs: {
@@ -661,10 +1130,10 @@ var requireSchemaParseAtBoundaryRule = createRule({
661
1130
  });
662
1131
 
663
1132
  // src/rules/restrict-throw-to-taxonomy.ts
664
- import { AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
665
- var RULE_NAME8 = "restrict-throw-to-taxonomy";
1133
+ import { AST_NODE_TYPES as AST_NODE_TYPES9 } from "@typescript-eslint/utils";
1134
+ var RULE_NAME9 = "restrict-throw-to-taxonomy";
666
1135
  var DEFAULT_ALLOW = ["Error"];
667
- var optionSchema6 = {
1136
+ var optionSchema7 = {
668
1137
  type: "object",
669
1138
  additionalProperties: false,
670
1139
  properties: {
@@ -677,25 +1146,25 @@ var optionSchema6 = {
677
1146
  };
678
1147
  function constructorSimpleName2(node) {
679
1148
  const callee = node.callee;
680
- if (callee.type === AST_NODE_TYPES8.Identifier) {
1149
+ if (callee.type === AST_NODE_TYPES9.Identifier) {
681
1150
  return callee.name;
682
1151
  }
683
- if (callee.type === AST_NODE_TYPES8.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES8.Identifier) {
1152
+ if (callee.type === AST_NODE_TYPES9.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES9.Identifier) {
684
1153
  return callee.property.name;
685
1154
  }
686
1155
  return null;
687
1156
  }
688
1157
  function isNonErrorValue(node) {
689
- return node.type === AST_NODE_TYPES8.Literal || node.type === AST_NODE_TYPES8.TemplateLiteral || node.type === AST_NODE_TYPES8.ObjectExpression || node.type === AST_NODE_TYPES8.ArrayExpression;
1158
+ return node.type === AST_NODE_TYPES9.Literal || node.type === AST_NODE_TYPES9.TemplateLiteral || node.type === AST_NODE_TYPES9.ObjectExpression || node.type === AST_NODE_TYPES9.ArrayExpression;
690
1159
  }
691
1160
  var restrictThrowToTaxonomyRule = createRule({
692
- name: RULE_NAME8,
1161
+ name: RULE_NAME9,
693
1162
  meta: {
694
1163
  type: "problem",
695
1164
  docs: {
696
1165
  description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
697
1166
  },
698
- schema: [optionSchema6],
1167
+ schema: [optionSchema7],
699
1168
  messages: {
700
1169
  disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
701
1170
  nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
@@ -708,7 +1177,7 @@ var restrictThrowToTaxonomyRule = createRule({
708
1177
  return {
709
1178
  ThrowStatement(node) {
710
1179
  const arg = node.argument;
711
- if (arg.type === AST_NODE_TYPES8.NewExpression) {
1180
+ if (arg.type === AST_NODE_TYPES9.NewExpression) {
712
1181
  const name = constructorSimpleName2(arg);
713
1182
  if (name !== null && !allow.has(name)) {
714
1183
  context.report({
@@ -720,7 +1189,7 @@ var restrictThrowToTaxonomyRule = createRule({
720
1189
  return;
721
1190
  }
722
1191
  if (isNonErrorValue(arg)) {
723
- const kind = arg.type === AST_NODE_TYPES8.ObjectExpression ? "object" : arg.type === AST_NODE_TYPES8.ArrayExpression ? "array" : "literal";
1192
+ const kind = arg.type === AST_NODE_TYPES9.ObjectExpression ? "object" : arg.type === AST_NODE_TYPES9.ArrayExpression ? "array" : "literal";
724
1193
  context.report({ node: arg, messageId: "nonErrorThrow", data: { kind } });
725
1194
  }
726
1195
  }
@@ -728,10 +1197,184 @@ var restrictThrowToTaxonomyRule = createRule({
728
1197
  }
729
1198
  });
730
1199
 
1200
+ // src/rules/schema-enum-field-consistency.ts
1201
+ import { AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
1202
+ var RULE_NAME10 = "schema-enum-field-consistency";
1203
+ var MODIFIERS = /* @__PURE__ */ new Set([
1204
+ "optional",
1205
+ "nullable",
1206
+ "nullish",
1207
+ "default",
1208
+ "prefault",
1209
+ "catch",
1210
+ "describe",
1211
+ "meta",
1212
+ "readonly"
1213
+ ]);
1214
+ var ENUM_PRESERVING = /* @__PURE__ */ new Set(["extract", "exclude"]);
1215
+ var OUTPUT_CHANGING = /* @__PURE__ */ new Set(["pipe", "transform"]);
1216
+ var OBJECT_FACTORIES = /* @__PURE__ */ new Set(["object", "strictObject", "looseObject"]);
1217
+ var SHAPE_EXTENDERS = /* @__PURE__ */ new Set(["extend", "safeExtend"]);
1218
+ var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
1219
+ var UNION = /* @__PURE__ */ new Set(["union"]);
1220
+ var LITERAL = /* @__PURE__ */ new Set(["literal"]);
1221
+ var STRING = /* @__PURE__ */ new Set(["string"]);
1222
+ var optionSchema8 = {
1223
+ type: "object",
1224
+ additionalProperties: false,
1225
+ properties: {
1226
+ zodIdentifiers: { type: "array", items: { type: "string" }, uniqueItems: true },
1227
+ ignoreFields: { type: "array", items: { type: "string" }, uniqueItems: true },
1228
+ enumIdentifierPattern: { type: "string" }
1229
+ }
1230
+ };
1231
+ var OTHER = { kind: "other" };
1232
+ function isEnumOccurrence(occurrence) {
1233
+ return occurrence.kind.kind === "enum";
1234
+ }
1235
+ function methodCall(node) {
1236
+ if (node.type !== AST_NODE_TYPES10.CallExpression) return null;
1237
+ const callee = node.callee;
1238
+ if (callee.type !== AST_NODE_TYPES10.MemberExpression || callee.computed) return null;
1239
+ if (callee.property.type !== AST_NODE_TYPES10.Identifier) return null;
1240
+ return { receiver: callee.object, method: callee.property.name, call: node };
1241
+ }
1242
+ function propertyName(property) {
1243
+ if (property.computed) return null;
1244
+ if (property.key.type === AST_NODE_TYPES10.Identifier) return property.key.name;
1245
+ if (property.key.type === AST_NODE_TYPES10.Literal && typeof property.key.value === "string") {
1246
+ return property.key.value;
1247
+ }
1248
+ return null;
1249
+ }
1250
+ var schemaEnumFieldConsistencyRule = createRule({
1251
+ name: RULE_NAME10,
1252
+ meta: {
1253
+ type: "problem",
1254
+ docs: {
1255
+ description: "Disallow a zod field that is an enum in one object schema of a module from being `z.string()` in another, which widens the wire type every consumer then narrows by hand."
1256
+ },
1257
+ schema: [optionSchema8],
1258
+ messages: {
1259
+ widenedEnumField: "`{{field}}` is `z.string()` here but an enum on line {{line}} of this file. The widened type leaks `string` to every consumer, which then has to narrow or cast it. Use {{suggestion}} instead (and, if the stored data is free text, migrate it first)."
1260
+ }
1261
+ },
1262
+ defaultOptions: [{ zodIdentifiers: ["z"], ignoreFields: [] }],
1263
+ create(context, [options]) {
1264
+ const zodIdentifiers = new Set(options.zodIdentifiers ?? ["z"]);
1265
+ const ignoreFields = new Set(options.ignoreFields ?? []);
1266
+ const enumIdentifierPattern = options.enumIdentifierPattern === void 0 ? null : new RegExp(options.enumIdentifierPattern, "u");
1267
+ const sourceCode = context.sourceCode;
1268
+ const fields = /* @__PURE__ */ new Map();
1269
+ function isZodCall(node, names) {
1270
+ const call = methodCall(node);
1271
+ return call !== null && call.receiver.type === AST_NODE_TYPES10.Identifier && zodIdentifiers.has(call.receiver.name) && names.has(call.method);
1272
+ }
1273
+ function resolveVariable(identifier) {
1274
+ let scope = sourceCode.getScope(identifier);
1275
+ while (scope !== null) {
1276
+ const variable = scope.set.get(identifier.name);
1277
+ if (variable !== void 0) return variable;
1278
+ scope = scope.upper;
1279
+ }
1280
+ return null;
1281
+ }
1282
+ function identifierIsEnum(identifier, seen) {
1283
+ const definition = resolveVariable(identifier)?.defs[0];
1284
+ if (definition === void 0) return false;
1285
+ if (definition.type === "ImportBinding") {
1286
+ return enumIdentifierPattern !== null && enumIdentifierPattern.test(identifier.name);
1287
+ }
1288
+ if (definition.type !== "Variable") return false;
1289
+ const init = definition.node.init;
1290
+ if (init === null || seen.has(init)) return false;
1291
+ return classify(init, /* @__PURE__ */ new Set([...seen, init])).kind === "enum";
1292
+ }
1293
+ function isLiteralUnion(node) {
1294
+ if (!isZodCall(node, UNION)) return false;
1295
+ const members = node.arguments[0];
1296
+ if (members?.type !== AST_NODE_TYPES10.ArrayExpression || members.elements.length === 0) {
1297
+ return false;
1298
+ }
1299
+ return members.elements.every((element) => element !== null && isZodCall(element, LITERAL));
1300
+ }
1301
+ function isMultiLiteral(node) {
1302
+ if (!isZodCall(node, LITERAL)) return false;
1303
+ const value = node.arguments[0];
1304
+ return value?.type === AST_NODE_TYPES10.ArrayExpression && value.elements.length > 1;
1305
+ }
1306
+ function classify(node, seen) {
1307
+ let current = node;
1308
+ for (; ; ) {
1309
+ const call = methodCall(current);
1310
+ if (call === null || !(MODIFIERS.has(call.method) || ENUM_PRESERVING.has(call.method))) {
1311
+ break;
1312
+ }
1313
+ current = call.receiver;
1314
+ }
1315
+ if (current.type === AST_NODE_TYPES10.Identifier) {
1316
+ return identifierIsEnum(current, seen) ? { kind: "enum", identifier: current.name } : OTHER;
1317
+ }
1318
+ if (isZodCall(current, ENUM_FACTORIES) || isLiteralUnion(current) || isMultiLiteral(current)) {
1319
+ return { kind: "enum", identifier: null };
1320
+ }
1321
+ current = node;
1322
+ for (; ; ) {
1323
+ if (isZodCall(current, STRING)) return { kind: "string" };
1324
+ const call = methodCall(current);
1325
+ if (call === null || OUTPUT_CHANGING.has(call.method)) return OTHER;
1326
+ current = call.receiver;
1327
+ }
1328
+ }
1329
+ function collectShape(shape) {
1330
+ if (shape?.type !== AST_NODE_TYPES10.ObjectExpression) return;
1331
+ for (const property of shape.properties) {
1332
+ if (property.type !== AST_NODE_TYPES10.Property) continue;
1333
+ const name = propertyName(property);
1334
+ if (name === null || ignoreFields.has(name)) continue;
1335
+ const kind = classify(property.value, /* @__PURE__ */ new Set());
1336
+ if (kind.kind === "other") continue;
1337
+ const occurrences = fields.get(name) ?? [];
1338
+ occurrences.push({ property, kind });
1339
+ fields.set(name, occurrences);
1340
+ }
1341
+ }
1342
+ return {
1343
+ CallExpression(node) {
1344
+ if (isZodCall(node, OBJECT_FACTORIES)) {
1345
+ collectShape(node.arguments[0]);
1346
+ return;
1347
+ }
1348
+ const call = methodCall(node);
1349
+ if (call !== null && SHAPE_EXTENDERS.has(call.method)) collectShape(node.arguments[0]);
1350
+ },
1351
+ "Program:exit"() {
1352
+ for (const [field, occurrences] of fields) {
1353
+ const enumOccurrence = occurrences.find(isEnumOccurrence);
1354
+ if (enumOccurrence === void 0) continue;
1355
+ const suggestion = enumOccurrence.kind.identifier === null ? "the same enum schema" : `\`${enumOccurrence.kind.identifier}\``;
1356
+ for (const occurrence of occurrences) {
1357
+ if (occurrence.kind.kind !== "string") continue;
1358
+ context.report({
1359
+ node: occurrence.property,
1360
+ messageId: "widenedEnumField",
1361
+ data: {
1362
+ field,
1363
+ line: String(enumOccurrence.property.loc.start.line),
1364
+ suggestion
1365
+ }
1366
+ });
1367
+ }
1368
+ }
1369
+ }
1370
+ };
1371
+ }
1372
+ });
1373
+
731
1374
  // src/rules/wire-message-naming.ts
732
- var RULE_NAME9 = "wire-message-naming";
1375
+ var RULE_NAME11 = "wire-message-naming";
733
1376
  var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
734
- var optionSchema7 = {
1377
+ var optionSchema9 = {
735
1378
  type: "object",
736
1379
  additionalProperties: false,
737
1380
  properties: {
@@ -772,14 +1415,14 @@ function typeLiteralNode(obj) {
772
1415
  return null;
773
1416
  }
774
1417
  var wireMessageNamingRule = createRule({
775
- name: RULE_NAME9,
1418
+ name: RULE_NAME11,
776
1419
  meta: {
777
1420
  type: "problem",
778
1421
  docs: {
779
1422
  description: "A message-schema const ending in a role suffix (default Event/Command/Query) whose zod object declares `type: z.literal(...)` must set that literal to kebab-case(const name minus its role suffix)."
780
1423
  },
781
1424
  fixable: "code",
782
- schema: [optionSchema7],
1425
+ schema: [optionSchema9],
783
1426
  messages: {
784
1427
  typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
785
1428
  }
@@ -815,11 +1458,11 @@ var wireMessageNamingRule = createRule({
815
1458
  });
816
1459
 
817
1460
  // src/rules/zod-schema-naming.ts
818
- var RULE_NAME10 = "zod-schema-naming";
1461
+ var RULE_NAME12 = "zod-schema-naming";
819
1462
  var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
820
1463
  var SUFFIX = "Schema";
821
1464
  var DEFAULT_ROLE_SUFFIXES2 = [];
822
- var optionSchema8 = {
1465
+ var optionSchema10 = {
823
1466
  type: "object",
824
1467
  additionalProperties: false,
825
1468
  properties: {
@@ -852,13 +1495,13 @@ function rootIdentifierName(node) {
852
1495
  return null;
853
1496
  }
854
1497
  var zodSchemaNamingRule = createRule({
855
- name: RULE_NAME10,
1498
+ name: RULE_NAME12,
856
1499
  meta: {
857
1500
  type: "problem",
858
1501
  docs: {
859
1502
  description: "Every exported zod schema is a PascalCase const suffixed `Schema`, paired with a same-named inferred type (`export type Foo = z.infer<typeof FooSchema>`)."
860
1503
  },
861
- schema: [optionSchema8],
1504
+ schema: [optionSchema10],
862
1505
  messages: {
863
1506
  schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
864
1507
  missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
@@ -919,12 +1562,14 @@ var rules = {
919
1562
  "restrict-throw-to-taxonomy": restrictThrowToTaxonomyRule,
920
1563
  "require-registered-keys": requireRegisteredKeysRule,
921
1564
  "env-var-schema-parity": envVarSchemaParityRule,
922
- "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule
1565
+ "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule,
1566
+ "schema-enum-field-consistency": schemaEnumFieldConsistencyRule,
1567
+ "fetch-must-check-ok": fetchMustCheckOkRule
923
1568
  };
924
1569
 
925
1570
  // src/index.ts
926
1571
  var NAMESPACE = "noctcore-contracts";
927
- var VERSION = "0.2.0";
1572
+ var VERSION = "0.3.0";
928
1573
  var plugin = {
929
1574
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
930
1575
  rules,