@noctcore/eslint-plugin-contracts 0.1.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.
@@ -0,0 +1,47 @@
1
+ # `noctcore-contracts/no-error-stringify`
2
+
3
+ > Stringifying an error with `${error}`, `error.toString()`, or `error + ""` drops its cause chain.
4
+
5
+ ## Why
6
+
7
+ `` `${error}` ``, `error.toString()`, and `error + ""` all coerce an `Error` to its `message` alone,
8
+ discarding `error.cause`, the stack, and any custom fields. The value that reaches your logs is a bare
9
+ sentence with no chain to the underlying failure. The guarded extractor idiom preserves the object for
10
+ structured loggers and stays legal:
11
+
12
+ ```ts
13
+ error instanceof Error ? error.message : String(error)
14
+ ```
15
+
16
+ ## What it flags
17
+
18
+ Only the three unambiguous cause-chain-dropping forms, and only when the operand is a known error
19
+ identifier (default `error`, `err`, `e`, `cause`):
20
+
21
+ ```ts
22
+ // ✗
23
+ logger.error(`request failed: ${error}`);
24
+ const msg = err.toString();
25
+ const msg = error + "";
26
+ const msg = "" + e;
27
+
28
+ // ✓ the guarded idiom (bare String(error) is intentionally NOT policed)
29
+ const msg = error instanceof Error ? error.message : String(error);
30
+ const m = `${error.message}`;
31
+ ```
32
+
33
+ ## Options
34
+
35
+ | Option | Type | Default | Meaning |
36
+ | --- | --- | --- | --- |
37
+ | `errorIdentifierNames` | `string[]` | `['error', 'err', 'e', 'cause']` | Identifier names treated as errors for the three flagged forms. |
38
+
39
+ ```js
40
+ 'noctcore-contracts/no-error-stringify': ['error', { errorIdentifierNames: ['error', 'cause', 'failure'] }]
41
+ ```
42
+
43
+ ## When not to use it
44
+
45
+ If your codebase deliberately renders errors to strings at the boundary (and has already extracted the
46
+ cause), or names error bindings unpredictably, this rule may be noisy — narrow or widen
47
+ `errorIdentifierNames` to fit.
@@ -0,0 +1,51 @@
1
+ # `noctcore-contracts/wire-message-naming`
2
+
3
+ > A message-schema's `type` discriminant must be the kebab-case of its const name minus its role suffix. 🔧
4
+
5
+ ## Why
6
+
7
+ When wire messages are modelled as zod objects with a `type: z.literal('…')` discriminant, the const
8
+ name and the on-the-wire discriminant are two spellings of the same fact. Deriving one from the other
9
+ by convention (`TaskCompletedEvent` ⇒ `'task-completed'`) removes a whole class of copy-paste
10
+ drift where the const is renamed but the literal is not.
11
+
12
+ ## What it flags
13
+
14
+ For every `export const` whose name ends in a role suffix (default `Event` / `Command` / `Query`) and
15
+ whose zod object declares a `type: z.literal('…')` property, the literal must equal
16
+ `kebab(constName minus the role suffix)`. A mismatch is reported and **autofixed** to the expected
17
+ value.
18
+
19
+ ```ts
20
+ // ✗ camelCase discriminant → autofixes to 'task-completed'
21
+ export const TaskCompletedEvent = z.object({ type: z.literal('taskCompleted') });
22
+
23
+ // ✗ wrong value → autofixes to 'run-task'
24
+ export const RunTaskCommand = z.object({ type: z.literal('run') });
25
+
26
+ // ✓
27
+ export const TaskCompletedEvent = z.object({ type: z.literal('task-completed') });
28
+ ```
29
+
30
+ Consts without a role suffix, and role-suffixed consts without a `type` literal, are ignored.
31
+
32
+ ## Options
33
+
34
+ | Option | Type | Default | Meaning |
35
+ | --- | --- | --- | --- |
36
+ | `roleSuffixes` | `string[]` | `['Event', 'Command', 'Query']` | Const-name suffixes that mark a schema as a wire message. |
37
+
38
+ ```js
39
+ 'noctcore-contracts/wire-message-naming': ['error', { roleSuffixes: ['Message'] }]
40
+ ```
41
+
42
+ ```ts
43
+ // with roleSuffixes: ['Message'] → autofixes to 'task-done'
44
+ export const TaskDoneMessage = z.object({ type: z.literal('done') });
45
+ ```
46
+
47
+ ## When not to use it
48
+
49
+ If your messages do not carry a role-suffixed const name plus a `type: z.literal(...)` discriminant,
50
+ this rule never fires. Pair it with [`zod-schema-naming`](./zod-schema-naming.md) (listing the same
51
+ suffixes there) so every export is covered by exactly one naming contract.
@@ -0,0 +1,54 @@
1
+ # `noctcore-contracts/zod-schema-naming`
2
+
3
+ > Every exported zod schema is a PascalCase const suffixed `Schema`, paired with a same-named inferred type.
4
+
5
+ ## Why
6
+
7
+ A contracts package is a shared spine. A uniform `FooSchema` + `Foo` pairing keeps the schema and
8
+ its TypeScript type discoverable and prevents a hand-authored duplicate type from drifting away from
9
+ the schema it is supposed to mirror.
10
+
11
+ ## What it flags
12
+
13
+ For every `export const` whose initializer is rooted at the `z` identifier (`z.object(...)`,
14
+ `z.string().min(1)`, `z.union([...])`, …):
15
+
16
+ - the const name must match `^[A-Z][A-Za-z0-9]*Schema$` — otherwise `schemaNaming`;
17
+ - a correctly-named `FooSchema` must have a sibling `export type Foo` (a `type` alias or
18
+ `interface`) — otherwise `missingType`.
19
+
20
+ ```ts
21
+ // ✗ not suffixed `Schema`
22
+ export const Task = z.object({});
23
+
24
+ // ✗ no sibling inferred type
25
+ export const TaskSchema = z.object({});
26
+
27
+ // ✓
28
+ export const TaskSchema = z.object({ id: z.string() });
29
+ export type Task = z.infer<typeof TaskSchema>;
30
+ ```
31
+
32
+ ## Options
33
+
34
+ | Option | Type | Default | Meaning |
35
+ | --- | --- | --- | --- |
36
+ | `roleSuffixes` | `string[]` | `[]` | Const-name suffixes that opt a schema **out** of the `*Schema` rule (their naming is owned by [`wire-message-naming`](./wire-message-naming.md)). |
37
+
38
+ With the default empty list the base convention applies to every exported zod schema. Codebases that
39
+ model wire messages as `*Event` / `*Command` / `*Query` discriminated-union members pass those
40
+ suffixes to carve them out:
41
+
42
+ ```js
43
+ 'noctcore-contracts/zod-schema-naming': ['error', { roleSuffixes: ['Event', 'Command', 'Query'] }]
44
+ ```
45
+
46
+ ```ts
47
+ // carved out only when 'Command' is listed in roleSuffixes
48
+ export const RunTaskCommand = z.object({ type: z.literal('run-task') });
49
+ ```
50
+
51
+ ## When not to use it
52
+
53
+ If your codebase does not import zod as `z`, or does not pair schemas with inferred types, this rule
54
+ will not fit — it keys off the `z` root identifier and the `FooSchema`/`Foo` pairing convention.
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@noctcore/eslint-plugin-contracts",
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "docs",
21
+ "README.md"
22
+ ],
23
+ "sideEffects": false,
24
+ "keywords": [
25
+ "eslint",
26
+ "eslintplugin",
27
+ "eslint-plugin",
28
+ "contracts",
29
+ "zod",
30
+ "noctcore"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/noctcore/eslint-plugins.git",
39
+ "directory": "packages/eslint-plugin-contracts"
40
+ },
41
+ "homepage": "https://github.com/noctcore/eslint-plugins/tree/main/packages/eslint-plugin-contracts",
42
+ "bugs": "https://github.com/noctcore/eslint-plugins/issues",
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run"
47
+ },
48
+ "dependencies": {
49
+ "@noctcore/eslint-utils": "^0.1.0",
50
+ "@typescript-eslint/utils": "^8.61.1"
51
+ },
52
+ "peerDependencies": {
53
+ "eslint": ">=9.0.0",
54
+ "typescript": ">=5.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@noctcore/eslint-test-utils": "workspace:*",
58
+ "@types/node": "^22.0.0",
59
+ "@typescript-eslint/parser": "^8.61.1",
60
+ "@typescript-eslint/rule-tester": "^8.61.1",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.6.0",
63
+ "vitest": "^3"
64
+ }
65
+ }