@noctcore/lint-meta-rules 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.
Files changed (42) hide show
  1. package/README.md +94 -60
  2. package/dist/{chunk-Z7TXSZR4.js → chunk-OYFQKSJN.js} +9 -1
  3. package/dist/i18n.js +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/prisma.js +3 -3
  6. package/dist/session.cjs +297 -0
  7. package/dist/session.d.cts +226 -0
  8. package/dist/session.d.ts +226 -0
  9. package/dist/session.js +244 -0
  10. package/dist/trpc.cjs +122 -0
  11. package/dist/trpc.d.cts +59 -0
  12. package/dist/trpc.d.ts +59 -0
  13. package/dist/trpc.js +73 -0
  14. package/docs/rules/agents-doc-presence.md +13 -4
  15. package/docs/rules/canonical-helpers-single-home.md +13 -3
  16. package/docs/rules/dockerfile-base-image-digest-pin.md +17 -4
  17. package/docs/rules/eslint-config-no-warn.md +22 -7
  18. package/docs/rules/file-size-ratchet.md +19 -9
  19. package/docs/rules/github-actions-least-privilege-permissions.md +20 -11
  20. package/docs/rules/github-actions-no-template-injection.md +21 -12
  21. package/docs/rules/github-actions-runner-pinned.md +19 -6
  22. package/docs/rules/github-actions-sha-pinned.md +19 -5
  23. package/docs/rules/idempotency-key-parity.md +91 -0
  24. package/docs/rules/layer-rank.md +17 -6
  25. package/docs/rules/no-cloned-component-folders.md +13 -4
  26. package/docs/rules/no-warn-severity.md +14 -3
  27. package/docs/rules/package-shape.md +13 -6
  28. package/docs/rules/prisma-method-surface.md +19 -4
  29. package/docs/rules/security-scanner-version-parity.md +16 -7
  30. package/docs/rules/service-image-digest-pin.md +21 -7
  31. package/docs/rules/session-epoch-captured.md +89 -0
  32. package/docs/rules/session-kind-stamped.md +89 -0
  33. package/docs/rules/session-landing-declared.md +95 -0
  34. package/docs/rules/session-mint-callers.md +85 -0
  35. package/docs/rules/tenant-model-registry-parity.md +18 -4
  36. package/docs/rules/test-runner-segregation.md +15 -5
  37. package/docs/rules/test-sibling-enforcement.md +12 -5
  38. package/docs/rules/test-workspace-enrollment.md +14 -6
  39. package/docs/rules/translation-dead-keys.md +33 -20
  40. package/docs/rules/ui-primitive-shape.md +13 -4
  41. package/docs/rules/workspace-graph-parity.md +15 -5
  42. package/package.json +18 -8
@@ -3,6 +3,10 @@
3
3
  > Every rule in the RESOLVED ESLint config is `error` or `off`, never `warn`, including severities a
4
4
  > spread preset injects.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createEslintConfigNoWarnRule` from `@noctcore/lint-meta-rules/resolved-config` · Category `config` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  Import it from the `resolved-config` entry point, which (unlike the main one) loads ESLint:
7
11
 
8
12
  ```ts
@@ -32,11 +36,17 @@ It **fails closed**:
32
36
  breaks for one file shape cannot pass on the shapes that still work;
33
37
  - a package whose config ignores every probe is a violation, since nothing was checked.
34
38
 
35
- A probe that is merely ignored is fine while another probe resolves.
36
-
37
39
  Async: it implements the harness's `runAsync` (`@noctcore/harness` 0.3.0 or newer).
38
40
 
39
- ## Factory
41
+ ## What it does not flag
42
+
43
+ - A probe that is merely ignored, while another probe resolves.
44
+ - A preset `warn` that a later block overrides to `error` or `off`: only the resolved severity counts.
45
+ - A directory matched by `packages` that holds none of `configFiles` (it inherits a config it does not
46
+ own).
47
+ - Blocks scoped to a file shape no probe matches (`.vue`, `e2e/**`) until you add a probe for it.
48
+
49
+ ## Options
40
50
 
