@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.cjs CHANGED
@@ -45,6 +45,8 @@ var recommended = {
45
45
  "noctcore-contracts/money-must-be-decimal": "error",
46
46
  "noctcore-contracts/require-error-cause": "error",
47
47
  "noctcore-contracts/restrict-throw-to-taxonomy": "error",
48
+ "noctcore-contracts/schema-enum-field-consistency": "error",
49
+ "noctcore-contracts/fetch-must-check-ok": "error",
48
50
  // Config-required / heuristic rules ship inert. `require-registered-keys` and
49
51
  // `env-var-schema-parity` do nothing until their `sinks` / `schema` options are
50
52
  // set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
@@ -151,9 +153,476 @@ var envVarSchemaParityRule = createRule({
151
153
  }
152
154
  });
153
155
 
154
- // src/rules/money-must-be-decimal.ts
156
+ // src/rules/fetch-must-check-ok.ts
155
157
  var import_utils2 = require("@typescript-eslint/utils");
156
- var RULE_NAME2 = "money-must-be-decimal";
158
+ var RULE_NAME2 = "fetch-must-check-ok";
159
+ var optionSchema2 = {
160
+ type: "object",
161
+ additionalProperties: false,
162
+ properties: {
163
+ fetchFunctions: {
164
+ type: "array",
165
+ items: { type: "string", minLength: 1 },
166
+ uniqueItems: true
167
+ }
168
+ }
169
+ };
170
+ function isNode(value) {
171
+ return typeof value === "object" && value !== null && "type" in value;
172
+ }
173
+ function walkSome(root, keys, predicate) {
174
+ const stack = [root];
175
+ for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
176
+ if (predicate(node)) {
177
+ return true;
178
+ }
179
+ for (const key of keys[node.type] ?? []) {
180
+ const value = Reflect.get(node, key);
181
+ if (Array.isArray(value)) {
182
+ for (const child of value) {
183
+ if (isNode(child)) {
184
+ stack.push(child);
185
+ }
186
+ }
187
+ } else if (isNode(value)) {
188
+ stack.push(value);
189
+ }
190
+ }
191
+ }
192
+ return false;
193
+ }
194
+ function calleePath(node) {
195
+ if (node.type === import_utils2.AST_NODE_TYPES.Identifier) {
196
+ return node.name;
197
+ }
198
+ if (node.type === import_utils2.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils2.AST_NODE_TYPES.Identifier) {
199
+ const object = calleePath(node.object);
200
+ return object === null ? null : `${object}.${node.property.name}`;
201
+ }
202
+ return null;
203
+ }
204
+ var OK_PROP = "ok";
205
+ var OK_PROPS = /* @__PURE__ */ new Set([OK_PROP, "status"]);
206
+ var ASSERTION_NAMES = /^(?:[Aa]ssert|[Ii]nvariant|[Ee]nsure|[Ee]xpect)(?:[A-Z_]\w*)?$/u;
207
+ var COMPARISONS = /* @__PURE__ */ new Set(["===", "!==", "==", "!=", "<", "<=", ">", ">="]);
208
+ var FIRST_ERROR_STATUS = 400;
209
+ function literalValue(node) {
210
+ if (node.type !== import_utils2.AST_NODE_TYPES.Literal) {
211
+ return void 0;
212
+ }
213
+ return typeof node.value === "number" || typeof node.value === "boolean" ? node.value : void 0;
214
+ }
215
+ function mirror(operator) {
216
+ switch (operator) {
217
+ case "<":
218
+ return ">";
219
+ case "<=":
220
+ return ">=";
221
+ case ">":
222
+ return "<";
223
+ case ">=":
224
+ return "<=";
225
+ default:
226
+ return operator;
227
+ }
228
+ }
229
+ function booleanPolarity(operator, value) {
230
+ if (operator === "===" || operator === "==") {
231
+ return value ? "positive" : "negative";
232
+ }
233
+ if (operator === "!==" || operator === "!=") {
234
+ return value ? "negative" : "positive";
235
+ }
236
+ return "opaque";
237
+ }
238
+ function statusPolarity(operator, value) {
239
+ const isSuccessCode = value >= 200 && value < 300;
240
+ switch (operator) {
241
+ case "===":
242
+ case "==":
243
+ return isSuccessCode ? "positive" : "opaque";
244
+ case "!==":
245
+ case "!=":
246
+ return isSuccessCode ? "negative" : "opaque";
247
+ case "<":
248
+ return value <= FIRST_ERROR_STATUS ? "positive" : "opaque";
249
+ case "<=":
250
+ return value < FIRST_ERROR_STATUS ? "positive" : "opaque";
251
+ // A failure test is only useful for what it says about the OTHER side, so
252
+ // what matters is that everything below the threshold is a success:
253
+ // `>= 300` and `>= 400` both leave only good responses behind, while
254
+ // `>= 500` leaves every 4xx there.
255
+ case ">=":
256
+ return value <= FIRST_ERROR_STATUS ? "negative" : "opaque";
257
+ case ">":
258
+ return value < FIRST_ERROR_STATUS ? "negative" : "opaque";
259
+ default:
260
+ return "opaque";
261
+ }
262
+ }
263
+ function comparisonPolarity(node, readIsLeft) {
264
+ const value = literalValue(readIsLeft ? node.right : node.left);
265
+ const operator = readIsLeft ? node.operator : mirror(node.operator);
266
+ if (typeof value === "boolean") {
267
+ return booleanPolarity(operator, value);
268
+ }
269
+ return typeof value === "number" ? statusPolarity(operator, value) : "opaque";
270
+ }
271
+ function propReadOn(node, objectName, props) {
272
+ if (node.type !== import_utils2.AST_NODE_TYPES.MemberExpression || node.computed) {
273
+ return false;
274
+ }
275
+ if (node.object.type !== import_utils2.AST_NODE_TYPES.Identifier || node.object.name !== objectName || node.property.type !== import_utils2.AST_NODE_TYPES.Identifier) {
276
+ return false;
277
+ }
278
+ const name = node.property.name;
279
+ return typeof props === "string" ? name === props : props.has(name);
280
+ }
281
+ function findJsonReads(root, keys, name) {
282
+ const reads = [];
283
+ walkSome(root, keys, (node) => {
284
+ if (propReadOn(node, name, "json")) {
285
+ reads.push(node);
286
+ }
287
+ return false;
288
+ });
289
+ return reads;
290
+ }
291
+ function isAssertionName(node) {
292
+ return node.type === import_utils2.AST_NODE_TYPES.Identifier && ASSERTION_NAMES.test(node.name);
293
+ }
294
+ function isAssertionCallee(callee) {
295
+ if (callee.type === import_utils2.AST_NODE_TYPES.Identifier) {
296
+ return isAssertionName(callee);
297
+ }
298
+ return callee.type === import_utils2.AST_NODE_TYPES.MemberExpression && !callee.computed && (isAssertionName(callee.object) || isAssertionName(callee.property));
299
+ }
300
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
301
+ import_utils2.AST_NODE_TYPES.IfStatement,
302
+ import_utils2.AST_NODE_TYPES.WhileStatement,
303
+ import_utils2.AST_NODE_TYPES.DoWhileStatement,
304
+ import_utils2.AST_NODE_TYPES.ConditionalExpression,
305
+ import_utils2.AST_NODE_TYPES.SwitchStatement,
306
+ import_utils2.AST_NODE_TYPES.CallExpression
307
+ ]);
308
+ var ASSERTION_OPERATORS = /* @__PURE__ */ new Map([
309
+ ["equal", "==="],
310
+ ["equals", "==="],
311
+ ["strictEqual", "==="],
312
+ ["deepEqual", "==="],
313
+ ["deepStrictEqual", "==="],
314
+ ["toBe", "==="],
315
+ ["toEqual", "==="],
316
+ ["notEqual", "!=="],
317
+ ["notStrictEqual", "!=="],
318
+ ["notDeepEqual", "!=="]
319
+ ]);
320
+ function assertionOperator(callee) {
321
+ return callee.type === import_utils2.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils2.AST_NODE_TYPES.Identifier ? ASSERTION_OPERATORS.get(callee.property.name) : void 0;
322
+ }
323
+ function assertionCheck(parent, child, check) {
324
+ if (check.compared) {
325
+ return check;
326
+ }
327
+ const operator = assertionOperator(parent.callee);
328
+ if (operator === void 0) {
329
+ return check;
330
+ }
331
+ const other = parent.arguments.find((arg) => arg !== child);
332
+ const value = other === void 0 ? void 0 : literalValue(other);
333
+ if (typeof value === "boolean") {
334
+ return {
335
+ ...check,
336
+ compared: true,
337
+ polarity: booleanPolarity(operator, value)
338
+ };
339
+ }
340
+ return typeof value === "number" ? { ...check, compared: true, polarity: statusPolarity(operator, value) } : check;
341
+ }
342
+ function terminalCheck(parent, child, state) {
343
+ const check = { owner: parent, ...state };
344
+ switch (parent.type) {
345
+ case import_utils2.AST_NODE_TYPES.IfStatement:
346
+ case import_utils2.AST_NODE_TYPES.WhileStatement:
347
+ case import_utils2.AST_NODE_TYPES.DoWhileStatement:
348
+ case import_utils2.AST_NODE_TYPES.ConditionalExpression:
349
+ return parent.test === child ? check : null;
350
+ case import_utils2.AST_NODE_TYPES.SwitchStatement:
351
+ return parent.discriminant === child ? { ...check, compared: true, polarity: "opaque" } : null;
352
+ // An assertion settles a status only when it COMPARES it.
353
+ // `assert.equal(res.status, 200)` does; `assert.ok(res.status)` asserts a
354
+ // number is truthy, which every response that arrived satisfies. Arity
355
+ // cannot tell them apart (`assert.ok(res.status, 'message')` also has two
356
+ // arguments), so the comparison is read from the assertion's own name.
357
+ case import_utils2.AST_NODE_TYPES.CallExpression:
358
+ return isAssertionCallee(parent.callee) && parent.callee !== child ? assertionCheck(parent, child, check) : null;
359
+ default:
360
+ return null;
361
+ }
362
+ }
363
+ function combinatorStep(parent, child, state, parse) {
364
+ switch (parent.type) {
365
+ case import_utils2.AST_NODE_TYPES.UnaryExpression:
366
+ if (parent.operator === "typeof") {
367
+ return "stop";
368
+ }
369
+ if (parent.operator === "!") {
370
+ state.polarity = state.polarity === "positive" ? "negative" : "positive";
371
+ }
372
+ return "continue";
373
+ case import_utils2.AST_NODE_TYPES.BinaryExpression: {
374
+ if (!COMPARISONS.has(parent.operator)) {
375
+ return "stop";
376
+ }
377
+ const polarity = comparisonPolarity(parent, parent.left === child);
378
+ state.compared = true;
379
+ state.polarity = polarity;
380
+ return polarity === "opaque" ? "stop" : "continue";
381
+ }
382
+ case import_utils2.AST_NODE_TYPES.LogicalExpression:
383
+ if (parent.operator === "&&") {
384
+ state.underAnd = true;
385
+ }
386
+ if (parent.operator === "||") {
387
+ state.underOr = true;
388
+ }
389
+ return parent.left === child && contains(parent.right, parse) ? { owner: parent, ...state } : "continue";
390
+ case import_utils2.AST_NODE_TYPES.ChainExpression:
391
+ case import_utils2.AST_NODE_TYPES.TSNonNullExpression:
392
+ return "continue";
393
+ default:
394
+ return "stop";
395
+ }
396
+ }
397
+ function climb(read, parse) {
398
+ const state = {
399
+ polarity: "positive",
400
+ compared: false,
401
+ underAnd: false,
402
+ underOr: false
403
+ };
404
+ let child = read;
405
+ let parent = read.parent;
406
+ while (parent !== void 0) {
407
+ if (TERMINAL_TYPES.has(parent.type)) {
408
+ return terminalCheck(parent, child, state);
409
+ }
410
+ const step = combinatorStep(parent, child, state, parse);
411
+ if (step === "stop") {
412
+ return null;
413
+ }
414
+ if (step !== "continue") {
415
+ return step;
416
+ }
417
+ child = parent;
418
+ parent = parent.parent;
419
+ }
420
+ return null;
421
+ }
422
+ function contains(outer, inner) {
423
+ return outer.range[0] <= inner.range[0] && outer.range[1] >= inner.range[1];
424
+ }
425
+ function alwaysExits(node) {
426
+ if (node.type === import_utils2.AST_NODE_TYPES.ReturnStatement || node.type === import_utils2.AST_NODE_TYPES.ThrowStatement) {
427
+ return true;
428
+ }
429
+ return node.type === import_utils2.AST_NODE_TYPES.BlockStatement && node.body.some((stmt) => alwaysExits(stmt));
430
+ }
431
+ function scopeOfCheck(node) {
432
+ let current = node;
433
+ while (current.parent !== void 0 && !current.type.endsWith("Statement")) {
434
+ current = current.parent;
435
+ }
436
+ return current.parent ?? current;
437
+ }
438
+ function isSuccessCase(arm) {
439
+ if (arm.test === null) {
440
+ return false;
441
+ }
442
+ const value = literalValue(arm.test);
443
+ return typeof value === "number" && value >= 200 && value < 300;
444
+ }
445
+ function switchArmProtects(owner, parse) {
446
+ const index = owner.cases.findIndex((arm) => contains(arm, parse));
447
+ const own = owner.cases[index];
448
+ if (own === void 0 || !isSuccessCase(own)) {
449
+ return false;
450
+ }
451
+ for (let i = index - 1; i >= 0; i -= 1) {
452
+ const arm = owner.cases[i];
453
+ if (arm === void 0 || arm.consequent.length > 0) {
454
+ break;
455
+ }
456
+ if (!isSuccessCase(arm)) {
457
+ return false;
458
+ }
459
+ }
460
+ return true;
461
+ }
462
+ function branchProtects(check, parse) {
463
+ const { owner } = check;
464
+ if (owner.type === import_utils2.AST_NODE_TYPES.SwitchStatement) {
465
+ return switchArmProtects(owner, parse);
466
+ }
467
+ if (owner.type === import_utils2.AST_NODE_TYPES.LogicalExpression) {
468
+ if (!contains(owner.right, parse)) {
469
+ return false;
470
+ }
471
+ if (owner.operator === "&&") {
472
+ return check.polarity === "positive";
473
+ }
474
+ return owner.operator === "||" && check.polarity === "negative";
475
+ }
476
+ if (owner.type === import_utils2.AST_NODE_TYPES.WhileStatement || owner.type === import_utils2.AST_NODE_TYPES.DoWhileStatement) {
477
+ return contains(owner.body, parse) && entersOnSuccess(check);
478
+ }
479
+ if (owner.type !== import_utils2.AST_NODE_TYPES.ConditionalExpression && owner.type !== import_utils2.AST_NODE_TYPES.IfStatement) {
480
+ return false;
481
+ }
482
+ if (contains(owner.consequent, parse)) {
483
+ return entersOnSuccess(check);
484
+ }
485
+ return owner.alternate !== null && contains(owner.alternate, parse) && skipsOnSuccess(check);
486
+ }
487
+ function entersOnSuccess(check) {
488
+ return check.polarity === "positive" && !check.underOr;
489
+ }
490
+ function skipsOnSuccess(check) {
491
+ return check.polarity === "negative" && !check.underAnd;
492
+ }
493
+ function guardProtects(check, parse) {
494
+ const { owner } = check;
495
+ if (owner.type === import_utils2.AST_NODE_TYPES.CallExpression) {
496
+ return entersOnSuccess(check) && owner.range[1] <= parse.range[0] && contains(scopeOfCheck(owner), parse);
497
+ }
498
+ if (owner.type !== import_utils2.AST_NODE_TYPES.IfStatement) {
499
+ return false;
500
+ }
501
+ if (owner.range[1] > parse.range[0] || !contains(scopeOfCheck(owner), parse)) {
502
+ return false;
503
+ }
504
+ if (alwaysExits(owner.consequent)) {
505
+ return skipsOnSuccess(check);
506
+ }
507
+ return owner.alternate !== null && alwaysExits(owner.alternate) && entersOnSuccess(check);
508
+ }
509
+ function protects(check, parse) {
510
+ if (!check.compared) {
511
+ return false;
512
+ }
513
+ return branchProtects(check, parse) || guardProtects(check, parse);
514
+ }
515
+ function statusAliases(root, keys, name) {
516
+ const aliases = /* @__PURE__ */ new Map();
517
+ walkSome(root, keys, (node) => {
518
+ if (node.type === import_utils2.AST_NODE_TYPES.VariableDeclarator && node.id.type === import_utils2.AST_NODE_TYPES.Identifier && node.init !== null && propReadOn(node.init, name, OK_PROPS) && node.init.property.type === import_utils2.AST_NODE_TYPES.Identifier) {
519
+ aliases.set(node.id.name, node.init.property.name);
520
+ }
521
+ return false;
522
+ });
523
+ return aliases;
524
+ }
525
+ function checkedProp(node, name, aliases) {
526
+ if (propReadOn(node, name, OK_PROPS)) {
527
+ return node.property.type === import_utils2.AST_NODE_TYPES.Identifier ? node.property.name : void 0;
528
+ }
529
+ return node.type === import_utils2.AST_NODE_TYPES.Identifier ? aliases.get(node.name) : void 0;
530
+ }
531
+ function isProtected(root, keys, name, aliases, parse) {
532
+ return walkSome(root, keys, (node) => {
533
+ const prop = checkedProp(node, name, aliases);
534
+ if (prop === void 0) {
535
+ return false;
536
+ }
537
+ const check = climb(node, parse);
538
+ if (check === null) {
539
+ return false;
540
+ }
541
+ return protects(
542
+ prop === OK_PROP ? { ...check, compared: true } : check,
543
+ parse
544
+ );
545
+ });
546
+ }
547
+ function scopeOf(node) {
548
+ let current = node.parent;
549
+ while (current !== void 0) {
550
+ if (current.type === import_utils2.AST_NODE_TYPES.BlockStatement || current.type === import_utils2.AST_NODE_TYPES.Program) {
551
+ return current;
552
+ }
553
+ current = current.parent;
554
+ }
555
+ return node;
556
+ }
557
+ function skipAwait(node) {
558
+ return node?.type === import_utils2.AST_NODE_TYPES.AwaitExpression ? node.parent : node;
559
+ }
560
+ function thenCallbackParam(node) {
561
+ if (node.type !== import_utils2.AST_NODE_TYPES.MemberExpression || node.computed || node.property.type !== import_utils2.AST_NODE_TYPES.Identifier || node.property.name !== "then" || node.parent.type !== import_utils2.AST_NODE_TYPES.CallExpression) {
562
+ return null;
563
+ }
564
+ const callback = node.parent.arguments[0];
565
+ if (callback === void 0 || callback.type !== import_utils2.AST_NODE_TYPES.ArrowFunctionExpression && callback.type !== import_utils2.AST_NODE_TYPES.FunctionExpression) {
566
+ return null;
567
+ }
568
+ const param = callback.params[0];
569
+ return param?.type === import_utils2.AST_NODE_TYPES.Identifier ? { name: param.name, body: callback.body } : null;
570
+ }
571
+ var fetchMustCheckOkRule = createRule({
572
+ name: RULE_NAME2,
573
+ meta: {
574
+ type: "problem",
575
+ docs: {
576
+ description: "Require a fetch response to be checked with `.ok` or a status comparison before `.json()` parses its body."
577
+ },
578
+ schema: [optionSchema2],
579
+ messages: {
580
+ 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."
581
+ }
582
+ },
583
+ defaultOptions: [{ fetchFunctions: ["fetch"] }],
584
+ create(context, [options]) {
585
+ const fetchFunctions = new Set(options.fetchFunctions ?? ["fetch"]);
586
+ const keys = context.sourceCode.visitorKeys;
587
+ function reportUnprotected(root, name) {
588
+ const aliases = statusAliases(root, keys, name);
589
+ for (const read of findJsonReads(root, keys, name)) {
590
+ if (!isProtected(root, keys, name, aliases, read)) {
591
+ context.report({ node: read, messageId: "missingOkCheck" });
592
+ }
593
+ }
594
+ }
595
+ return {
596
+ CallExpression(node) {
597
+ const path2 = calleePath(node.callee);
598
+ if (path2 === null || !fetchFunctions.has(path2)) {
599
+ return;
600
+ }
601
+ const parent = skipAwait(node.parent);
602
+ if (parent === void 0) {
603
+ return;
604
+ }
605
+ if (parent.type === import_utils2.AST_NODE_TYPES.MemberExpression && !parent.computed && parent.property.type === import_utils2.AST_NODE_TYPES.Identifier && parent.property.name === "json") {
606
+ context.report({ node: parent, messageId: "missingOkCheck" });
607
+ return;
608
+ }
609
+ const callback = thenCallbackParam(parent);
610
+ if (callback !== null) {
611
+ reportUnprotected(callback.body, callback.name);
612
+ return;
613
+ }
614
+ if (parent.type !== import_utils2.AST_NODE_TYPES.VariableDeclarator || parent.id.type !== import_utils2.AST_NODE_TYPES.Identifier) {
615
+ return;
616
+ }
617
+ reportUnprotected(scopeOf(parent), parent.id.name);
618
+ }
619
+ };
620
+ }
621
+ });
622
+
623
+ // src/rules/money-must-be-decimal.ts
624
+ var import_utils3 = require("@typescript-eslint/utils");
625
+ var RULE_NAME3 = "money-must-be-decimal";
157
626
  var DEFAULT_DECIMAL_TYPE = "Decimal";
