@veda-hq/eslint-plugin-shuddhi 0.0.0-stage → 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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -2
  3. package/package.json +64 -3
  4. package/src/error-handling/configs.js +122 -0
  5. package/src/error-handling/error-handling.js +990 -0
  6. package/src/error-handling/index.js +12 -0
  7. package/src/index.js +54 -0
  8. package/src/input-parsing/configs.js +116 -0
  9. package/src/input-parsing/env-through-schema.js +173 -0
  10. package/src/input-parsing/helpers.js +129 -0
  11. package/src/input-parsing/index.js +57 -0
  12. package/src/input-parsing/no-hand-rolled-argv.js +208 -0
  13. package/src/input-parsing/no-regex-structured-parse.js +167 -0
  14. package/src/input-parsing/prisma-json-through-schema.js +130 -0
  15. package/src/input-parsing/schema-parse-untrusted.js +226 -0
  16. package/src/input-parsing/schema-satisfies-contract.js +163 -0
  17. package/src/input-parsing/strict-scalar-parse.js +609 -0
  18. package/src/nest-prisma/helpers.js +336 -0
  19. package/src/nest-prisma/index.js +16 -0
  20. package/src/nest-prisma/nest-prisma-rules.js +577 -0
  21. package/src/nest-prisma/no-self-mock-sut.js +225 -0
  22. package/src/nest-prisma/require-orderby-findfirst.js +68 -0
  23. package/src/server-boundary/compare-and-set.js +664 -0
  24. package/src/server-boundary/configs.js +62 -0
  25. package/src/server-boundary/dto-field-constraints.js +313 -0
  26. package/src/server-boundary/dto-implements-contract.js +81 -0
  27. package/src/server-boundary/helpers.js +53 -0
  28. package/src/server-boundary/index.js +47 -0
  29. package/src/server-boundary/nest-body-dto.js +278 -0
  30. package/src/server-boundary/no-capability-literal.js +92 -0
  31. package/src/server-boundary/no-local-api-contract.js +95 -0
  32. package/src/test-lint/c05-candidates.js +1462 -0
  33. package/src/test-lint/c05.js +423 -0
  34. package/src/test-lint/configs.js +177 -0
  35. package/src/test-lint/detox.js +124 -0
  36. package/src/test-lint/helpers.js +341 -0
  37. package/src/test-lint/index.js +227 -0
  38. package/src/test-lint/process-hygiene.js +1125 -0
  39. package/src/test-lint/sql-fake.js +175 -0
  40. package/src/test-lint/test-structure.js +1166 -0
  41. package/src/test-lint/vacuous-assertion.js +751 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Veda Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,68 @@