41
51
  ```ts
42
52
  createEslintConfigNoWarnRule(options?: EslintConfigNoWarnOptions): IMetaRule
@@ -50,10 +60,10 @@ createEslintConfigNoWarnRule(options?: EslintConfigNoWarnOptions): IMetaRule
50
60
  | `probes` | `string[]` | `src/__lint_meta_probe__.{ts,tsx,test.ts,test.tsx}` | Files, relative to each package, the config is resolved for. Add one per file shape your config scopes blocks to (`.js`, `.vue`, `e2e/**`). |
51
61
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
52
62
 
53
- ## Worked example: a pnpm monorepo
63
+ ### Worked example: a pnpm monorepo
54
64
 
55
- Settly-style layout, where every app and package owns a config built from a shared
56
- `@repo/eslint-config`, and the root config only lints tooling:
65
+ A layout where every app and package owns a config built from a shared `@repo/eslint-config`, and the
66
+ root config only lints tooling:
57
67
 
58
68
  ```ts
59
69
  createEslintConfigNoWarnRule({ packages: ['apps/*', 'packages/*'] });
@@ -63,9 +73,14 @@ Deleting the three `react-hooks/*` overrides from the shared React config (so th
63
73
  `recommended-latest` preset's `warn` shows through, with no `warn` literal anywhere) reports three
64
74
  rules in each of the three packages that spread it.
65
75
 
66
- ## Notes
76
+ ### Notes
67
77
 
68
78
  - The rule resolves with the `eslint` that `@noctcore/lint-meta-rules` resolves, which is the
69
79
  consumer's own install when it is hoisted.
70
80
  - If a config imports a workspace package that must be built first, build it before lint-meta, or
71
81
  the rule reports that the config could not be resolved.
82
+
83
+ ## When not to use it
84
+
85
+ If your repo has no flat ESLint config per package, or you cannot build the workspace packages a
86
+ config imports before lint-meta runs, rely on [`no-warn-severity`](./no-warn-severity.md) alone.
@@ -2,10 +2,13 @@
2
2
 
3
3
  > Source files stay at or under a line cap, with a one-way, self-tightening baseline ratchet.
4
4
 
5
- This single factory subsumes **both** of nightcore's file-size rules — `web-file-size-ratchet`
6
- (apps/web/src, `.ts`+`.tsx`) and `engine-file-size-ratchet` (packages/engine/src, `.ts`) — which were
7
- byte-identical logic differing only in scanned roots, extensions and exclusions. Create one instance
8
- per capped area, each with its own `id` (the `id` names both the rule and its committed baseline file).
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createFileSizeRatchetRule` from `@noctcore/lint-meta-rules` · Category `source-text` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
9
+ One factory covers every capped area: create one instance per area (an app's `src`, a package's
10
+ `src`), each with its own roots, extensions, exclusions and `id`. The `id` names both the rule and its
11
+ committed baseline file.
9
12
 
10
13
  ## Why
11
14
 
@@ -21,14 +24,23 @@ Measured in raw physical lines (`wc -l` semantics). Against a committed baseline
21
24
 
22
25
  - a **new** over-cap file (not in the baseline), or a baselined one that **grew** past its frozen
23
26
  count — a live violation;
24
- - a baselined file still within its frozen count — **grandfathered** (a stderr notice, no violation);
25
27
  - **self-tightening**: a baseline entry whose file is gone, is now at/under the cap, or shrank far
26
28
  below its frozen value (`< frozen * tightenRatio`) is itself a violation demanding a baseline update.
27
29
 
28
30
  The rule implements `baseline(ctx)` (the `IMetaRule` ratchet hook), which snapshots the current
29
31
  offender map so the runner can regenerate the frozen file.
30
32
 
31
- ## Factory
33
+ ## What it does not flag
34
+
35
+ - A file at or under `cap`.
36
+ - A baselined file still within its frozen count: it is **grandfathered** (a stderr notice, no
37
+ violation). One that shrank only a little (still at or above `frozen * tightenRatio`) stays
38
+ grandfathered too.
39
+ - Paths containing an `excludeContains` fragment (tests, specs, stories by default) or starting with an
40
+ `excludePrefixes` entry, and files whose extension is not in `extensions`.
41
+ - Anything at all when `roots` is empty: the rule is inert until you name a root.
42
+
43
+ ## Options
32
44
 
33
45
  ```ts
34
46
  createFileSizeRatchetRule(options?: FileSizeRatchetOptions): IMetaRule
@@ -46,7 +58,7 @@ createFileSizeRatchetRule(options?: FileSizeRatchetOptions): IMetaRule
46
58
  | `baselineDir` | `string` | `.nightcore/lint-meta/baselines` | Where committed baselines live (from `@noctcore/harness`). |
47
59
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
48
60
 
49
- ### Example: the two nightcore instances
61
+ ### Example: two capped areas
50
62
 
51
63
  ```ts
52
64
  createFileSizeRatchetRule({
@@ -64,8 +76,6 @@ createFileSizeRatchetRule({
64
76
  });
65
77
  ```
66
78
 
67
- De-projected from nightcore, which hardcoded the roots, extensions, exclusions and the `400` cap.
68
-
69
79
  ## When not to use it
70
80
 
71
81
  If you have no over-cap files and no desire to cap file size, skip it. If you want a hard cap with no
@@ -2,6 +2,10 @@
2
2
 
3
3
  > A workflow's top-level `permissions:` exists, is not `write-all` / `read-all`, and grants no write.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createGithubActionsLeastPrivilegePermissionsRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  The top-level `permissions:` is the `GITHUB_TOKEN` every job gets unless the job says otherwise. A
@@ -13,6 +17,13 @@ cheapest containment there is.
13
17
 
14
18
  Keep the top level read-only and grant each write on the job that needs it.
15
19
 
20
+ ### Prior art
21
+
22
+ It mirrors zizmor's [`excessive-permissions`](https://docs.zizmor.sh/audits/#excessive-permissions)
23
+ audit and the OpenSSF Scorecard
24
+ [Token-Permissions](https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions)
25
+ check.
26
+
16
27
  ## What it flags
17
28
 
18
29
  - A workflow with no top-level `permissions:`, naming each job that has no job-level
@@ -40,14 +51,18 @@ jobs:
40
51
  id-token: write
41
52
  ```
42
53
 
43
- ## What it leaves alone
54
+ ## What it does not flag
44
55
 
45
56
  - A workflow with no top-level block when every job declares its own `permissions:`: the default
46
57
  token then reaches no job.
47
58
  - `permissions: {}`, and any scope at `read` or `none`.
48
59
  - Writes on a job, a `permissions:` input under a step's `with:`, and a commented-out line.
49
60
 
50
- ## Factory
61
+ Line-based text, not a YAML parse. It checks the top level only; a job that grants itself more than
62
+ it uses is not judged. A reusable workflow (`on: workflow_call`) is held to the same bar, although
63
+ its token can never exceed its caller's.
64
+
65
+ ## Options
51
66
 
52
67
  ```ts
53
68
  createGithubActionsLeastPrivilegePermissionsRule(options?: GithubActionsLeastPrivilegePermissionsOptions): IMetaRule
@@ -59,13 +74,7 @@ createGithubActionsLeastPrivilegePermissionsRule(options?: GithubActionsLeastPri
59
74
  | `allowTopLevelWrite` | `string[]` | `[]` | Scopes allowed at `write` in the top-level block, for a repo that accepts, say, `contents: write` on a single-job release workflow. |
60
75
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
61
76
 
62
- ## Limits
63
-
64
- Line-based text, not a YAML parse. It checks the top level only; a job that grants itself more than
65
- it uses is not judged. A reusable workflow (`on: workflow_call`) is held to the same bar, although
66
- its token can never exceed its caller's.
77
+ ## When not to use it
67
78
 
68
- Prior art: zizmor's [`excessive-permissions`](https://docs.zizmor.sh/audits/#excessive-permissions)
69
- audit and the OpenSSF Scorecard
70
- [Token-Permissions](https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions)
71
- check.
79
+ If your repo has no GitHub Actions workflows there is nothing to check. If zizmor's
80
+ `excessive-permissions` audit already gates CI, one of the two is enough.
@@ -2,6 +2,10 @@
2
2
 
3
3
  > `run:` scripts and `actions/github-script` bodies never expand attacker-controllable `${{ }}` context.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createGithubActionsNoTemplateInjectionRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  GitHub substitutes `${{ }}` into a `run:` script before the shell sees it. A PR titled
@@ -14,6 +18,12 @@ JavaScript built from the substituted text.
14
18
  The fix is to pass the value through `env:` and read it as a variable. An environment variable is
15
19
  data; the shell never parses its contents as script.
16
20
 
21
+ ### Prior art
22
+
23
+ It mirrors zizmor's [`template-injection`](https://docs.zizmor.sh/audits/#template-injection) audit
24
+ and GitHub's
25
+ [security hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections).
26
+
17
27
  ## What it flags
18
28
 
19
29
  A `${{ }}` expression inside a `run:` value (inline, a `|` / `>` block, or a multi-line plain
@@ -53,7 +63,7 @@ carries the 1-indexed line.
53
63
  script: console.log(process.env.BODY)
54
64
  ```
55
65
 
56
- ## What it leaves alone
66
+ ## What it does not flag
57
67
 
58
68
  - The same expression under `env:`, `with:` (other than a github-script `script:`), `if:`,
59
69
  `name:` or `defaults.run`. Those are not parsed as script.
@@ -65,7 +75,13 @@ carries the 1-indexed line.
65
75
  strips before GitHub sees the value.
66
76
  - Paths with a `node_modules`, `.git`, `dist`, `.turbo` or `coverage` segment.
67
77
 
68
- ## Factory
78
+ Line-based text, not a YAML parse. An expression split across lines, the bracket form
79
+ (`github.event['issue']['title']`), `toJSON(github.event)` and a value laundered through `env.*`
80
+ set from event text are not seen. An expression that only tests a tainted field
81
+ (`${{ contains(github.event.issue.title, 'x') }}`) evaluates to a boolean but is still reported;
82
+ move the test to `if:` or into the script.
83
+
84
+ ## Options
69
85
 
70
86
  ```ts
71
87
  createGithubActionsNoTemplateInjectionRule(options?: GithubActionsNoTemplateInjectionOptions): IMetaRule
@@ -80,14 +96,7 @@ createGithubActionsNoTemplateInjectionRule(options?: GithubActionsNoTemplateInje
80
96
  | `checkStepOutputs` | `boolean` | `false` | Treat `steps.*.outputs.*` as attacker-controlled. Turn it on when steps echo event text into outputs. |
81
97
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
82
98
 
83
- ## Limits
99
+ ## When not to use it
84
100
 
85
- Line-based text, not a YAML parse. An expression split across lines, the bracket form
86
- (`github.event['issue']['title']`), `toJSON(github.event)` and a value laundered through `env.*`
87
- set from event text are not seen. An expression that only tests a tainted field
88
- (`${{ contains(github.event.issue.title, 'x') }}`) evaluates to a boolean but is still reported;
89
- move the test to `if:` or into the script.
90
-
91
- Prior art: zizmor's [`template-injection`](https://docs.zizmor.sh/audits/#template-injection) audit
92
- and GitHub's
93
- [security hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections).
101
+ If your repo has no GitHub Actions workflows or composite actions there is nothing to check. If
102
+ zizmor's `template-injection` audit already gates CI, one of the two is enough.
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Workflow jobs run on a named runner image, never a `*-latest` label.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createGithubActionsRunnerPinnedRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  GitHub repoints `ubuntu-latest` (and `macos-latest`, `windows-latest`) to a new OS image on its own
@@ -11,8 +15,7 @@ repo. Pinning a named image makes the move a reviewed diff.
11
15
  ## What it flags
12
16
 
13
17
  Every floating label in a `runs-on:` value, read as a scalar, a flow list (`[a, b]`), a block list, or
14
- a `group:` / `labels:` mapping. An expression (`${{ matrix.os }}`) cannot be judged from the text and
15
- is left alone; a commented-out `runs-on:` is ignored.
18
+ a `group:` / `labels:` mapping.
16
19
 
17
20
  ```yaml
18
21
  # Bad
@@ -24,7 +27,17 @@ runs-on: ubuntu-24.04
24
27
  runs-on: ${{ matrix.os }}
25
28
  ```
26
29
 
27
- ## Factory
30
+ ## What it does not flag
31
+
32
+ - A named image (`ubuntu-24.04`), quoted or with a trailing comment, and a custom label such as
33
+ `self-hosted`.
34
+ - An expression (`${{ matrix.os }}`): it cannot be judged from the text.
35
+ - A commented-out `runs-on:`, and a later key in the job (`name: ubuntu-latest`) after the label list.
36
+
37
+ Line-based text, not a YAML parse. A matrix value such as `os: [ubuntu-latest]` that feeds
38
+ `runs-on: ${{ matrix.os }}` is not checked.
39
+
40
+ ## Options
28
41
 
29
42
  ```ts
30
43
  createGithubActionsRunnerPinnedRule(options?: GithubActionsRunnerPinnedOptions): IMetaRule
@@ -36,7 +49,7 @@ createGithubActionsRunnerPinnedRule(options?: GithubActionsRunnerPinnedOptions):
36
49
  | `floatingLabel` | `RegExp` | `/^[\w.-]+-latest$/u` | A label matching this is floating. Do not pass a `g`-flagged regex. |
37
50
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
38
51
 
39
- ## Limits
52
+ ## When not to use it
40
53
 
41
- Line-based text, not a YAML parse. A matrix value such as `os: [ubuntu-latest]` that feeds
42
- `runs-on: ${{ matrix.os }}` is not checked.
54
+ If you want your CI to track GitHub's newest runner images automatically and accept the unreviewed
55
+ moves, skip it.
@@ -2,6 +2,10 @@
2
2
 
3
3
  > GitHub Actions `uses:` refs are pinned to a 40-character commit SHA with a `# vN` comment.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createGithubActionsShaPinnedRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  A tag or branch ref is a moving target: whoever controls the action's repository can repoint it, and
@@ -17,7 +21,7 @@ Every `uses:` line (step-level and job-level reusable workflow calls) in the sca
17
21
  - a SHA-pinned ref with no `# vN` comment,
18
22
  - a `docker://` ref with no `@sha256:<digest>`.
19
23
 
20
- Local actions (`uses: ./path`) are exempt. Each violation carries the 1-indexed line.
24
+ Each violation carries the 1-indexed line.
21
25
 
22
26
  ```yaml
23
27
  # Bad
@@ -29,7 +33,17 @@ Local actions (`uses: ./path`) are exempt. Each violation carries the 1-indexed
29
33
  - uses: ./.github/actions/setup
30
34
  ```
31
35
 
32
- ## Factory
36
+ ## What it does not flag
37
+
38
+ - Local actions (`uses: ./path`).
39
+ - A full 40-character SHA with a `# vN` comment, quoted or not, including a path inside the action
40
+ repo (`github/codeql-action/analyze@<sha> # v3`).
41
+ - A `docker://` ref pinned by `@sha256:<digest>`.
42
+
43
+ The check is line-based text, not a YAML parse. A `uses:` value split across lines, or built from an
44
+ expression, is not seen.
45
+
46
+ ## Options
33
47
 
34
48
  ```ts
35
49
  createGithubActionsShaPinnedRule(options?: GithubActionsShaPinnedOptions): IMetaRule
@@ -40,7 +54,7 @@ createGithubActionsShaPinnedRule(options?: GithubActionsShaPinnedOptions): IMeta
40
54
  | `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
41
55
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
42
56
 
43
- ## Limits
57
+ ## When not to use it
44
58
 
45
- The check is line-based text, not a YAML parse. A `uses:` value split across lines, or built from an
46
- expression, is not seen.
59
+ If your repo has no GitHub Actions workflows, or you accept tag refs and review action updates some
60
+ other way, skip it.
@@ -0,0 +1,91 @@
1
+ # `idempotency-key-parity`
2
+
3
+ > A procedure guarded by an idempotency middleware has a client caller that sends the key, or no
4
+ > client caller at all.
5
+
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createIdempotencyKeyParityRule` from `@noctcore/lint-meta-rules/trpc` · Category `source-text` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
10
+ Import it from the `trpc` entry point:
11
+
12
+ ```ts
13
+ import { createIdempotencyKeyParityRule } from '@noctcore/lint-meta-rules/trpc';
14
+ ```
15
+
16
+ ## Why
17
+
18
+ An idempotency middleware only de-duplicates when the client sends a key, and neither half fails when
19
+ the other is missing. The server passes the request straight through, the client gets a normal
20
+ response, and both test suites pass. A guard nobody sends a key to is decoration advertising
21
+ double-submit protection that does not exist. One audit of a production API found 14 of 30 guarded
22
+ procedures whose only web caller had never sent a key.
23
+
24
+ The gap is only visible by holding the two lists side by side, which is what this rule does.
25
+
26
+ ## What it flags
27
+
28
+ For every method in a `routerGlobs` file decorated with a `@<middlewareDecorator>(...)` list naming
29
+ `middleware` (as a whole word), the rule derives `<alias>.<method>` from the class's
30
+ `@<routerDecorator>({ <aliasKey>: '...' })`. If some `clientGlobs` file contains
31
+ `<clientPrefix><alias>.<method>` and none of those files contains `keyToken`, the router file is
32
+ reported.
33
+
34
+ The method is read from the decorator to the next `async <name>(`, the shape `nestjs-trpc` routers
35
+ take.
36
+
37
+ ## What it does not flag
38
+
39
+ - A guarded procedure with no client caller: the guard is correct in advance of the screen that will
40
+ use it, and demanding a caller would be demanding the screen.
41
+ - A procedure without the middleware, even when a client sends it a key: the header is ignored, so it
42
+ is inert rather than misleading.
43
+ - A middleware whose name merely starts with the configured one (`IdempotencyMiddlewareLegacy`).
44
+ - Procedures listed in `exempt`, and a router file with no alias.
45
+
46
+ A key sent from a DIFFERENT client file than the one naming the call counts, and so does a client test
47
+ file, if `clientGlobs` match it: the check is per procedure, across the client tree.
48
+
49
+ With no `middleware` the rule is inert.
50
+
51
+ ## Options
52
+
53
+ ```ts
54
+ createIdempotencyKeyParityRule(options?: IdempotencyKeyParityOptions): IMetaRule
55
+ ```
56
+
57
+ | Option | Type | Default | Meaning |
58
+ | --- | --- | --- | --- |
59
+ | `id` | `string` | `'idempotency-key-parity'` | Rule id. |
60
+ | `middleware` | `string` | none (inert) | The middleware class that de-duplicates on a client-sent key. |
61
+ | `routerGlobs` | `string[]` | `[]` | Server router files. |
62
+ | `clientGlobs` | `string[]` | `[]` | Every client file that can call a procedure or send a key. |
63
+ | `keyToken` | `string` | `'idempotencyKey'` | The token a client file must mention to count as sending a key. |
64
+ | `clientPrefix` | `string` | `'trpc.'` | What precedes `<alias>.<method>` at a client call site. |
65
+ | `routerDecorator` | `string` | `'Router'` | The class decorator that names the router. |
66
+ | `aliasKey` | `string` | `'alias'` | The key in that decorator's object holding the alias. |
67
+ | `middlewareDecorator` | `string` | `'UseMiddlewares'` | The method decorator listing a procedure's middlewares. |
68
+ | `exempt` | `string[]` | `[]` | `<alias>.<method>` procedures whose caller deliberately sends no key. Keep it empty: the honest fix is to drop the middleware. |
69
+ | `skipDirs` | `string[]` | `node_modules`, `.git`, `dist`, `.turbo`, `coverage` | Path segments skipped. |
70
+ | `hint` | `string` | none | Appended to every message: how this project threads the key. |
71
+ | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
72
+
73
+ ### Worked example: an app with an idempotency middleware
74
+
75
+ A NestJS API guards its mutations with an `IdempotencyMiddleware` in `*.router.ts` files, and a web app
76
+ calls them through tRPC hooks. The `hint` tells whoever hits a violation how the web app threads the
77
+ key:
78
+
79
+ ```ts
80
+ createIdempotencyKeyParityRule({
81
+ middleware: 'IdempotencyMiddleware',
82
+ routerGlobs: ['apps/api/src/**/*.router.ts'],
83
+ clientGlobs: ['apps/web/src/**/*.{ts,tsx}'],
84
+ hint: "Thread `trpc: { context: { idempotencyKey } }` through the hook's mutationOptions (see hooks/use-idempotency-key.ts).",
85
+ });
86
+ ```
87
+
88
+ ## When not to use it
89
+
90
+ If the key is generated server-side or by a client interceptor on every mutation, no call site can
91
+ forget it.
@@ -3,6 +3,10 @@
3
3
  > A module may import only strictly-lower-ranked `<scope>` packages — equal (sideways) or higher
4
4
  > (upward) is forbidden.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createLayerRankRule` from `@noctcore/lint-meta-rules` · Category `source-text` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  ## Why