158
627
  var DEFAULT_FIELD_PATTERNS = [
159
628
  "amount",
@@ -163,7 +632,7 @@ var DEFAULT_FIELD_PATTERNS = [
163
632
  "balance"
164
633
  ];
165
634
  var DEFAULT_ALLOWED_FILES = [];
166
- var optionSchema2 = {
635
+ var optionSchema3 = {
167
636
  type: "object",
168
637
  additionalProperties: false,
169
638
  properties: {
@@ -192,22 +661,22 @@ function isAllowedFile(filename, patterns) {
192
661
  return patterns.some((pattern) => normalized.endsWith(toForwardSlash(pattern)));
193
662
  }
194
663
  function staticName(node) {
195
- if (node.type === import_utils2.AST_NODE_TYPES.Identifier) {
664
+ if (node.type === import_utils3.AST_NODE_TYPES.Identifier) {
196
665
  return node.name;
197
666
  }
198
667
  return void 0;
199
668
  }
200
669
  function isNumberAnnotation(annotation) {
201
- return annotation?.typeAnnotation.type === import_utils2.AST_NODE_TYPES.TSNumberKeyword;
670
+ return annotation?.typeAnnotation.type === import_utils3.AST_NODE_TYPES.TSNumberKeyword;
202
671
  }
203
672
  var moneyMustBeDecimalRule = createRule({
204
- name: RULE_NAME2,
673
+ name: RULE_NAME3,
205
674
  meta: {
206
675
  type: "problem",
207
676
  docs: {
208
677
  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."
209
678
  },
210
- schema: [optionSchema2],
679
+ schema: [optionSchema3],
211
680
  messages: {
212
681
  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."
213
682
  }
@@ -243,7 +712,7 @@ var moneyMustBeDecimalRule = createRule({
243
712
  },
244
713
  // `const total: number = ...`: annotated variable declarator.
245
714
  VariableDeclarator(node) {
246
- if (node.id.type !== import_utils2.AST_NODE_TYPES.Identifier) {
715
+ if (node.id.type !== import_utils3.AST_NODE_TYPES.Identifier) {
247
716
  return;
248
717
  }
249
718
  const name = node.id.name;
@@ -256,15 +725,15 @@ var moneyMustBeDecimalRule = createRule({
256
725
  });
257
726
 
258
727
  // src/rules/no-direct-process-env.ts
259
- var import_utils3 = require("@typescript-eslint/utils");
260
- var RULE_NAME3 = "no-direct-process-env";
728
+ var import_utils4 = require("@typescript-eslint/utils");
729
+ var RULE_NAME4 = "no-direct-process-env";
261
730
  var DEFAULT_CONFIG_MODULE = "@/config";
262
731
  var DEFAULT_ALLOWED_FILES2 = [
263
732
  "**/*.config.{ts,js,mjs,cjs}",
264
733
  "**/*.{spec,test}.{ts,tsx}",
265
734
  "**/scripts/**"
266
735
  ];
267
- var optionSchema3 = {
736
+ var optionSchema4 = {
268
737
  type: "object",
269
738
  additionalProperties: false,
270
739
  properties: {
@@ -319,22 +788,22 @@ function isAllowedFile2(filename, patterns) {
319
788
  return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
320
789
  }
321
790
  function isProcessEnv2(node) {
322
- if (node.type !== import_utils3.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils3.AST_NODE_TYPES.Identifier || node.object.name !== "process") {
791
+ if (node.type !== import_utils4.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils4.AST_NODE_TYPES.Identifier || node.object.name !== "process") {
323
792
  return false;
324
793
  }
325
794
  if (node.computed) {
326
- return node.property.type === import_utils3.AST_NODE_TYPES.Literal && node.property.value === "env";
795
+ return node.property.type === import_utils4.AST_NODE_TYPES.Literal && node.property.value === "env";
327
796
  }
328
- return node.property.type === import_utils3.AST_NODE_TYPES.Identifier && node.property.name === "env";
797
+ return node.property.type === import_utils4.AST_NODE_TYPES.Identifier && node.property.name === "env";
329
798
  }
330
799
  var noDirectProcessEnvRule = createRule({
331
- name: RULE_NAME3,
800
+ name: RULE_NAME4,
332
801
  meta: {
333
802
  type: "problem",
334
803
  docs: {
335
804
  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."
336
805
  },
337
- schema: [optionSchema3],
806
+ schema: [optionSchema4],
338
807
  messages: {
339
808
  directProcessEnv: "Read environment variables through your typed config accessor (import from `{{configModule}}`). Direct `process.env` access bypasses boot-time validation."
340
809
  }
@@ -369,10 +838,10 @@ var noDirectProcessEnvRule = createRule({
369
838
  });
370
839
 
371
840
  // src/rules/no-error-stringify.ts
372
- var import_utils4 = require("@typescript-eslint/utils");
373
- var RULE_NAME4 = "no-error-stringify";
841
+ var import_utils5 = require("@typescript-eslint/utils");
842
+ var RULE_NAME5 = "no-error-stringify";
374
843
  var DEFAULT_ERROR_NAMES = ["error", "err", "e", "cause"];
375
- var optionSchema4 = {
844
+ var optionSchema5 = {
376
845
  type: "object",
377
846
  additionalProperties: false,
378
847
  properties: {
@@ -385,19 +854,19 @@ var optionSchema4 = {
385
854
  }
386
855
  };
387
856
  function isEmptyStringLiteral(node) {
388
- return node.type === import_utils4.AST_NODE_TYPES.Literal && node.value === "";
857
+ return node.type === import_utils5.AST_NODE_TYPES.Literal && node.value === "";
389
858
  }
390
859
  function isErrorIdentifier(node, names) {
391
- return node.type === import_utils4.AST_NODE_TYPES.Identifier && names.has(node.name);
860
+ return node.type === import_utils5.AST_NODE_TYPES.Identifier && names.has(node.name);
392
861
  }
393
862
  var noErrorStringifyRule = createRule({
394
- name: RULE_NAME4,
863
+ name: RULE_NAME5,
395
864
  meta: {
396
865
  type: "problem",
397
866
  docs: {
398
867
  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.'
399
868
  },
400
- schema: [optionSchema4],
869
+ schema: [optionSchema5],
401
870
  messages: {
402
871
  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)."
403
872
  }
@@ -412,7 +881,7 @@ var noErrorStringifyRule = createRule({
412
881
  // `error.toString()`
413
882
  'CallExpression[callee.type="MemberExpression"]'(node) {
414
883
  const callee = node.callee;
415
- if (!callee.computed && callee.property.type === import_utils4.AST_NODE_TYPES.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
884
+ if (!callee.computed && callee.property.type === import_utils5.AST_NODE_TYPES.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
416
885
  report(node, callee.object.name);
417
886
  }
418
887
  },
@@ -445,14 +914,14 @@ var noErrorStringifyRule = createRule({
445
914
  });
446
915
 
447
916
  // src/rules/require-error-cause.ts
448
- var import_utils5 = require("@typescript-eslint/utils");
449
- var RULE_NAME5 = "require-error-cause";
917
+ var import_utils6 = require("@typescript-eslint/utils");
918
+ var RULE_NAME6 = "require-error-cause";
450
919
  function constructorSimpleName(node) {
451
920
  const callee = node.callee;
452
- if (callee.type === import_utils5.AST_NODE_TYPES.Identifier) {
921
+ if (callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
453
922
  return callee.name;
454
923
  }
455
- if (callee.type === import_utils5.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils5.AST_NODE_TYPES.Identifier) {
924
+ if (callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils6.AST_NODE_TYPES.Identifier) {
456
925
  return callee.property.name;
457
926
  }
458
927
  return null;
@@ -462,16 +931,16 @@ function isErrorLikeName(name) {
462
931
  }
463
932
  function alreadyHasCause(node) {
464
933
  for (const arg of node.arguments) {
465
- if (arg.type === import_utils5.AST_NODE_TYPES.SpreadElement) {
934
+ if (arg.type === import_utils6.AST_NODE_TYPES.SpreadElement) {
466
935
  return true;
467
936
  }
468
- if (arg.type === import_utils5.AST_NODE_TYPES.ObjectExpression) {
937
+ if (arg.type === import_utils6.AST_NODE_TYPES.ObjectExpression) {
469
938
  for (const prop of arg.properties) {
470
- if (prop.type === import_utils5.AST_NODE_TYPES.SpreadElement) {
939
+ if (prop.type === import_utils6.AST_NODE_TYPES.SpreadElement) {
471
940
  return true;
472
941
  }
473
942
  const key = prop.key;
474
- const isCause = key.type === import_utils5.AST_NODE_TYPES.Identifier && key.name === "cause" || key.type === import_utils5.AST_NODE_TYPES.Literal && key.value === "cause";
943
+ const isCause = key.type === import_utils6.AST_NODE_TYPES.Identifier && key.name === "cause" || key.type === import_utils6.AST_NODE_TYPES.Literal && key.value === "cause";
475
944
  if (isCause) {
476
945
  return true;
477
946
  }
@@ -489,7 +958,7 @@ function buildFix(node, binding) {
489
958
  if (last === void 0) {
490
959
  return null;
491
960
  }
492
- if (last.type === import_utils5.AST_NODE_TYPES.ObjectExpression) {
961
+ if (last.type === import_utils6.AST_NODE_TYPES.ObjectExpression) {
493
962
  const props = last.properties;
494
963
  if (props.length === 0) {
495
964
  return (fixer) => fixer.replaceText(last, `{ cause: ${binding} }`);
@@ -503,7 +972,7 @@ function buildFix(node, binding) {
503
972
  return (fixer) => fixer.insertTextAfter(last, `, { cause: ${binding} }`);
504
973
  }
505
974
  var requireErrorCauseRule = createRule({
506
- name: RULE_NAME5,
975
+ name: RULE_NAME6,
507
976
  meta: {
508
977
  type: "problem",
509
978
  docs: {
@@ -522,7 +991,7 @@ var requireErrorCauseRule = createRule({
522
991
  CatchClause(node) {
523
992
  const param = node.param;
524
993
  catchBindings.push(
525
- param && param.type === import_utils5.AST_NODE_TYPES.Identifier ? param.name : null
994
+ param && param.type === import_utils6.AST_NODE_TYPES.Identifier ? param.name : null
526
995
  );
527
996
  },
528
997
  "CatchClause:exit"() {
@@ -534,7 +1003,7 @@ var requireErrorCauseRule = createRule({
534
1003
  return;
535
1004
  }
536
1005
  const arg = node.argument;
537
- if (arg.type !== import_utils5.AST_NODE_TYPES.NewExpression) {
1006
+ if (arg.type !== import_utils6.AST_NODE_TYPES.NewExpression) {
538
1007
  return;
539
1008
  }
540
1009
  const ctor = constructorSimpleName(arg);
@@ -557,9 +1026,9 @@ var requireErrorCauseRule = createRule({
557
1026
  });
558
1027
 
559
1028
  // src/rules/require-registered-keys.ts
560
- var import_utils6 = require("@typescript-eslint/utils");
561
- var RULE_NAME6 = "require-registered-keys";
562
- var optionSchema5 = {
1029
+ var import_utils7 = require("@typescript-eslint/utils");
1030
+ var RULE_NAME7 = "require-registered-keys";
1031
+ var optionSchema6 = {
563
1032
  type: "object",
564
1033
  additionalProperties: false,
565
1034
  properties: {
@@ -578,30 +1047,30 @@ var optionSchema5 = {
578
1047
  registry: { type: "string", minLength: 1 }
579
1048
  }
580
1049
  };
581
- function calleePath(callee) {
582
- if (callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
1050
+ function calleePath2(callee) {
1051
+ if (callee.type === import_utils7.AST_NODE_TYPES.Identifier) {
583
1052
  return callee.name;
584
1053
  }
585
- if (callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && !callee.computed) {
586
- if (callee.property.type !== import_utils6.AST_NODE_TYPES.Identifier) {
1054
+ if (callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && !callee.computed) {
1055
+ if (callee.property.type !== import_utils7.AST_NODE_TYPES.Identifier) {
587
1056
  return null;
588
1057
  }
589
- const objectPath = calleePath(callee.object);
1058
+ const objectPath = calleePath2(callee.object);
590
1059
  return objectPath === null ? null : `${objectPath}.${callee.property.name}`;
591
1060
  }
592
1061
  return null;
593
1062
  }
594
1063
  function isStringLiteral(node) {
595
- return node.type === import_utils6.AST_NODE_TYPES.Literal && typeof node.value === "string";
1064
+ return node.type === import_utils7.AST_NODE_TYPES.Literal && typeof node.value === "string";
596
1065
  }
597
1066
  var requireRegisteredKeysRule = createRule({
598
- name: RULE_NAME6,
1067
+ name: RULE_NAME7,
599
1068
  meta: {
600
1069
  type: "suggestion",
601
1070
  docs: {
602
1071
  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."
603
1072
  },
604
- schema: [optionSchema5],
1073
+ schema: [optionSchema6],
605
1074
  messages: {
606
1075
  unregisteredKey: "Pass an imported key constant to `{{callee}}`, not the raw string {{value}}{{registryHint}}. Raw string keys drift out of sync across call sites."
607
1076
  }
@@ -625,7 +1094,7 @@ var requireRegisteredKeysRule = createRule({
625
1094
  const registryHint = registry ? ` (import it from '${registry}')` : "";
626
1095
  return {
627
1096
  CallExpression(node) {
628
- const path2 = calleePath(node.callee);
1097
+ const path2 = calleePath2(node.callee);
629
1098
  if (path2 === null) {
630
1099
  return;
631
1100
  }
@@ -649,29 +1118,29 @@ var requireRegisteredKeysRule = createRule({
649
1118
  });
650
1119
 
651
1120
  // src/rules/require-schema-parse-at-boundary.ts
652
- var import_utils7 = require("@typescript-eslint/utils");
653
- var RULE_NAME7 = "require-schema-parse-at-boundary";
1121
+ var import_utils8 = require("@typescript-eslint/utils");
1122
+ var RULE_NAME8 = "require-schema-parse-at-boundary";
654
1123
  function isJsonParseCall(node) {
655
- return node.type === import_utils7.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils7.AST_NODE_TYPES.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === import_utils7.AST_NODE_TYPES.Identifier && node.callee.property.name === "parse";
1124
+ return node.type === import_utils8.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils8.AST_NODE_TYPES.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier && node.callee.property.name === "parse";
656
1125
  }
657
1126
  function isAwaitJsonCall(node) {
658
- if (node.type !== import_utils7.AST_NODE_TYPES.AwaitExpression) {
1127
+ if (node.type !== import_utils8.AST_NODE_TYPES.AwaitExpression) {
659
1128
  return false;
660
1129
  }
661
1130
  const call = node.argument;
662
- return call.type === import_utils7.AST_NODE_TYPES.CallExpression && call.arguments.length === 0 && call.callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && !call.callee.computed && call.callee.property.type === import_utils7.AST_NODE_TYPES.Identifier && call.callee.property.name === "json";
1131
+ return call.type === import_utils8.AST_NODE_TYPES.CallExpression && call.arguments.length === 0 && call.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !call.callee.computed && call.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier && call.callee.property.name === "json";
663
1132
  }
664
1133
  function isShapeClaim(annotation) {
665
- if (annotation.type === import_utils7.AST_NODE_TYPES.TSArrayType) {
1134
+ if (annotation.type === import_utils8.AST_NODE_TYPES.TSArrayType) {
666
1135
  return true;
667
1136
  }
668
- if (annotation.type === import_utils7.AST_NODE_TYPES.TSTypeReference) {
669
- return !(annotation.typeName.type === import_utils7.AST_NODE_TYPES.Identifier && annotation.typeName.name === "const");
1137
+ if (annotation.type === import_utils8.AST_NODE_TYPES.TSTypeReference) {
1138
+ return !(annotation.typeName.type === import_utils8.AST_NODE_TYPES.Identifier && annotation.typeName.name === "const");
670
1139
  }
671
1140
  return false;
672
1141
  }
673
1142
  var requireSchemaParseAtBoundaryRule = createRule({
674
- name: RULE_NAME7,
1143
+ name: RULE_NAME8,
675
1144
  meta: {
676
1145
  type: "problem",
677
1146
  docs: {
@@ -699,10 +1168,10 @@ var requireSchemaParseAtBoundaryRule = createRule({
699
1168
  });
700
1169
 
701
1170
  // src/rules/restrict-throw-to-taxonomy.ts
702
- var import_utils8 = require("@typescript-eslint/utils");
703
- var RULE_NAME8 = "restrict-throw-to-taxonomy";
1171
+ var import_utils9 = require("@typescript-eslint/utils");
1172
+ var RULE_NAME9 = "restrict-throw-to-taxonomy";
704
1173
  var DEFAULT_ALLOW = ["Error"];
705
- var optionSchema6 = {
1174
+ var optionSchema7 = {
706
1175
  type: "object",
707
1176
  additionalProperties: false,
708
1177
  properties: {
@@ -715,25 +1184,25 @@ var optionSchema6 = {
715
1184
  };
716
1185
  function constructorSimpleName2(node) {
717
1186
  const callee = node.callee;
718
- if (callee.type === import_utils8.AST_NODE_TYPES.Identifier) {
1187
+ if (callee.type === import_utils9.AST_NODE_TYPES.Identifier) {
719
1188
  return callee.name;
720
1189
  }
721
- if (callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils8.AST_NODE_TYPES.Identifier) {
1190
+ if (callee.type === import_utils9.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils9.AST_NODE_TYPES.Identifier) {
722
1191
  return callee.property.name;
723
1192
  }
724
1193
  return null;
725
1194
  }
726
1195
  function isNonErrorValue(node) {
727
- return node.type === import_utils8.AST_NODE_TYPES.Literal || node.type === import_utils8.AST_NODE_TYPES.TemplateLiteral || node.type === import_utils8.AST_NODE_TYPES.ObjectExpression || node.type === import_utils8.AST_NODE_TYPES.ArrayExpression;
1196
+ return node.type === import_utils9.AST_NODE_TYPES.Literal || node.type === import_utils9.AST_NODE_TYPES.TemplateLiteral || node.type === import_utils9.AST_NODE_TYPES.ObjectExpression || node.type === import_utils9.AST_NODE_TYPES.ArrayExpression;
728
1197
  }
729
1198
  var restrictThrowToTaxonomyRule = createRule({
730
- name: RULE_NAME8,
1199
+ name: RULE_NAME9,
731
1200
  meta: {
732
1201
  type: "problem",
733
1202
  docs: {
734
1203
  description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
735
1204
  },
736
- schema: [optionSchema6],
1205
+ schema: [optionSchema7],
737
1206
  messages: {
738
1207
  disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
739
1208
  nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
@@ -746,7 +1215,7 @@ var restrictThrowToTaxonomyRule = createRule({
746
1215
  return {
747
1216
  ThrowStatement(node) {
748
1217
  const arg = node.argument;
749
- if (arg.type === import_utils8.AST_NODE_TYPES.NewExpression) {
1218
+ if (arg.type === import_utils9.AST_NODE_TYPES.NewExpression) {
750
1219
  const name = constructorSimpleName2(arg);
751
1220
  if (name !== null && !allow.has(name)) {
752
1221
  context.report({
@@ -758,7 +1227,7 @@ var restrictThrowToTaxonomyRule = createRule({
758
1227
  return;
759
1228
  }
760
1229
  if (isNonErrorValue(arg)) {
761
- const kind = arg.type === import_utils8.AST_NODE_TYPES.ObjectExpression ? "object" : arg.type === import_utils8.AST_NODE_TYPES.ArrayExpression ? "array" : "literal";
1230
+ const kind = arg.type === import_utils9.AST_NODE_TYPES.ObjectExpression ? "object" : arg.type === import_utils9.AST_NODE_TYPES.ArrayExpression ? "array" : "literal";
762
1231
  context.report({ node: arg, messageId: "nonErrorThrow", data: { kind } });
763
1232
  }
764
1233
  }
@@ -766,10 +1235,184 @@ var restrictThrowToTaxonomyRule = createRule({
766
1235
  }
767
1236
  });
768
1237
 
1238
+ // src/rules/schema-enum-field-consistency.ts
1239
+ var import_utils10 = require("@typescript-eslint/utils");
1240
+ var RULE_NAME10 = "schema-enum-field-consistency";
1241
+ var MODIFIERS = /* @__PURE__ */ new Set([
1242
+ "optional",
1243
+ "nullable",
1244
+ "nullish",
1245
+ "default",
1246
+ "prefault",
1247
+ "catch",
1248
+ "describe",
1249
+ "meta",
1250
+ "readonly"
1251
+ ]);
1252
+ var ENUM_PRESERVING = /* @__PURE__ */ new Set(["extract", "exclude"]);
1253
+ var OUTPUT_CHANGING = /* @__PURE__ */ new Set(["pipe", "transform"]);
1254
+ var OBJECT_FACTORIES = /* @__PURE__ */ new Set(["object", "strictObject", "looseObject"]);
1255
+ var SHAPE_EXTENDERS = /* @__PURE__ */ new Set(["extend", "safeExtend"]);
1256
+ var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
1257
+ var UNION = /* @__PURE__ */ new Set(["union"]);
1258
+ var LITERAL = /* @__PURE__ */ new Set(["literal"]);
1259
+ var STRING = /* @__PURE__ */ new Set(["string"]);
1260
+ var optionSchema8 = {
1261
+ type: "object",
1262
+ additionalProperties: false,
1263
+ properties: {
1264
+ zodIdentifiers: { type: "array", items: { type: "string" }, uniqueItems: true },
1265
+ ignoreFields: { type: "array", items: { type: "string" }, uniqueItems: true },
1266
+ enumIdentifierPattern: { type: "string" }
1267
+ }
1268
+ };
1269
+ var OTHER = { kind: "other" };
1270
+ function isEnumOccurrence(occurrence) {
1271
+ return occurrence.kind.kind === "enum";
1272
+ }
1273
+ function methodCall(node) {
1274
+ if (node.type !== import_utils10.AST_NODE_TYPES.CallExpression) return null;
1275
+ const callee = node.callee;
1276
+ if (callee.type !== import_utils10.AST_NODE_TYPES.MemberExpression || callee.computed) return null;
1277
+ if (callee.property.type !== import_utils10.AST_NODE_TYPES.Identifier) return null;
1278
+ return { receiver: callee.object, method: callee.property.name, call: node };
1279
+ }
1280
+ function propertyName(property) {
1281
+ if (property.computed) return null;
1282
+ if (property.key.type === import_utils10.AST_NODE_TYPES.Identifier) return property.key.name;
1283
+ if (property.key.type === import_utils10.AST_NODE_TYPES.Literal && typeof property.key.value === "string") {
1284
+ return property.key.value;
1285
+ }
1286
+ return null;
1287
+ }
1288
+ var schemaEnumFieldConsistencyRule = createRule({
1289
+ name: RULE_NAME10,
1290
+ meta: {
1291
+ type: "problem",
1292
+ docs: {
1293
+ 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."
1294
+ },
1295
+ schema: [optionSchema8],
1296
+ messages: {
1297
+ 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)."
1298
+ }
1299
+ },
1300
+ defaultOptions: [{ zodIdentifiers: ["z"], ignoreFields: [] }],
1301
+ create(context, [options]) {
1302
+ const zodIdentifiers = new Set(options.zodIdentifiers ?? ["z"]);
1303
+ const ignoreFields = new Set(options.ignoreFields ?? []);
1304
+ const enumIdentifierPattern = options.enumIdentifierPattern === void 0 ? null : new RegExp(options.enumIdentifierPattern, "u");
1305
+ const sourceCode = context.sourceCode;
1306
+ const fields = /* @__PURE__ */ new Map();
1307
+ function isZodCall(node, names) {
1308
+ const call = methodCall(node);
1309
+ return call !== null && call.receiver.type === import_utils10.AST_NODE_TYPES.Identifier && zodIdentifiers.has(call.receiver.name) && names.has(call.method);
1310
+ }
1311
+ function resolveVariable(identifier) {
1312
+ let scope = sourceCode.getScope(identifier);
1313
+ while (scope !== null) {
1314
+ const variable = scope.set.get(identifier.name);
1315
+ if (variable !== void 0) return variable;
1316
+ scope = scope.upper;
1317
+ }
1318
+ return null;
1319
+ }
1320
+ function identifierIsEnum(identifier, seen) {
1321
+ const definition = resolveVariable(identifier)?.defs[0];
1322
+ if (definition === void 0) return false;
1323
+ if (definition.type === "ImportBinding") {
1324
+ return enumIdentifierPattern !== null && enumIdentifierPattern.test(identifier.name);
1325
+ }
1326
+ if (definition.type !== "Variable") return false;
1327
+ const init = definition.node.init;
1328
+ if (init === null || seen.has(init)) return false;
1329
+ return classify(init, /* @__PURE__ */ new Set([...seen, init])).kind === "enum";
1330
+ }
1331
+ function isLiteralUnion(node) {
1332
+ if (!isZodCall(node, UNION)) return false;
1333
+ const members = node.arguments[0];
1334
+ if (members?.type !== import_utils10.AST_NODE_TYPES.ArrayExpression || members.elements.length === 0) {
1335
+ return false;
1336
+ }
1337
+ return members.elements.every((element) => element !== null && isZodCall(element, LITERAL));
1338
+ }
1339
+ function isMultiLiteral(node) {
1340
+ if (!isZodCall(node, LITERAL)) return false;
1341
+ const value = node.arguments[0];
1342
+ return value?.type === import_utils10.AST_NODE_TYPES.ArrayExpression && value.elements.length > 1;
1343
+ }
1344
+ function classify(node, seen) {
1345
+ let current = node;
1346
+ for (; ; ) {
1347
+ const call = methodCall(current);
1348
+ if (call === null || !(MODIFIERS.has(call.method) || ENUM_PRESERVING.has(call.method))) {
1349
+ break;
1350
+ }
1351
+ current = call.receiver;
1352
+ }
1353
+ if (current.type === import_utils10.AST_NODE_TYPES.Identifier) {
1354
+ return identifierIsEnum(current, seen) ? { kind: "enum", identifier: current.name } : OTHER;
1355
+ }
1356
+ if (isZodCall(current, ENUM_FACTORIES) || isLiteralUnion(current) || isMultiLiteral(current)) {
1357
+ return { kind: "enum", identifier: null };
1358
+ }
1359
+ current = node;
1360
+ for (; ; ) {
1361
+ if (isZodCall(current, STRING)) return { kind: "string" };
1362
+ const call = methodCall(current);
1363
+ if (call === null || OUTPUT_CHANGING.has(call.method)) return OTHER;
1364
+ current = call.receiver;
1365
+ }
1366
+ }
1367
+ function collectShape(shape) {
1368
+ if (shape?.type !== import_utils10.AST_NODE_TYPES.ObjectExpression) return;
1369
+ for (const property of shape.properties) {
1370
+ if (property.type !== import_utils10.AST_NODE_TYPES.Property) continue;
1371
+ const name = propertyName(property);
1372
+ if (name === null || ignoreFields.has(name)) continue;
1373
+ const kind = classify(property.value, /* @__PURE__ */ new Set());
1374
+ if (kind.kind === "other") continue;
1375
+ const occurrences = fields.get(name) ?? [];
1376
+ occurrences.push({ property, kind });
1377
+ fields.set(name, occurrences);
1378
+ }
1379
+ }
1380
+ return {
1381
+ CallExpression(node) {
1382
+ if (isZodCall(node, OBJECT_FACTORIES)) {
1383
+ collectShape(node.arguments[0]);
1384
+ return;
1385
+ }
1386
+ const call = methodCall(node);
1387
+ if (call !== null && SHAPE_EXTENDERS.has(call.method)) collectShape(node.arguments[0]);
1388
+ },
1389
+ "Program:exit"() {
1390
+ for (const [field, occurrences] of fields) {
1391
+ const enumOccurrence = occurrences.find(isEnumOccurrence);
1392
+ if (enumOccurrence === void 0) continue;
1393
+ const suggestion = enumOccurrence.kind.identifier === null ? "the same enum schema" : `\`${enumOccurrence.kind.identifier}\``;
1394
+ for (const occurrence of occurrences) {
1395
+ if (occurrence.kind.kind !== "string") continue;
1396
+ context.report({
1397
+ node: occurrence.property,
1398
+ messageId: "widenedEnumField",
1399
+ data: {
1400
+ field,
1401
+ line: String(enumOccurrence.property.loc.start.line),
1402
+ suggestion
1403
+ }
1404
+ });
1405
+ }
1406
+ }
1407
+ }
1408
+ };
1409
+ }
1410
+ });
1411
+
769
1412
  // src/rules/wire-message-naming.ts
770
- var RULE_NAME9 = "wire-message-naming";
1413
+ var RULE_NAME11 = "wire-message-naming";
771
1414
  var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
772
- var optionSchema7 = {
1415
+ var optionSchema9 = {
773
1416
  type: "object",
774
1417
  additionalProperties: false,
775
1418
  properties: {
@@ -810,14 +1453,14 @@ function typeLiteralNode(obj) {
810
1453
  return null;
811
1454
  }
812
1455
  var wireMessageNamingRule = createRule({
813
- name: RULE_NAME9,
1456
+ name: RULE_NAME11,
814
1457
  meta: {
815
1458
  type: "problem",
816
1459
  docs: {
817
1460
  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)."
818
1461
  },
819
1462
  fixable: "code",
820
- schema: [optionSchema7],
1463
+ schema: [optionSchema9],
821
1464
  messages: {
822
1465
  typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
823
1466
  }
@@ -853,11 +1496,11 @@ var wireMessageNamingRule = createRule({
853
1496
  });
854
1497
 
855
1498
  // src/rules/zod-schema-naming.ts
856
- var RULE_NAME10 = "zod-schema-naming";
1499
+ var RULE_NAME12 = "zod-schema-naming";
857
1500
  var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
858
1501
  var SUFFIX = "Schema";
859
1502
  var DEFAULT_ROLE_SUFFIXES2 = [];
860
- var optionSchema8 = {
1503
+ var optionSchema10 = {
861
1504
  type: "object",
862
1505
  additionalProperties: false,
863
1506
  properties: {
@@ -890,13 +1533,13 @@ function rootIdentifierName(node) {
890
1533
  return null;
891
1534
  }
892
1535
  var zodSchemaNamingRule = createRule({
893
- name: RULE_NAME10,
1536
+ name: RULE_NAME12,
894
1537
  meta: {
895
1538
  type: "problem",
896
1539
  docs: {
897
1540
  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>`)."
898
1541
  },
899
- schema: [optionSchema8],
1542
+ schema: [optionSchema10],
900
1543
  messages: {
901
1544
  schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
902
1545
  missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
@@ -957,12 +1600,14 @@ var rules = {
957
1600
  "restrict-throw-to-taxonomy": restrictThrowToTaxonomyRule,
958
1601
  "require-registered-keys": requireRegisteredKeysRule,
959
1602
  "env-var-schema-parity": envVarSchemaParityRule,
960
- "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule
1603
+ "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule,
1604
+ "schema-enum-field-consistency": schemaEnumFieldConsistencyRule,
1605
+ "fetch-must-check-ok": fetchMustCheckOkRule
961
1606
  };
962
1607
 
963
1608
  // src/index.ts
964
1609
  var NAMESPACE = "noctcore-contracts";
965
- var VERSION = "0.2.0";
1610
+ var VERSION = "0.3.0";
966
1611
  var plugin = {
967
1612
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
968
1613
  rules,