@noctcore/eslint-plugin-contracts 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -32,18 +42,118 @@ var recommended = {
32
42
  "noctcore-contracts/wire-message-naming": "error",
33
43
  "noctcore-contracts/no-error-stringify": "error",
34
44
  "noctcore-contracts/no-direct-process-env": "error",
35
- "noctcore-contracts/money-must-be-decimal": "error"
45
+ "noctcore-contracts/money-must-be-decimal": "error",
46
+ "noctcore-contracts/require-error-cause": "error",
47
+ "noctcore-contracts/restrict-throw-to-taxonomy": "error",
48
+ // Config-required / heuristic rules ship inert. `require-registered-keys` and
49
+ // `env-var-schema-parity` do nothing until their `sinks` / `schema` options are
50
+ // set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
51
+ // type-aware concern. Enable them explicitly once configured for your project.
52
+ "noctcore-contracts/require-registered-keys": "off",
53
+ "noctcore-contracts/env-var-schema-parity": "off",
54
+ "noctcore-contracts/require-schema-parse-at-boundary": "off"
36
55
  };
37
56
 
38
- // src/rules/money-must-be-decimal.ts
57
+ // src/rules/env-var-schema-parity.ts
58
+ var import_node_fs = require("fs");
59
+ var import_node_path = __toESM(require("path"), 1);
39
60
  var import_utils = require("@typescript-eslint/utils");
40
61
 
41
62
  // src/createRule.ts
42
63
  var import_eslint_utils = require("@noctcore/eslint-utils");
43
64
  var createRule = (0, import_eslint_utils.makeCreateRule)("contracts");
44
65
 