7
11
 
8
12
  Layered architectures encode a fixed dependency direction (e.g. `contracts → shared → storage/skills
@@ -15,11 +19,19 @@ are caught before they calcify.
15
19
  Each source file is assigned an **importer rank**: surface files (matched by `surfacePrefix`) get
16
20
  `surfaceRank`; otherwise the `packageDirPattern` capture is looked up in `ranks`. For each
17
21
  `<scope>/<pkg>` import whose target is also ranked, the rule flags it when the target rank is `>=` the
18
- importer rank (sideways when equal, upward when greater). Unranked importers and unranked targets are
19
- skipped, so a package outside the documented spine never produces a false positive. Test files are
20
- skipped.
22
+ importer rank (sideways when equal, upward when greater).
23
+
24
+ ## What it does not flag
25
+
26
+ - A downward import: the target's rank is strictly lower than the importer's.
27
+ - Unranked importers and unranked targets, so a package outside the documented spine never produces a
28
+ false positive. Surface files are unranked unless `surfaceRank` is set.
29
+ - Test files (`.test.ts`, `.test.tsx`).
30
+ - Imports without a `from` clause (`import '<scope>/x'`, `import()`, `require()`): only
31
+ `from '<scope>/<pkg>'` is read.
32
+ - Anything at all while `ranks` is empty.
21
33
 
22
- ## Factory
34
+ ## Options
23
35
 
24
36
  ```ts
25
37
  createLayerRankRule(options?: LayerRankOptions): IMetaRule
@@ -35,8 +47,7 @@ createLayerRankRule(options?: LayerRankOptions): IMetaRule
35
47
  | `sourceGlobs` | `string[]` | packages + apps `src/**` `.ts`/`.tsx` | Source files to scan. |
36
48
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
37
49
 
38
- De-projected from nightcore, which hardcoded a fixed rank table and the `@nightcore` scope. `ranks`
39
- defaults to `{}` so the rule is inert until a consumer supplies its own layering.
50
+ `ranks` defaults to `{}`, so the rule is inert until you supply your own layering.
40
51
 
41
52
  ## When not to use it
42
53
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  > A component folder name may exist under only one feature.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createNoClonedComponentFoldersRule` from `@noctcore/lint-meta-rules` · Category `source-text` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  Same-named component folders across features are how sibling drift starts: a component cloned into a
@@ -16,7 +20,15 @@ A `<feature>/<Name>/<barrelFile>` path marks a component folder. Grouping by `<N
16
20
  `allowedClones` — is flagged. The allowlist freezes today's clone groups and only shrinks: an
17
21
  `allowedClones` entry whose clone group no longer exists is itself flagged as stale.
18
22
 
19
- ## Factory
23
+ ## What it does not flag
24
+
25
+ - A component folder name that exists under only one feature.
26
+ - Folders under `excludedFeatures` (`ui` and `app` by default).
27
+ - Names in `allowedClones` while their clone group still exists.
28
+ - Folders without the `barrelFile`, and folders nested deeper than `<feature>/<Name>/` under
29
+ `componentsRoot`.
30
+
31
+ ## Options
20
32
 
21
33
  ```ts
22
34
  createNoClonedComponentFoldersRule(options?: NoClonedComponentFoldersOptions): IMetaRule
@@ -31,9 +43,6 @@ createNoClonedComponentFoldersRule(options?: NoClonedComponentFoldersOptions): I
31
43
  | `sharedDest` | `string` | `'components/ui'` | Where a shared surface should be hoisted (used in the message). |
32
44
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
33
45
 
34
- De-projected from nightcore, which hardcoded `apps/web/src/components`, the `ui`/`app` excluded
35
- features, and a fixed `ALLOWED_CLONES` set.
36
-
37
46
  ## When not to use it
38
47
 
39
48
  If your components are not organized as `<feature>/<Component>/` folders under one root, or you
@@ -2,6 +2,10 @@
2
2
 
3
3
  > ESLint severity is `error` or `off`, never `warn`.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createNoWarnSeverityRule` from `@noctcore/lint-meta-rules` · Category `config` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  Agents iterate by reading CI failures. A `warn` severity is a silent miss — it neither fails the
@@ -14,7 +18,16 @@ Reads each configured flat-config file (skipping any that do not exist), strips
14
18
  reports any line containing a `'warn'` / `"warn"` severity literal. The violation carries the
15
19
  1-indexed line number.
16
20
 
17
- ## Factory
21
+ ## What it does not flag
22
+
23
+ - A `'warn'` inside a `//` line comment.
24
+ - The numeric severity `1`: only the quoted `'warn'` / `"warn"` literal is matched.
25
+ - A `warn` a spread preset injects, since no file the project owns spells it. Use
26
+ [`eslint-config-no-warn`](./eslint-config-no-warn.md) for the resolved config.
27
+ - Config files not listed in `configFiles` (the defaults are the root flat configs), and listed ones that
28
+ do not exist.
29
+
30
+ ## Options
18
31
 
19
32
  ```ts
20
33
  createNoWarnSeverityRule(options?: NoWarnSeverityOptions): IMetaRule
@@ -25,8 +38,6 @@ createNoWarnSeverityRule(options?: NoWarnSeverityOptions): IMetaRule
25
38
  | `configFiles` | `string[]` | `['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs']` | Flat-config files to scan; each is read only if present. |
26
39
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
27
40
 
28
- De-projected from nightcore, which hardcoded `eslint.config.mjs`.
29
-
30
41
  ## When not to use it
31
42
 
32
43
  If your project deliberately uses `warn` as an in-editor nudge that is not meant to gate CI, this rule
@@ -3,6 +3,10 @@
3
3
  > Every workspace is named `<scope>/<dir>`; library packages expose a barrel and point their build
4
4
  > fields at the built output.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createPackageShapeRule` from `@noctcore/lint-meta-rules` · Category `config` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  ## Why
7
11
 
8
12
  A monorepo stays navigable when a package's identity is mechanical: its npm name matches its folder,
@@ -20,10 +24,16 @@ For each `package.json` matched by `libraryGlobs` (full checks) or `appGlobs` (n
20
24
  `distMarker`, and `exports` must reference it.
21
25
  - **invalid JSON** — reported as a violation.
22
26
 
23
- App/surface packages are deployable entrypoints (vite/tauri/bun), so only the name check applies to
24
- them.
27
+ ## What it does not flag
28
+
29
+ - Barrel and build-field problems in app/surface packages: they are deployable entrypoints
30
+ (vite/tauri/bun), so only the name check applies to them.
31
+ - A package whose directory has an `externalNames` entry and whose name equals it.
32
+ - A missing `distFields` entry or a missing `exports`: only fields that are present are checked, and
33
+ `exports` passes when any of its targets contains `distMarker`.
34
+ - `package.json` files outside `libraryGlobs` and `appGlobs`.
25
35
 
26
- ## Factory
36
+ ## Options
27
37
 
28
38
  ```ts
29
39
  createPackageShapeRule(options?: PackageShapeOptions): IMetaRule
@@ -40,9 +50,6 @@ createPackageShapeRule(options?: PackageShapeOptions): IMetaRule
40
50
  | `distFields` | `string[]` | `['main','module','types']` | `package.json` string fields that must point at built output. |
41
51
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
42
52
 
43
- De-projected from nightcore, which hardcoded the `@nightcore` scope, the `packages/*` vs `apps/*`
44
- split, the `src/index.ts` barrel and the `dist/` marker.
45
-
46
53
  ## When not to use it
47
54
 
48
55
  If your packages are not scope-named after their folders, or libraries publish raw source (no build
@@ -3,6 +3,10 @@
3
3
  > The Prisma reads and writes your rules police partition the generated client's `<Model>Delegate`
4
4
  > method surface exactly, so a Prisma upgrade cannot add an unguarded method.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createPrismaMethodSurfaceRule` from `@noctcore/lint-meta-rules/prisma` · Category `config` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  Import it from the `prisma` entry point:
7
11
 
8
12
  ```ts
@@ -30,10 +34,16 @@ configured reads and writes are exactly its delegate surface.
30
34
  It **fails closed**: no generated client under `clientGlobs` is a violation, because a checkout that
31
35
  never ran `prisma generate` is exactly where drift hides. Run `prisma generate` before lint-meta.
32
36
 
37
+ ## What it does not flag
38
+
39
+ - A generated client whose delegates expose exactly the configured reads and writes, in either the
40
+ per-model (`prisma-client`) or the single-file (`prisma-client-js`) layout.
41
+ - Members that are not query methods: `fields`, the symbol brand and `$`-prefixed members.
42
+
33
43
  Members are read line by line: a delegate method is a generic, `name<T ...>(...)`, at the interface's
34
- member indentation. `fields`, the symbol brand and `$`-prefixed members are not query methods.
44
+ member indentation, so a non-generic member is not counted.
35
45
 
36
- ## Factory
46
+ ## Options
37
47
 
38
48
  ```ts
39
49
  createPrismaMethodSurfaceRule(options?: PrismaMethodSurfaceOptions): IMetaRule
@@ -50,9 +60,9 @@ createPrismaMethodSurfaceRule(options?: PrismaMethodSurfaceOptions): IMetaRule
50
60
  With the defaults, the rule checks the exact lists `@noctcore/eslint-plugin-prisma`'s rules read.
51
61
  If your own rules read their own lists, pass those.
52
62
 
53
- ## Worked example: Settly
63
+ ### Worked example: a monorepo with a database package
54
64
 
55
- Settly's schema generates into `packages/database/generated/prisma` with the `prisma-client`
65
+ A schema that generates into `packages/database/generated/prisma` with the `prisma-client`
56
66
  generator:
57
67
 
58
68
  ```ts
@@ -63,3 +73,8 @@ createPrismaMethodSurfaceRule({
63
73
 
64
74
  Adding a method to every generated delegate reports it as unguarded; adding it to one model only
65
75
  reports that delegate as divergent; removing the generated folder reports that no client was found.
76
+
77
+ ## When not to use it
78
+
79
+ If no lint rule of yours reads a list of Prisma method names (no tenant, writer or transaction fence),
80
+ there is no list to keep in step, so skip it.