1
- # Temporary Holding Version
1
+ # @veda-hq/eslint-plugin-shuddhi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ शुद्धि, purification. ESLint rules for code that coding agents write and
4
+ reviewers keep sending back: errors swallowed or stringified unsafely, input
5
+ parsed by hand, NestJS and Prisma boundaries left open, and tests that pass
6
+ without proving anything.
7
+
8
+ Each rule closes a defect class that a code reviewer reported repeatedly after
9
+ push. Its suite holds those findings, reduced, as invalid cases, with the
10
+ corrected code and the near misses as valid ones.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pnpm add -D eslint @veda-hq/eslint-plugin-shuddhi
16
+ ```
17
+
18
+ ESLint 9 flat config, Node 24. `typescript` is an optional peer: only the rules
19
+ that read types (`strict-scalar-parse`, `nest-body-dto`) need it, and only
20
+ where the config gives the parser a TypeScript project.
21
+
22
+ ## Use
23
+
24
+ ```js
25
+ // eslint.config.js
26
+ import shuddhi from '@veda-hq/eslint-plugin-shuddhi';
27
+
28
+ export default [
29
+ {
30
+ files: ['**/*.ts'],
31
+ plugins: { shuddhi },
32
+ rules: {
33
+ 'shuddhi/no-swallowed-catch': 'error',
34
+ 'shuddhi/no-vacuous-assertion': 'error',
35
+ },
36
+ },
37
+ ];
38
+ ```
39
+
40
+ The plugin can be bound under any namespace: a repository that had these
41
+ rules under its own prefix keeps its rule ids (and its bulk suppressions) by
42
+ binding the plugin, or one family's `rules`, under that prefix.
43
+
44
+ Each family also exports config-block helpers that turn its rules on for a set
45
+ of globs, with a `namespace` parameter:
46
+
47
+ | Family | Rules | Helper |
48
+ | --- | --- | --- |
49
+ | error handling | `no-swallowed-catch`, `safe-log`, `no-raw-error-string`, `no-direct-child-process` | `errorHandlingBlocks` |
50
+ | input parsing | `no-hand-rolled-argv`, `env-through-schema`, `strict-scalar-parse`, `schema-parse-untrusted`, `schema-satisfies-contract`, `prisma-json-through-schema`, `no-regex-structured-parse` | `inputParsingConfigs` |
51
+ | server boundary | `nest-body-dto`, `dto-field-constraints`, `no-local-api-contract`, `dto-implements-contract`, `no-capability-literal`, `compare-and-set` | `boundaryBlocks` |
52
+ | NestJS and Prisma | `no-raw-sql-interpolation`, `nest-http-exception`, `prisma-transaction-multiwrite`, `nest-param-pipe`, `no-unbound-test-app`, `require-orderby-findfirst`, `no-self-mock-sut` | none |
53
+ | test lint | 41 rules on test structure, vacuous assertions, process hygiene, fixtures and fakes | `testLintBlocks` |
54
+
55
+ Every name of a repository's own helper (the safe-logging module, the child
56
+ process runner, the argument parser, the contracts package) is a rule option,
57
+ so the messages point at the helper your repository actually has.
58
+
59
+ ## Checks
60
+
61
+ `pnpm check`: the suites under `node --test` with at least 95% of lines and
62
+ functions and 90% of branches covered, and a smoke import of the published
63
+ entry point. The rules are plain JavaScript, shipped as written; the suites
64
+ are TypeScript that Node runs by stripping types.
65
+
66
+ ## Licence
67
+
68
+ MIT.
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@veda-hq/eslint-plugin-shuddhi",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "ESLint rules for code coding agents write: error handling, input parsing, NestJS and Prisma boundaries, and test quality. Each rule closes a defect class reviewers kept finding after push.",
5
+ "keywords": [
6
+ "eslint",
7
+ "eslintplugin",
8
+ "eslint-plugin",
9
+ "nestjs",
10
+ "prisma",
11
+ "vitest",
12
+ "testing",
13
+ "coding agents"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "engines": {
18
+ "node": ">=24.15"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/veda-hq/veda.git",
23
+ "directory": "packages/eslint-plugin-shuddhi"
24
+ },
25
+ "homepage": "https://github.com/veda-hq/veda/tree/main/packages/eslint-plugin-shuddhi#readme",
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": true
29
+ },
30
+ "exports": {
31
+ ".": "./src/index.js",
32
+ "./error-handling": "./src/error-handling/index.js",
33
+ "./error-handling/configs": "./src/error-handling/configs.js",
34
+ "./input-parsing": "./src/input-parsing/index.js",
35
+ "./nest-prisma": "./src/nest-prisma/index.js",
36
+ "./server-boundary": "./src/server-boundary/index.js",
37
+ "./server-boundary/configs": "./src/server-boundary/configs.js",
38
+ "./test-lint": "./src/test-lint/index.js",
39
+ "./test-lint/configs": "./src/test-lint/configs.js",
40
+ "./nest-prisma/helpers": "./src/nest-prisma/helpers.js"
41
+ },
42
+ "files": [
43
+ "src",
44
+ "!src/**/*.test.ts",
45
+ "!src/**/fixtures"
46
+ ],
47
+ "peerDependencies": {
48
+ "eslint": "^9.24.0",
49
+ "typescript": ">=5.4 <6"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "typescript": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@typescript-eslint/rule-tester": "8.48.0",
58
+ "eslint": "9.39.1",
59
+ "typescript": "5.9.3",
60
+ "typescript-eslint": "8.48.0"
61
+ },
62
+ "scripts": {
63
+ "test": "node --test --experimental-test-coverage --test-coverage-lines=95 --test-coverage-functions=95 --test-coverage-branches=90 --test-coverage-exclude=src/**/*.test.ts --test-coverage-exclude=src/**/fixtures/** src/**/*.test.ts",
64
+ "smoke": "node --input-type=module -e \"const p = (await import('@veda-hq/eslint-plugin-shuddhi')).default; if (Object.keys(p.rules).length === 0) throw new Error('no rules');\"",
65
+ "check": "pnpm test && pnpm smoke"
66
+ }
6
67
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Flat-config blocks for the error-handling rules (catalogue ERR-01, ERR-02
3
+ * with OBS-03, ERR-03, ERR-08).
4
+ *
5
+ * The rule ids need the plugin bound under a namespace, so a workspace takes
6
+ * them through {@link errorHandlingBlocks} rather than a bare spread. The
7
+ * namespace defaults to `shuddhi`; a project that already has these rules
8
+ * under another namespace (and suppressions recorded against those ids)
9
+ * passes its own.
10
+ */
11
+
12
+ import { rules } from './index.js';
13
+
14
+ /**
15
+ * The plugin object bound when the caller passes none. One module-level
16
+ * object, so calling the helpers twice binds the same plugin: flat config
17
+ * refuses two different objects under one namespace.
18
+ */
19
+ const familyPlugin = { rules };
20
+
21
+ /**
22
+ * @typedef {object} ErrorHandlingOptions
23
+ * @property {string} [namespace] Plugin namespace the rule ids use. Default `shuddhi`.
24
+ * @property {string} [helperModule] Module named in the safe-log and
25
+ * no-raw-error-string messages as the home of `safeLog`, `safeInvoke` and
26
+ * `errorDetail`.
27
+ * @property {string} [helperSourcePattern] Regular expression over import
28
+ * specifiers: an import of `safeLog`/`safeInvoke` from a matching module is
29
+ * the real wrapper (safe-log).
30
+ * @property {string} [runChildPath] The one file allowed to use the async
31
+ * child_process APIs (the runner itself), as a path from `root`; also named
32
+ * in the no-direct-child-process message.
33
+ * @property {string} [root] Directory `runChildPath` is relative to. Defaults
34
+ * to ESLint's working directory.
35
+ */
36
+
37
+ /** `'error'`, or `['error', options]` when any option was given. */
38
+ function entry(options) {
39
+ const given = Object.fromEntries(
40
+ Object.entries(options).filter(([, value]) => value !== undefined),
41
+ );
42
+ return Object.keys(given).length > 0 ? ['error', given] : 'error';
43
+ }
44
+
45
+ /**
46
+ * The error-handling rules as a rules object.
47
+ *
48
+ * @param {ErrorHandlingOptions} [options]
49
+ */
50
+ export function errorHandling({
51
+ namespace = 'shuddhi',
52
+ helperModule,
53
+ helperSourcePattern,
54
+ runChildPath,
55
+ root,
56
+ } = {}) {
57
+ return {
58
+ // ERR-01. A catch that neither rethrows nor rejects needs
59
+ // `// fail-open: <reason>`; `no-empty` only ever saw empty blocks.
60
+ [`${namespace}/no-swallowed-catch`]: 'error',
61
+ // ERR-02 and OBS-03, one rule: injected loggers, and console on a catch
62
+ // path, go through safeLog/safeInvoke.
63
+ [`${namespace}/safe-log`]: entry({ helperModule, helperSourcePattern }),
64
+ // ERR-03. A caught value becomes text only through errorDetail().
65
+ [`${namespace}/no-raw-error-string`]: entry({ helperModule }),
66
+ // ERR-08. Async child processes only through the runner.
67
+ [`${namespace}/no-direct-child-process`]: entry({
68
+ allow: runChildPath === undefined ? undefined : [runChildPath],
69
+ runner: runChildPath,
70
+ root,
71
+ }),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Where the error-handling rules do not apply: tests, fixtures and mocks.
77
+ * A test's catch that captures an error to assert on it is the point of the
78
+ * test, and a test harness's direct spawn is not shipped tooling. Globs are
79
+ * relative to whichever workspace config uses them.
80
+ */
81
+ const CODE_FILES = '**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}';
82
+
83
+ export const ERROR_HANDLING_EXEMPT = [
84
+ '**/*.test.*',
85
+ '**/*.spec.*',
86
+ '**/*.perf-test.*',
87
+ '**/*.fixture.*',
88
+ '**/__tests__/**',
89
+ '**/__mocks__/**',
90
+ '**/fixtures/**',
91
+ '**/test/**',
92
+ '**/e2e/**',
93
+ ];
94
+
95
+ /**
96
+ * Config blocks that turn the error-handling rules on for `files` and off for
97
+ * {@link ERROR_HANDLING_EXEMPT}. Legacy hits are best held in the workspace's
98
+ * `eslint-suppressions.json` (ESLint bulk suppressions): counts only fall.
99
+ *
100
+ * @param {import('eslint').Linter.Config['files']} files
101
+ * @param {ErrorHandlingOptions & { plugin?: import('eslint').ESLint.Plugin }} [options]
102
+ * `plugin` is the object bound under `namespace`; pass the one your config
103
+ * already binds there, if any. Defaults to a plugin of this family's rules.
104
+ */
105
+ export function errorHandlingBlocks(files, options = {}) {
106
+ const { plugin = familyPlugin, namespace = 'shuddhi', ...rest } = options;
107
+ const ruleSet = errorHandling({ namespace, ...rest });
108
+ return [
109
+ { files, plugins: { [namespace]: plugin }, rules: ruleSet },
110
+ {
111
+ // Each exemption is ANDed with a code extension (a nested array is an
112
+ // intersection in flat config). A bare `**/test/**` or `**/*.test.*`
113
+ // here would make ESLint lint every .sh and .json under those paths,
114
+ // since any `files` match marks a file as lintable.
115
+ files: ERROR_HANDLING_EXEMPT.map(glob => [glob, CODE_FILES]),
116
+ plugins: { [namespace]: plugin },
117
+ rules: Object.fromEntries(
118
+ Object.keys(ruleSet).map(rule => [rule, 'off']),
119
+ ),
120
+ },
121
+ ];
122
+ }