@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.
- package/README.md +94 -60
- package/dist/{chunk-Z7TXSZR4.js → chunk-OYFQKSJN.js} +9 -1
- package/dist/i18n.js +1 -1
- package/dist/index.js +1 -1
- package/dist/prisma.js +3 -3
- package/dist/session.cjs +297 -0
- package/dist/session.d.cts +226 -0
- package/dist/session.d.ts +226 -0
- package/dist/session.js +244 -0
- package/dist/trpc.cjs +122 -0
- package/dist/trpc.d.cts +59 -0
- package/dist/trpc.d.ts +59 -0
- package/dist/trpc.js +73 -0
- package/docs/rules/agents-doc-presence.md +13 -4
- package/docs/rules/canonical-helpers-single-home.md +13 -3
- package/docs/rules/dockerfile-base-image-digest-pin.md +17 -4
- package/docs/rules/eslint-config-no-warn.md +22 -7
- package/docs/rules/file-size-ratchet.md +19 -9
- package/docs/rules/github-actions-least-privilege-permissions.md +20 -11
- package/docs/rules/github-actions-no-template-injection.md +21 -12
- package/docs/rules/github-actions-runner-pinned.md +19 -6
- package/docs/rules/github-actions-sha-pinned.md +19 -5
- package/docs/rules/idempotency-key-parity.md +91 -0
- package/docs/rules/layer-rank.md +17 -6
- package/docs/rules/no-cloned-component-folders.md +13 -4
- package/docs/rules/no-warn-severity.md +14 -3
- package/docs/rules/package-shape.md +13 -6
- package/docs/rules/prisma-method-surface.md +19 -4
- package/docs/rules/security-scanner-version-parity.md +16 -7
- package/docs/rules/service-image-digest-pin.md +21 -7
- package/docs/rules/session-epoch-captured.md +89 -0
- package/docs/rules/session-kind-stamped.md +89 -0
- package/docs/rules/session-landing-declared.md +95 -0
- package/docs/rules/session-mint-callers.md +85 -0
- package/docs/rules/tenant-model-registry-parity.md +18 -4
- package/docs/rules/test-runner-segregation.md +15 -5
- package/docs/rules/test-sibling-enforcement.md +12 -5
- package/docs/rules/test-workspace-enrollment.md +14 -6
- package/docs/rules/translation-dead-keys.md +33 -20
- package/docs/rules/ui-primitive-shape.md +13 -4
- package/docs/rules/workspace-graph-parity.md +15 -5
- 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
|
-
##
|
|
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
|
-
|
|
63
|
+
### Worked example: a pnpm monorepo
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
##
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
69
|
-
audit
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
99
|
+
## When not to use it
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
##
|
|
52
|
+
## When not to use it
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
57
|
+
## When not to use it
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
package/docs/rules/layer-rank.md
CHANGED
|
@@ -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).
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
##
|
|
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
|
|
44
|
+
member indentation, so a non-generic member is not counted.
|
|
35
45
|
|
|
36
|
-
##
|
|
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
|
-
|
|
63
|
+
### Worked example: a monorepo with a database package
|
|
54
64
|
|
|
55
|
-
|
|
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.
|