@noctcore/eslint-plugin-contracts 0.5.0 → 0.6.1

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
@@ -639,6 +639,7 @@ var DEFAULT_FIELD_PATTERNS = [
639
639
  "balance"
640
640
  ];
641
641
  var DEFAULT_ALLOWED_FILES = [];
642
+ var DEFAULT_MINOR_UNIT_PATTERNS = [];
642
643
  var optionSchema3 = {
643
644
  type: "object",
644
645
  additionalProperties: false,
@@ -654,6 +655,11 @@ var optionSchema3 = {
654
655
  type: "array",
655
656
  items: { type: "string" },
656
657
  uniqueItems: true
658
+ },
659
+ minorUnitPatterns: {
660
+ type: "array",
661
+ items: { type: "string", minLength: 1 },
662
+ uniqueItems: true
657
663
  }
658
664
  }
659
665
  };
@@ -692,7 +698,8 @@ var moneyMustBeDecimalRule = createRule({
692
698
  {
693
699
  decimalType: DEFAULT_DECIMAL_TYPE,
694
700
  fieldPatterns: [...DEFAULT_FIELD_PATTERNS],
695
- allowedFiles: []
701
+ allowedFiles: [],
702
+ minorUnitPatterns: []
696
703
  }
697
704
  ],
698
705
  create(context, [options]) {
@@ -703,6 +710,11 @@ var moneyMustBeDecimalRule = createRule({
703
710
  }
704
711
  const fieldPatterns = options.fieldPatterns ?? DEFAULT_FIELD_PATTERNS;
705
712
  const moneyPattern = new RegExp(`(${fieldPatterns.join("|")})`, "i");
713
+ const minorUnitPatterns = options.minorUnitPatterns ?? DEFAULT_MINOR_UNIT_PATTERNS;
714
+ const minorUnitPattern = minorUnitPatterns.length > 0 ? new RegExp(`(${minorUnitPatterns.join("|")})`, "i") : null;
715
+ function isDecimalMoneyName(name) {
716
+ return moneyPattern.test(name) && !(minorUnitPattern?.test(name) ?? false);
717
+ }
706
718
  function report(node) {
707
719
  context.report({ node, messageId: "moneyMustBeDecimal", data: { decimalType } });
708
720
  }
@@ -713,7 +725,7 @@ var moneyMustBeDecimalRule = createRule({
713
725
  return;
714
726
  }
715
727
  const name = staticName(node.key);
716
- if (name !== void 0 && moneyPattern.test(name) && isNumberAnnotation(node.typeAnnotation)) {
728
+ if (name !== void 0 && isDecimalMoneyName(name) && isNumberAnnotation(node.typeAnnotation)) {
717
729
  report(node);
718
730
  }
719
731
  },
@@ -723,7 +735,7 @@ var moneyMustBeDecimalRule = createRule({
723
735
  return;
724
736
  }
725
737
  const name = node.id.name;
726
- if (moneyPattern.test(name) && isNumberAnnotation(node.id.typeAnnotation)) {
738
+ if (isDecimalMoneyName(name) && isNumberAnnotation(node.id.typeAnnotation)) {
727
739
  report(node);
728
740
  }
729
741
  }
@@ -2294,7 +2306,7 @@ var rules = {
2294
2306
 
2295
2307
  // src/index.ts
2296
2308
  var NAMESPACE = "noctcore-contracts";
2297
- var VERSION = "0.3.0";
2309
+ var VERSION = "0.6.1";
2298
2310
  var plugin = {
2299
2311
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
2300
2312
  rules,
package/dist/index.d.cts CHANGED
@@ -51,6 +51,12 @@ interface MoneyMustBeDecimalOptions {
51
51
  readonly fieldPatterns?: readonly string[];
52
52
  /** Path-suffix allowlist of files to skip entirely. */
53
53
  readonly allowedFiles?: readonly string[];
54
+ /**
55
+ * Regex fragments (case-insensitive) naming fields that hold an integer count
56
+ * of minor units (cents), where `number` is the correct type. A money-named
57
+ * field matching one of these is not reported. Empty by default.
58
+ */
59
+ readonly minorUnitPatterns?: readonly string[];
54
60
  }
55
61
 
56
62
  interface NoDirectProcessEnvOptions {
package/dist/index.d.ts CHANGED
@@ -51,6 +51,12 @@ interface MoneyMustBeDecimalOptions {
51
51
  readonly fieldPatterns?: readonly string[];
52
52
  /** Path-suffix allowlist of files to skip entirely. */
53
53
  readonly allowedFiles?: readonly string[];
54
+ /**
55
+ * Regex fragments (case-insensitive) naming fields that hold an integer count
56
+ * of minor units (cents), where `number` is the correct type. A money-named
57
+ * field matching one of these is not reported. Empty by default.
58
+ */
59
+ readonly minorUnitPatterns?: readonly string[];
54
60
  }
55
61
 
56
62
  interface NoDirectProcessEnvOptions {
package/dist/index.js CHANGED
@@ -595,6 +595,7 @@ var DEFAULT_FIELD_PATTERNS = [
595
595
  "balance"
596
596
  ];
597
597
  var DEFAULT_ALLOWED_FILES = [];
598
+ var DEFAULT_MINOR_UNIT_PATTERNS = [];
598
599
  var optionSchema3 = {
599
600
  type: "object",
600
601
  additionalProperties: false,
@@ -610,6 +611,11 @@ var optionSchema3 = {
610
611
  type: "array",
611
612
  items: { type: "string" },
612
613
  uniqueItems: true
614
+ },
615
+ minorUnitPatterns: {
616
+ type: "array",
617
+ items: { type: "string", minLength: 1 },
618
+ uniqueItems: true
613
619
  }
614
620
  }
615
621
  };
@@ -648,7 +654,8 @@ var moneyMustBeDecimalRule = createRule({
648
654
  {
649
655
  decimalType: DEFAULT_DECIMAL_TYPE,
650
656
  fieldPatterns: [...DEFAULT_FIELD_PATTERNS],
651
- allowedFiles: []
657
+ allowedFiles: [],
658
+ minorUnitPatterns: []
652
659
  }
653
660
  ],
654
661
  create(context, [options]) {
@@ -659,6 +666,11 @@ var moneyMustBeDecimalRule = createRule({
659
666
  }
660
667
  const fieldPatterns = options.fieldPatterns ?? DEFAULT_FIELD_PATTERNS;
661
668
  const moneyPattern = new RegExp(`(${fieldPatterns.join("|")})`, "i");
669
+ const minorUnitPatterns = options.minorUnitPatterns ?? DEFAULT_MINOR_UNIT_PATTERNS;
670
+ const minorUnitPattern = minorUnitPatterns.length > 0 ? new RegExp(`(${minorUnitPatterns.join("|")})`, "i") : null;
671
+ function isDecimalMoneyName(name) {
672
+ return moneyPattern.test(name) && !(minorUnitPattern?.test(name) ?? false);
673
+ }
662
674
  function report(node) {
663
675
  context.report({ node, messageId: "moneyMustBeDecimal", data: { decimalType } });
664
676
  }
@@ -669,7 +681,7 @@ var moneyMustBeDecimalRule = createRule({
669
681
  return;
670
682
  }
671
683
  const name = staticName(node.key);
672
- if (name !== void 0 && moneyPattern.test(name) && isNumberAnnotation(node.typeAnnotation)) {
684
+ if (name !== void 0 && isDecimalMoneyName(name) && isNumberAnnotation(node.typeAnnotation)) {
673
685
  report(node);
674
686
  }
675
687
  },
@@ -679,7 +691,7 @@ var moneyMustBeDecimalRule = createRule({
679
691
  return;
680
692
  }
681
693
  const name = node.id.name;
682
- if (moneyPattern.test(name) && isNumberAnnotation(node.id.typeAnnotation)) {
694
+ if (isDecimalMoneyName(name) && isNumberAnnotation(node.id.typeAnnotation)) {
683
695
  report(node);
684
696
  }
685
697
  }
@@ -2250,7 +2262,7 @@ var rules = {
2250
2262
 
2251
2263
  // src/index.ts
2252
2264
  var NAMESPACE = "noctcore-contracts";
2253
- var VERSION = "0.3.0";
2265
+ var VERSION = "0.6.1";
2254
2266
  var plugin = {
2255
2267
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
2256
2268
  rules,
@@ -15,7 +15,7 @@ declaration in lockstep.
15
15
  A static `process.env.FOO` or `import.meta.env.FOO` access whose key `FOO` is not declared in the
16
16
  configured schema file:
17
17
 
18
- ```ts
18
+ ```ts prose reason="the rule reads the schema file named in its options from disk"
19
19
  // schema (.env.example) declares DATABASE_URL, PORT, NODE_ENV
20
20
 
21
21
  // ✗
@@ -15,27 +15,37 @@ status the server actually sent. Check the response first, and the failure is na
15
15
 
16
16
  A `.json()` read on a response bound from a configured fetch callee when no check governs it.
17
17
 
18
- ```ts
19
- // ✗ no check at all
18
+ ```ts bad reports=4
19
+ // no check at all
20
20
  export async function loadUser(id: string) {
21
21
  const res = await fetch(`/api/users/${id}`);
22
22
  return res.json();
23
23
  }
24
24
 
25
- // ✗ the check comes after the body is already parsed
26
- const data = await res.json();
27
- if (!res.ok) throw new Error('failed');
25
+ // the check comes after the body is already parsed
26
+ export async function loadOrder(id: string) {
27
+ const res = await fetch(`/api/orders/${id}`);
28
+ const data = await res.json();
29
+ if (!res.ok) throw new Error('failed');
30
+ return data;
31
+ }
28
32
 
29
- // ✗ a single-code guard lets every other error through
30
- if (res.status === 404) return null;
31
- return res.json();
33
+ // a single-code guard lets every other error through
34
+ export async function findUser(id: string) {
35
+ const res = await fetch(`/api/users/${id}`);
36
+ if (res.status === 404) return null;
37
+ return res.json();
38
+ }
32
39
 
33
- // ✗ `||` runs the parse exactly on the failure path
34
- return res.ok || res.json();
40
+ // `||` runs the parse exactly on the failure path
41
+ export async function loadFlags() {
42
+ const res = await fetch('/api/flags');
43
+ return res.ok || res.json();
44
+ }
35
45
  ```
36
46
 
37
- ```ts
38
- // ✓ guard clause
47
+ ```ts good
48
+ // guard clause
39
49
  export async function loadUser(id: string) {
40
50
  const res = await fetch(`/api/users/${id}`);
41
51
  if (!res.ok) {
@@ -44,16 +54,30 @@ export async function loadUser(id: string) {
44
54
  return res.json();
45
55
  }
46
56
 
47
- // ✓ the parse sits in the branch the check permits
48
- return res.ok ? res.json() : null;
57
+ // the parse sits in the branch the check permits
58
+ export async function findUser(id: string) {
59
+ const res = await fetch(`/api/users/${id}`);
60
+ return res.ok ? res.json() : null;
61
+ }
49
62
 
50
- // ✓ a status split at the success/error boundary, or a switch on success codes
51
- if (res.status >= 400) return null;
52
- switch (res.status) { case 200: case 201: return res.json(); default: return null; }
63
+ // a status split at the success/error boundary, or a switch on success codes
64
+ export async function loadOrder(id: string) {
65
+ const res = await fetch(`/api/orders/${id}`);
66
+ if (res.status >= 400) return null;
67
+ return res.json();
68
+ }
69
+
70
+ export async function createOrder() {
71
+ const res = await fetch('/api/orders', { method: 'POST' });
72
+ switch (res.status) { case 200: case 201: return res.json(); default: return null; }
73
+ }
53
74
 
54
- // ✓ an assertion helper (`assert`, `invariant`, `ensure*`, `expect*`)
55
- invariant(res.ok, 'user request failed');
56
- return res.json();
75
+ // an assertion helper (`assert`, `invariant`, `ensure*`, `expect*`)
76
+ export async function loadFlags() {
77
+ const res = await fetch('/api/flags');
78
+ invariant(res.ok, 'user request failed');
79
+ return res.json();
80
+ }
57
81
  ```
58
82
 
59
83
  Three response shapes are tracked: `const res = await fetch(...)` then `res.json()` in the same block,
@@ -20,12 +20,12 @@ Conservative on purpose. Untyped declarations and numeric-literal initializers (
20
20
  **not** flagged — those are usually counters/accumulators. Interface and type-literal members
21
21
  (`{ amount: number }`) are **out of scope** so non-money type members do not regress.
22
22
 
23
- ```ts
24
- // ✗
23
+ ```ts bad reports=2
25
24
  class Invoice { total: number; }
26
25
  const amount: number = 5;
26
+ ```
27
27
 
28
- // ✓
28
+ ```ts good
29
29
  class Invoice { total: Decimal; }
30
30
  const count: number = 3; // not a money name
31
31
  interface Payment { amount: number; } // type member, out of scope
@@ -38,6 +38,7 @@ interface Payment { amount: number; } // type member, out of scope
38
38
  | `decimalType` | `string` | `'Decimal'` | Name of the money type to require; appears in the report message. |
39
39
  | `fieldPatterns` | `string[]` | `['amount', 'price', 'cost', 'total', 'balance']` | Case-insensitive regex fragments identifying money-named fields (OR-combined). |
40
40
  | `allowedFiles` | `string[]` | `[]` | Path-suffix allowlist of files skipped entirely (e.g. `apps/api/src/legacy/totals.ts`). |
41
+ | `minorUnitPatterns` | `string[]` | `[]` | Case-insensitive regex fragments naming fields that hold integer minor units on purpose and are therefore not reported. Checked before `fieldPatterns`. |
41
42
 
42
43
  ```js
43
44
  'noctcore-contracts/money-must-be-decimal': ['error', {
@@ -46,7 +47,26 @@ interface Payment { amount: number; } // type member, out of scope
46
47
  }]
47
48
  ```
48
49
 
50
+ ## Talking to a payment API
51
+
52
+ Stripe and most payment APIs deal in integer minor units: `amount` is a number of cents, and that
53
+ is correct at that boundary. Without `minorUnitPatterns` this rule flags every one of those
54
+ fields, which is how a project ends up turning the rule off entirely and losing it everywhere
55
+ else.
56
+
57
+ Name the boundary fields instead, so the rule keeps policing the rest of the codebase:
58
+
59
+ ```json
60
+ { "minorUnitPatterns": ["amountInCents", "unitAmount", "^amount$"] }
61
+ ```
62
+
63
+ Prefer patterns that are specific to the boundary. A blanket `amount` would exempt every field
64
+ whose name contains it, including the internal totals this rule exists to protect.
65
+
49
66
  ## When not to use it
50
67
 
51
- If your project does not have a dedicated Decimal money type, or represents money as integer minor
52
- units (cents) typed `number` on purpose, this rule does not fit.
68
+ If your project has no dedicated Decimal money type, this rule does not fit.
69
+
70
+ If it represents money as integer minor units **everywhere**, by choice, the rule can still be
71
+ useful with `minorUnitPatterns` covering that convention, but at that point it is asserting a
72
+ naming convention rather than a type, and you may not want it.
@@ -15,19 +15,25 @@ Any `process.env` access, in every position — property read (`process.env.X`),
15
15
  (`process.env[X]`), destructure (`const { X } = process.env`), or the bare value passed / returned /
16
16
  assigned (`log(process.env)`, `return process.env`). Computed `process['env']` cannot bypass it.
17
17
 
18
- ```ts
19
- // ✗
18
+ ```ts bad reports=3
20
19
  const isProd = process.env.NODE_ENV === 'production';
21
20
  const { DATABASE_URL } = process.env;
22
21
  const env = process['env'];
22
+ ```
23
+
24
+ ```ts good
25
+ import { config } from '@/config';
23
26
 
24
- // ✓
25
27
  const isProd = config.isProduction;
26
28
  ```
27
29
 
28
30
  Files matched by the `allowedFiles` glob allowlist are skipped entirely, so bootstrap entrypoints,
29
31
  config files, and tests may still read `process.env` directly.
30
32
 
33
+ ```ts good filename=vite.config.ts relocation
34
+ const isProd = process.env.NODE_ENV === 'production';
35
+ ```
36
+
31
37
  ## Options
32
38
 
33
39
  | Option | Type | Default | Meaning |
@@ -9,7 +9,7 @@ discarding `error.cause`, the stack, and any custom fields. The value that reach
9
9
  sentence with no chain to the underlying failure. The guarded extractor idiom preserves the object for
10
10
  structured loggers and stays legal:
11
11
 
12
- ```ts
12
+ ```ts good
13
13
  error instanceof Error ? error.message : String(error)
14
14
  ```
15
15
 
@@ -18,14 +18,15 @@ error instanceof Error ? error.message : String(error)
18
18
  Only the three unambiguous cause-chain-dropping forms, and only when the operand is a known error
19
19
  identifier (default `error`, `err`, `e`, `cause`):
20
20
 
21
- ```ts
22
- // ✗
21
+ ```ts bad reports=4
23
22
  logger.error(`request failed: ${error}`);
24
- const msg = err.toString();
25
- const msg = error + "";
26
- const msg = "" + e;
23
+ const a = err.toString();
24
+ const b = error + "";
25
+ const c = "" + e;
26
+ ```
27
27
 
28
- // ✓ the guarded idiom (bare String(error) is intentionally NOT policed)
28
+ ```ts good
29
+ // the guarded idiom (bare String(error) is intentionally NOT policed)
29
30
  const msg = error instanceof Error ? error.message : String(error);
30
31
  const m = `${error.message}`;
31
32
  ```
@@ -9,7 +9,7 @@ forward the caught error as `cause`, the underlying failure — its message, sta
9
9
  cause — is gone. The stack you see points only at the re-throw site. `Error`'s standard `cause`
10
10
  option (and every `*Error` subclass that forwards it) preserves the chain:
11
11
 
12
- ```ts
12
+ ```ts good
13
13
  try {
14
14
  await db.query(sql);
15
15
  } catch (err) {
@@ -34,14 +34,15 @@ Deliberately conservative:
34
34
  A `throw` nested in a closure declared inside the catch is still flagged: the binding is genuinely in
35
35
  scope there. Nested `try/catch` uses the nearest binding.
36
36
 
37
- ```ts
38
- // ✗ drops the cause → autofixes to `new Error('failed', { cause: err })`
37
+ ```ts bad reports=2
38
+ // drops the cause: autofixes to `new Error('failed', { cause: err })`
39
39
  try { work(); } catch (err) { throw new Error('failed'); }
40
40
 
41
- // ✗ merges into an existing options object
41
+ // merges into an existing options object
42
42
  try { work(); } catch (err) { throw new HttpError('failed', { status: 500 }); }
43
+ ```
43
44
 
44
- // ✓
45
+ ```ts good
45
46
  try { work(); } catch (err) { throw new Error('failed', { cause: err }); }
46
47
  try { work(); } catch (err) { throw err; }
47
48
  ```
@@ -14,14 +14,13 @@ compile error instead of a runtime miss.
14
14
 
15
15
  A **raw string literal** in the configured key position of a configured sink call:
16
16
 
17
- ```ts
18
- // with sinks: [{ callee: 'localStorage.getItem', argIndex: 0 }, { callee: 'emitter.on', argIndex: 0 }]
19
-
20
- // ✗
17
+ ```ts bad reports=2 options={"sinks":[{"callee":"localStorage.getItem","argIndex":0},{"callee":"emitter.on","argIndex":0}]}
21
18
  localStorage.getItem('user-profile');
22
19
  emitter.on('task-done', handler);
20
+ ```
23
21
 
24
- // ✓ imported constant
22
+ ```ts good options={"sinks":[{"callee":"localStorage.getItem","argIndex":0},{"callee":"emitter.on","argIndex":0}]}
23
+ // imported constant
25
24
  import { USER_PROFILE_KEY, TASK_DONE } from '@/keys';
26
25
  localStorage.getItem(USER_PROFILE_KEY);
27
26
  emitter.on(TASK_DONE, handler);
@@ -10,7 +10,7 @@ promise the runtime never checks: a field the server dropped is now `undefined`
10
10
  `string`, and the corruption surfaces far from the boundary. Parsing with a runtime schema
11
11
  (zod/valibot) validates the shape at the edge and fails loudly there:
12
12
 
13
- ```ts
13
+ ```ts good
14
14
  const user = UserSchema.parse(await res.json()); // validated
15
15
  ```
16
16
 
@@ -19,15 +19,15 @@ const user = UserSchema.parse(await res.json()); // validated
19
19
  This is a **conservative syntactic slice** of a concept that is fully general only with type
20
20
  information. It flags a cast applied **directly** to a call site that is unmistakably a boundary read:
21
21
 
22
- ```ts
23
- // ✗
22
+ ```ts bad reports=3
24
23
  const user = JSON.parse(raw) as User;
25
24
  const users = JSON.parse(raw) as User[];
26
- const user = (await res.json()) as User;
25
+ const fetched = (await res.json()) as User;
26
+ ```
27
27
 
28
- // ✓
28
+ ```ts good
29
29
  const user = UserSchema.parse(JSON.parse(raw));
30
- const user = UserSchema.parse(await res.json());
30
+ const fetched = UserSchema.parse(await res.json());
31
31
  const data = JSON.parse(raw) as unknown; // safe widening, not a shape claim
32
32
  ```
33
33
 
@@ -19,15 +19,17 @@ Conservative on the ambiguous forms. A bare identifier (`throw err` — the re-t
19
19
  (`throw ctx.error`), and a call (`throw makeError()`) are all left alone: a syntactic rule cannot know
20
20
  whether they resolve to an Error, and re-throwing a caught error is the most common `throw` there is.
21
21
 
22
- ```ts
23
- // ✗ built-in not in the taxonomy
22
+ ```ts bad reports=3
23
+ // built-in not in the taxonomy
24
24
  throw new TypeError('bad');
25
25
 
26
- // ✗ bare values
26
+ // bare values
27
27
  throw 'boom';
28
28
  throw { code: 500 };
29
+ ```
29
30
 
30
- // ✓ (default allow is ['Error'])
31
+ ```ts good
32
+ // default allow is ['Error']
31
33
  throw new Error('boom');
32
34
  try { work(); } catch (err) { throw err; }
33
35
  ```
@@ -26,8 +26,8 @@ Per file, purely syntactic, no type information. The rule collects every propert
26
26
  Every **string** occurrence of a key that is **enum** elsewhere in the file is reported. No autofix:
27
27
  the right fix may be a data migration (the stored column was free text), not a schema edit.
28
28
 
29
- ```ts
30
- // ✗ the output widens what both inputs narrow
29
+ ```ts bad reports=1
30
+ // the output widens what both inputs narrow
31
31
  export const statusSchema = z.enum(['OPEN', 'CLOSED']);
32
32
 
33
33
  export const ticketCreateInput = z.object({ status: statusSchema.default('OPEN') });
@@ -38,8 +38,12 @@ export const ticketOutput = z.object({
38
38
  });
39
39
  ```
40
40
 
41
- ```ts
42
- // ✓ the output reuses the enum
41
+ ```ts good
42
+ // the output reuses the enum
43
+ export const statusSchema = z.enum(['OPEN', 'CLOSED']);
44
+
45
+ export const ticketCreateInput = z.object({ status: statusSchema.default('OPEN') });
46
+ export const ticketUpdateInput = z.object({ status: statusSchema.optional() });
43
47
  export const ticketOutput = z.object({
44
48
  id: z.string(),
45
49
  status: statusSchema.nullable(),
@@ -14,7 +14,7 @@ the ones that cannot resolve.
14
14
 
15
15
  A static key that no configured catalog of the namespace in scope contains:
16
16
 
17
- ```tsx
17
+ ```tsx prose reason="the rule reads the translation catalogs named in its options from disk"
18
18
  // catalogs: common = { actions: { save, cancel } }, portal = { tasks: { title } }
19
19
 
20
20
  const { t } = useTranslation(); // default namespace: common
@@ -16,15 +16,17 @@ whose zod object declares a `type: z.literal('…')` property, the literal must
16
16
  `kebab(constName minus the role suffix)`. A mismatch is reported and **autofixed** to the expected
17
17
  value.
18
18
 
19
- ```ts
20
- // ✗ camelCase discriminant → autofixes to 'task-completed'
19
+ ```ts bad reports=2
20
+ // camelCase discriminant: autofixes to 'task-completed'
21
21
  export const TaskCompletedEvent = z.object({ type: z.literal('taskCompleted') });
22
22
 
23
- // ✗ wrong value → autofixes to 'run-task'
23
+ // wrong value: autofixes to 'run-task'
24
24
  export const RunTaskCommand = z.object({ type: z.literal('run') });
25
+ ```
25
26
 
26
- // ✓
27
+ ```ts good
27
28
  export const TaskCompletedEvent = z.object({ type: z.literal('task-completed') });
29
+ export const RunTaskCommand = z.object({ type: z.literal('run-task') });
28
30
  ```
29
31
 
30
32
  Consts without a role suffix, and role-suffixed consts without a `type` literal, are ignored.
@@ -39,8 +41,8 @@ Consts without a role suffix, and role-suffixed consts without a `type` literal,
39
41
  'noctcore-contracts/wire-message-naming': ['error', { roleSuffixes: ['Message'] }]
40
42
  ```
41
43
 
42
- ```ts
43
- // with roleSuffixes: ['Message'] → autofixes to 'task-done'
44
+ ```ts bad options={"roleSuffixes":["Message"]}
45
+ // with roleSuffixes: ['Message'], autofixes to 'task-done'
44
46
  export const TaskDoneMessage = z.object({ type: z.literal('done') });
45
47
  ```
46
48
 
@@ -17,14 +17,15 @@ For every `export const` whose initializer is rooted at the `z` identifier (`z.o
17
17
  - a correctly-named `FooSchema` must have a sibling `export type Foo` (a `type` alias or
18
18
  `interface`) — otherwise `missingType`.
19
19
 
20
- ```ts
21
- // ✗ not suffixed `Schema`
20
+ ```ts bad reports=2
21
+ // not suffixed `Schema`
22
22
  export const Task = z.object({});
23
23
 
24
- // ✗ no sibling inferred type
24
+ // no sibling inferred type
25
25
  export const TaskSchema = z.object({});
26
+ ```
26
27
 
27
- // ✓
28
+ ```ts good
28
29
  export const TaskSchema = z.object({ id: z.string() });
29
30
  export type Task = z.infer<typeof TaskSchema>;
30
31
  ```
@@ -43,7 +44,7 @@ suffixes to carve them out:
43
44
  'noctcore-contracts/zod-schema-naming': ['error', { roleSuffixes: ['Event', 'Command', 'Query'] }]
44
45
  ```
45
46
 
46
- ```ts
47
+ ```ts good options={"roleSuffixes":["Event","Command","Query"]} reconfigured
47
48
  // carved out only when 'Command' is listed in roleSuffixes
48
49
  export const RunTaskCommand = z.object({ type: z.literal('run-task') });
49
50
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noctcore/eslint-plugin-contracts",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "ESLint rules for shared contract, config, error-handling, and money-precision conventions (zod schema naming, wire discriminants, no-direct-process-env, decimal money).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,7 +46,7 @@
46
46
  "test": "vitest run"
47
47
  },
48
48
  "dependencies": {
49
- "@noctcore/eslint-utils": "^0.1.0",
49
+ "@noctcore/eslint-utils": "^0.1.1",
50
50
  "@typescript-eslint/utils": "^8.61.1"
51
51
  },
52
52
  "peerDependencies": {