66
+ // src/rules/env-var-schema-parity.ts
67
+ var RULE_NAME = "env-var-schema-parity";
68
+ var optionSchema = {
69
+ type: "object",
70
+ additionalProperties: false,
71
+ properties: {
72
+ schema: { type: "string", minLength: 1 }
73
+ }
74
+ };
75
+ var schemaCache = /* @__PURE__ */ new Map();
76
+ var DOTENV_KEY = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm;
77
+ var OBJECT_KEY = /["'`]?([A-Za-z_][A-Za-z0-9_]*)["'`]?\s*:/g;
78
+ function parseDeclaredKeys(source) {
79
+ const keys = /* @__PURE__ */ new Set();
80
+ for (const match of source.matchAll(DOTENV_KEY)) {
81
+ if (match[1]) keys.add(match[1]);
82
+ }
83
+ for (const match of source.matchAll(OBJECT_KEY)) {
84
+ if (match[1]) keys.add(match[1]);
85
+ }
86
+ return keys;
87
+ }
88
+ function loadSchema(cwd, schema) {
89
+ const resolved = import_node_path.default.isAbsolute(schema) ? schema : import_node_path.default.resolve(cwd, schema);
90
+ const cached = schemaCache.get(resolved);
91
+ if (cached !== void 0) {
92
+ return cached;
93
+ }
94
+ let keys;
95
+ try {
96
+ keys = parseDeclaredKeys((0, import_node_fs.readFileSync)(resolved, "utf8"));
97
+ } catch {
98
+ keys = null;
99
+ }
100
+ schemaCache.set(resolved, keys);
101
+ return keys;
102
+ }
103
+ function isProcessEnv(node) {
104
+ return node.type === import_utils.AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === import_utils.AST_NODE_TYPES.Identifier && node.object.name === "process" && node.property.type === import_utils.AST_NODE_TYPES.Identifier && node.property.name === "env";
105
+ }
106
+ function isImportMetaEnv(node) {
107
+ return node.type === import_utils.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils.AST_NODE_TYPES.Identifier && node.property.name === "env" && node.object.type === import_utils.AST_NODE_TYPES.MetaProperty && node.object.meta.name === "import" && node.object.property.name === "meta";
108
+ }
109
+ var envVarSchemaParityRule = createRule({
110
+ name: RULE_NAME,
111
+ meta: {
112
+ type: "suggestion",
113
+ docs: {
114
+ description: "Require every `process.env.FOO` / `import.meta.env.FOO` key to be declared in a schema file (`.env.example` or a zod-env module), so config access and config declaration cannot drift apart."
115
+ },
116
+ schema: [optionSchema],
117
+ messages: {
118
+ undeclaredEnvVar: "Env var `{{name}}` is read here but not declared in `{{schema}}`. Add it to the schema (or fix the typo) so config stays a validated contract."
119
+ }
120
+ },
121
+ defaultOptions: [{}],
122
+ create(context, [options]) {
123
+ const schema = options.schema;
124
+ if (!schema) {
125
+ return {};
126
+ }
127
+ const declared = loadSchema(context.cwd, schema);
128
+ if (declared === null) {
129
+ return {};
130
+ }
131
+ function check(node) {
132
+ if (node.computed || node.property.type !== import_utils.AST_NODE_TYPES.Identifier) {
133
+ return;
134
+ }
135
+ const name = node.property.name;
136
+ if (!declared.has(name)) {
137
+ context.report({
138
+ node: node.property,
139
+ messageId: "undeclaredEnvVar",
140
+ data: { name, schema }
141
+ });
142
+ }
143
+ }
144
+ return {
145
+ MemberExpression(node) {
146
+ if (isProcessEnv(node.object) || isImportMetaEnv(node.object)) {
147
+ check(node);
148
+ }
149
+ }
150
+ };
151
+ }
152
+ });
153
+
45
154
  // src/rules/money-must-be-decimal.ts
46
- var RULE_NAME = "money-must-be-decimal";
155
+ var import_utils2 = require("@typescript-eslint/utils");
156
+ var RULE_NAME2 = "money-must-be-decimal";
47
157
  var DEFAULT_DECIMAL_TYPE = "Decimal";
48
158
  var DEFAULT_FIELD_PATTERNS = [
49
159
  "amount",
@@ -53,7 +163,7 @@ var DEFAULT_FIELD_PATTERNS = [
53
163
  "balance"
54
164
  ];
55
165
  var DEFAULT_ALLOWED_FILES = [];
56
- var optionSchema = {
166
+ var optionSchema2 = {
57
167
  type: "object",
58
168
  additionalProperties: false,
59
169
  properties: {
@@ -82,22 +192,22 @@ function isAllowedFile(filename, patterns) {
82
192
  return patterns.some((pattern) => normalized.endsWith(toForwardSlash(pattern)));
83
193
  }
84
194
  function staticName(node) {
85
- if (node.type === import_utils.AST_NODE_TYPES.Identifier) {
195
+ if (node.type === import_utils2.AST_NODE_TYPES.Identifier) {
86
196
  return node.name;
87
197
  }
88
198
  return void 0;
89
199
  }
90
200
  function isNumberAnnotation(annotation) {
91
- return annotation?.typeAnnotation.type === import_utils.AST_NODE_TYPES.TSNumberKeyword;
201
+ return annotation?.typeAnnotation.type === import_utils2.AST_NODE_TYPES.TSNumberKeyword;
92
202
  }
93
203
  var moneyMustBeDecimalRule = createRule({
94
- name: RULE_NAME,
204
+ name: RULE_NAME2,
95
205
  meta: {
96
206
  type: "problem",
97
207
  docs: {
98
208
  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."
99
209
  },
100
- schema: [optionSchema],
210
+ schema: [optionSchema2],
101
211
  messages: {
102
212
  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."
103
213
  }
@@ -133,7 +243,7 @@ var moneyMustBeDecimalRule = createRule({
133
243
  },
134
244
  // `const total: number = ...`: annotated variable declarator.
135
245
  VariableDeclarator(node) {
136
- if (node.id.type !== import_utils.AST_NODE_TYPES.Identifier) {
246
+ if (node.id.type !== import_utils2.AST_NODE_TYPES.Identifier) {
137
247
  return;
138
248
  }
139
249
  const name = node.id.name;
@@ -146,15 +256,15 @@ var moneyMustBeDecimalRule = createRule({
146
256
  });
147
257
 
148
258
  // src/rules/no-direct-process-env.ts
149
- var import_utils2 = require("@typescript-eslint/utils");
150
- var RULE_NAME2 = "no-direct-process-env";
259
+ var import_utils3 = require("@typescript-eslint/utils");
260
+ var RULE_NAME3 = "no-direct-process-env";
151
261
  var DEFAULT_CONFIG_MODULE = "@/config";
152
262
  var DEFAULT_ALLOWED_FILES2 = [
153
263
  "**/*.config.{ts,js,mjs,cjs}",
154
264
  "**/*.{spec,test}.{ts,tsx}",
155
265
  "**/scripts/**"
156
266
  ];
157
- var optionSchema2 = {
267
+ var optionSchema3 = {
158
268
  type: "object",
159
269
  additionalProperties: false,
160
270
  properties: {
@@ -208,23 +318,23 @@ function isAllowedFile2(filename, patterns) {
208
318
  const normalized = filename.split("\\").join("/");
209
319
  return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
210
320
  }
211
- function isProcessEnv(node) {
212
- if (node.type !== import_utils2.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils2.AST_NODE_TYPES.Identifier || node.object.name !== "process") {
321
+ 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") {
213
323
  return false;
214
324
  }
215
325
  if (node.computed) {
216
- return node.property.type === import_utils2.AST_NODE_TYPES.Literal && node.property.value === "env";
326
+ return node.property.type === import_utils3.AST_NODE_TYPES.Literal && node.property.value === "env";
217
327
  }
218
- return node.property.type === import_utils2.AST_NODE_TYPES.Identifier && node.property.name === "env";
328
+ return node.property.type === import_utils3.AST_NODE_TYPES.Identifier && node.property.name === "env";
219
329
  }
220
330
  var noDirectProcessEnvRule = createRule({
221
- name: RULE_NAME2,
331
+ name: RULE_NAME3,
222
332
  meta: {
223
333
  type: "problem",
224
334
  docs: {
225
335
  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."
226
336
  },
227
- schema: [optionSchema2],
337
+ schema: [optionSchema3],
228
338
  messages: {
229
339
  directProcessEnv: "Read environment variables through your typed config accessor (import from `{{configModule}}`). Direct `process.env` access bypasses boot-time validation."
230
340
  }
@@ -250,7 +360,7 @@ var noDirectProcessEnvRule = createRule({
250
360
  * returned, or assigned (`log(process.env)`, `return process.env`).
251
361
  */
252
362
  MemberExpression(node) {
253
- if (isProcessEnv(node)) {
363
+ if (isProcessEnv2(node)) {
254
364
  context.report({ node, messageId: "directProcessEnv", data: { configModule } });
255
365
  }
256
366
  }
@@ -259,10 +369,10 @@ var noDirectProcessEnvRule = createRule({
259
369
  });
260
370
 
261
371
  // src/rules/no-error-stringify.ts
262
- var import_utils3 = require("@typescript-eslint/utils");
263
- var RULE_NAME3 = "no-error-stringify";
372
+ var import_utils4 = require("@typescript-eslint/utils");
373
+ var RULE_NAME4 = "no-error-stringify";
264
374
  var DEFAULT_ERROR_NAMES = ["error", "err", "e", "cause"];
265
- var optionSchema3 = {
375
+ var optionSchema4 = {
266
376
  type: "object",
267
377
  additionalProperties: false,
268
378
  properties: {
@@ -275,19 +385,19 @@ var optionSchema3 = {
275
385
  }
276
386
  };
277
387
  function isEmptyStringLiteral(node) {
278
- return node.type === import_utils3.AST_NODE_TYPES.Literal && node.value === "";
388
+ return node.type === import_utils4.AST_NODE_TYPES.Literal && node.value === "";
279
389
  }
280
390
  function isErrorIdentifier(node, names) {
281
- return node.type === import_utils3.AST_NODE_TYPES.Identifier && names.has(node.name);
391
+ return node.type === import_utils4.AST_NODE_TYPES.Identifier && names.has(node.name);
282
392
  }
283
393
  var noErrorStringifyRule = createRule({
284
- name: RULE_NAME3,
394
+ name: RULE_NAME4,
285
395
  meta: {
286
396
  type: "problem",
287
397
  docs: {
288
398
  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.'
289
399
  },
290
- schema: [optionSchema3],
400
+ schema: [optionSchema4],
291
401
  messages: {
292
402
  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)."
293
403
  }
@@ -302,7 +412,7 @@ var noErrorStringifyRule = createRule({
302
412
  // `error.toString()`
303
413
  'CallExpression[callee.type="MemberExpression"]'(node) {
304
414
  const callee = node.callee;
305
- if (!callee.computed && callee.property.type === import_utils3.AST_NODE_TYPES.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
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)) {
306
416
  report(node, callee.object.name);
307
417
  }
308
418
  },
@@ -334,10 +444,332 @@ var noErrorStringifyRule = createRule({
334
444
  }
335
445
  });
336
446
 
447
+ // src/rules/require-error-cause.ts
448
+ var import_utils5 = require("@typescript-eslint/utils");
449
+ var RULE_NAME5 = "require-error-cause";
450
+ function constructorSimpleName(node) {
451
+ const callee = node.callee;
452
+ if (callee.type === import_utils5.AST_NODE_TYPES.Identifier) {
453
+ return callee.name;
454
+ }
455
+ if (callee.type === import_utils5.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils5.AST_NODE_TYPES.Identifier) {
456
+ return callee.property.name;
457
+ }
458
+ return null;
459
+ }
460
+ function isErrorLikeName(name) {
461
+ return /(?:Error|Exception)$/.test(name);
462
+ }
463
+ function alreadyHasCause(node) {
464
+ for (const arg of node.arguments) {
465
+ if (arg.type === import_utils5.AST_NODE_TYPES.SpreadElement) {
466
+ return true;
467
+ }
468
+ if (arg.type === import_utils5.AST_NODE_TYPES.ObjectExpression) {
469
+ for (const prop of arg.properties) {
470
+ if (prop.type === import_utils5.AST_NODE_TYPES.SpreadElement) {
471
+ return true;
472
+ }
473
+ 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";
475
+ if (isCause) {
476
+ return true;
477
+ }
478
+ }
479
+ }
480
+ }
481
+ return false;
482
+ }
483
+ function buildFix(node, binding) {
484
+ const args = node.arguments;
485
+ if (args.length === 0) {
486
+ return null;
487
+ }
488
+ const last = args[args.length - 1];
489
+ if (last === void 0) {
490
+ return null;
491
+ }
492
+ if (last.type === import_utils5.AST_NODE_TYPES.ObjectExpression) {
493
+ const props = last.properties;
494
+ if (props.length === 0) {
495
+ return (fixer) => fixer.replaceText(last, `{ cause: ${binding} }`);
496
+ }
497
+ const lastProp = props[props.length - 1];
498
+ if (lastProp === void 0) {
499
+ return null;
500
+ }
501
+ return (fixer) => fixer.insertTextAfter(lastProp, `, cause: ${binding}`);
502
+ }
503
+ return (fixer) => fixer.insertTextAfter(last, `, { cause: ${binding} }`);
504
+ }
505
+ var requireErrorCauseRule = createRule({
506
+ name: RULE_NAME5,
507
+ meta: {
508
+ type: "problem",
509
+ docs: {
510
+ description: "Require re-thrown errors inside a `catch` to forward the caught error as `{ cause }`. A `throw new SomeError(...)` that omits the cause severs the chain to the original failure."
511
+ },
512
+ fixable: "code",
513
+ schema: [],
514
+ messages: {
515
+ missingCause: "Re-throwing inside `catch` without `{ cause: {{binding}} }` drops the original error. Pass `{ cause: {{binding}} }` to `new {{ctor}}(...)` so the chain is preserved."
516
+ }
517
+ },
518
+ defaultOptions: [],
519
+ create(context) {
520
+ const catchBindings = [];
521
+ return {
522
+ CatchClause(node) {
523
+ const param = node.param;
524
+ catchBindings.push(
525
+ param && param.type === import_utils5.AST_NODE_TYPES.Identifier ? param.name : null
526
+ );
527
+ },
528
+ "CatchClause:exit"() {
529
+ catchBindings.pop();
530
+ },
531
+ ThrowStatement(node) {
532
+ const binding = catchBindings[catchBindings.length - 1];
533
+ if (binding == null) {
534
+ return;
535
+ }
536
+ const arg = node.argument;
537
+ if (arg.type !== import_utils5.AST_NODE_TYPES.NewExpression) {
538
+ return;
539
+ }
540
+ const ctor = constructorSimpleName(arg);
541
+ if (ctor === null || !isErrorLikeName(ctor)) {
542
+ return;
543
+ }
544
+ if (alreadyHasCause(arg)) {
545
+ return;
546
+ }
547
+ const fix = buildFix(arg, binding);
548
+ context.report({
549
+ node: arg,
550
+ messageId: "missingCause",
551
+ data: { binding, ctor },
552
+ ...fix ? { fix } : {}
553
+ });
554
+ }
555
+ };
556
+ }
557
+ });
558
+
559
+ // src/rules/require-registered-keys.ts
560
+ var import_utils6 = require("@typescript-eslint/utils");
561
+ var RULE_NAME6 = "require-registered-keys";
562
+ var optionSchema5 = {
563
+ type: "object",
564
+ additionalProperties: false,
565
+ properties: {
566
+ sinks: {
567
+ type: "array",
568
+ items: {
569
+ type: "object",
570
+ additionalProperties: false,
571
+ required: ["callee", "argIndex"],
572
+ properties: {
573
+ callee: { type: "string", minLength: 1 },
574
+ argIndex: { type: "integer", minimum: 0 }
575
+ }
576
+ }
577
+ },
578
+ registry: { type: "string", minLength: 1 }
579
+ }
580
+ };
581
+ function calleePath(callee) {
582
+ if (callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
583
+ return callee.name;
584
+ }
585
+ if (callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && !callee.computed) {
586
+ if (callee.property.type !== import_utils6.AST_NODE_TYPES.Identifier) {
587
+ return null;
588
+ }
589
+ const objectPath = calleePath(callee.object);
590
+ return objectPath === null ? null : `${objectPath}.${callee.property.name}`;
591
+ }
592
+ return null;
593
+ }
594
+ function isStringLiteral(node) {
595
+ return node.type === import_utils6.AST_NODE_TYPES.Literal && typeof node.value === "string";
596
+ }
597
+ var requireRegisteredKeysRule = createRule({
598
+ name: RULE_NAME6,
599
+ meta: {
600
+ type: "suggestion",
601
+ docs: {
602
+ 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
+ },
604
+ schema: [optionSchema5],
605
+ messages: {
606
+ 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
+ }
608
+ },
609
+ defaultOptions: [{ sinks: [] }],
610
+ create(context, [options]) {
611
+ const sinks = options.sinks ?? [];
612
+ if (sinks.length === 0) {
613
+ return {};
614
+ }
615
+ const sinkMap = /* @__PURE__ */ new Map();
616
+ for (const sink of sinks) {
617
+ const existing = sinkMap.get(sink.callee);
618
+ if (existing) {
619
+ existing.add(sink.argIndex);
620
+ } else {
621
+ sinkMap.set(sink.callee, /* @__PURE__ */ new Set([sink.argIndex]));
622
+ }
623
+ }
624
+ const registry = options.registry;
625
+ const registryHint = registry ? ` (import it from '${registry}')` : "";
626
+ return {
627
+ CallExpression(node) {
628
+ const path2 = calleePath(node.callee);
629
+ if (path2 === null) {
630
+ return;
631
+ }
632
+ const indexes = sinkMap.get(path2);
633
+ if (indexes === void 0) {
634
+ return;
635
+ }
636
+ for (const index of indexes) {
637
+ const arg = node.arguments[index];
638
+ if (arg !== void 0 && isStringLiteral(arg)) {
639
+ context.report({
640
+ node: arg,
641
+ messageId: "unregisteredKey",
642
+ data: { callee: path2, value: `'${arg.value}'`, registryHint }
643
+ });
644
+ }
645
+ }
646
+ }
647
+ };
648
+ }
649
+ });
650
+
651
+ // 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";
654
+ 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";
656
+ }
657
+ function isAwaitJsonCall(node) {
658
+ if (node.type !== import_utils7.AST_NODE_TYPES.AwaitExpression) {
659
+ return false;
660
+ }
661
+ 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";
663
+ }
664
+ function isShapeClaim(annotation) {
665
+ if (annotation.type === import_utils7.AST_NODE_TYPES.TSArrayType) {
666
+ return true;
667
+ }
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");
670
+ }
671
+ return false;
672
+ }
673
+ var requireSchemaParseAtBoundaryRule = createRule({
674
+ name: RULE_NAME7,
675
+ meta: {
676
+ type: "problem",
677
+ docs: {
678
+ description: "Disallow asserting external boundary data with `as T` instead of parsing it at runtime. Flags `JSON.parse(...) as T` and `(await res.json()) as T`; use a zod/valibot parse."
679
+ },
680
+ schema: [],
681
+ messages: {
682
+ castedBoundaryData: "Boundary data is asserted with `as` here, not parsed. A cast is unchecked \u2014 validate this with a runtime schema (e.g. `Schema.parse(...)`) so a wire-shape change fails loudly."
683
+ }
684
+ },
685
+ defaultOptions: [],
686
+ create(context) {
687
+ return {
688
+ TSAsExpression(node) {
689
+ if (!isShapeClaim(node.typeAnnotation)) {
690
+ return;
691
+ }
692
+ const expr = node.expression;
693
+ if (isJsonParseCall(expr) || isAwaitJsonCall(expr)) {
694
+ context.report({ node, messageId: "castedBoundaryData" });
695
+ }
696
+ }
697
+ };
698
+ }
699
+ });
700
+
701
+ // src/rules/restrict-throw-to-taxonomy.ts
702
+ var import_utils8 = require("@typescript-eslint/utils");
703
+ var RULE_NAME8 = "restrict-throw-to-taxonomy";
704
+ var DEFAULT_ALLOW = ["Error"];
705
+ var optionSchema6 = {
706
+ type: "object",
707
+ additionalProperties: false,
708
+ properties: {
709
+ allow: {
710
+ type: "array",
711
+ items: { type: "string" },
712
+ uniqueItems: true
713
+ }
714
+ }
715
+ };
716
+ function constructorSimpleName2(node) {
717
+ const callee = node.callee;
718
+ if (callee.type === import_utils8.AST_NODE_TYPES.Identifier) {
719
+ return callee.name;
720
+ }
721
+ if (callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils8.AST_NODE_TYPES.Identifier) {
722
+ return callee.property.name;
723
+ }
724
+ return null;
725
+ }
726
+ 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;
728
+ }
729
+ var restrictThrowToTaxonomyRule = createRule({
730
+ name: RULE_NAME8,
731
+ meta: {
732
+ type: "problem",
733
+ docs: {
734
+ 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
+ },
736
+ schema: [optionSchema6],
737
+ messages: {
738
+ disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
739
+ nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
740
+ }
741
+ },
742
+ defaultOptions: [{ allow: [...DEFAULT_ALLOW] }],
743
+ create(context, [options]) {
744
+ const allow = new Set(options.allow ?? DEFAULT_ALLOW);
745
+ const allowedList = [...allow].join(", ");
746
+ return {
747
+ ThrowStatement(node) {
748
+ const arg = node.argument;
749
+ if (arg.type === import_utils8.AST_NODE_TYPES.NewExpression) {
750
+ const name = constructorSimpleName2(arg);
751
+ if (name !== null && !allow.has(name)) {
752
+ context.report({
753
+ node: arg,
754
+ messageId: "disallowedErrorClass",
755
+ data: { name, allowed: allowedList }
756
+ });
757
+ }
758
+ return;
759
+ }
760
+ 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";
762
+ context.report({ node: arg, messageId: "nonErrorThrow", data: { kind } });
763
+ }
764
+ }
765
+ };
766
+ }
767
+ });
768
+
337
769
  // src/rules/wire-message-naming.ts
338
- var RULE_NAME4 = "wire-message-naming";
770
+ var RULE_NAME9 = "wire-message-naming";
339
771
  var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
340
- var optionSchema4 = {
772
+ var optionSchema7 = {
341
773
  type: "object",
342
774
  additionalProperties: false,
343
775
  properties: {
@@ -378,14 +810,14 @@ function typeLiteralNode(obj) {
378
810
  return null;
379
811
  }
380
812
  var wireMessageNamingRule = createRule({
381
- name: RULE_NAME4,
813
+ name: RULE_NAME9,
382
814
  meta: {
383
815
  type: "problem",
384
816
  docs: {
385
817
  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)."
386
818
  },
387
819
  fixable: "code",
388
- schema: [optionSchema4],
820
+ schema: [optionSchema7],
389
821
  messages: {
390
822
  typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
391
823
  }
@@ -421,11 +853,11 @@ var wireMessageNamingRule = createRule({
421
853
  });
422
854
 
423
855
  // src/rules/zod-schema-naming.ts
424
- var RULE_NAME5 = "zod-schema-naming";
856
+ var RULE_NAME10 = "zod-schema-naming";
425
857
  var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
426
858
  var SUFFIX = "Schema";
427
859
  var DEFAULT_ROLE_SUFFIXES2 = [];
428
- var optionSchema5 = {
860
+ var optionSchema8 = {
429
861
  type: "object",
430
862
  additionalProperties: false,
431
863
  properties: {
@@ -458,13 +890,13 @@ function rootIdentifierName(node) {
458
890
  return null;
459
891
  }
460
892
  var zodSchemaNamingRule = createRule({
461
- name: RULE_NAME5,
893
+ name: RULE_NAME10,
462
894
  meta: {
463
895
  type: "problem",
464
896
  docs: {
465
897
  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>`)."
466
898
  },
467
- schema: [optionSchema5],
899
+ schema: [optionSchema8],
468
900
  messages: {
469
901
  schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
470
902
  missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
@@ -520,12 +952,17 @@ var rules = {
520
952
  "wire-message-naming": wireMessageNamingRule,
521
953
  "no-error-stringify": noErrorStringifyRule,
522
954
  "no-direct-process-env": noDirectProcessEnvRule,
523
- "money-must-be-decimal": moneyMustBeDecimalRule
955
+ "money-must-be-decimal": moneyMustBeDecimalRule,
956
+ "require-error-cause": requireErrorCauseRule,
957
+ "restrict-throw-to-taxonomy": restrictThrowToTaxonomyRule,
958
+ "require-registered-keys": requireRegisteredKeysRule,
959
+ "env-var-schema-parity": envVarSchemaParityRule,
960
+ "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule
524
961
  };
525
962
 
526
963
  // src/index.ts
527
964
  var NAMESPACE = "noctcore-contracts";
528
- var VERSION = "0.1.0";
965
+ var VERSION = "0.2.0";
529
966
  var plugin = {
530
967
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
531
968
  rules,