@noctcore/lint-meta-rules 0.6.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 +83 -73
- 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 +13 -5
- 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 +15 -9
- package/docs/rules/session-kind-stamped.md +15 -7
- package/docs/rules/session-landing-declared.md +13 -5
- package/docs/rules/session-mint-callers.md +11 -3
- 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 +4 -4
package/README.md
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
1
|
# @noctcore/lint-meta-rules
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Whole-repo checks that ESLint's one-file-at-a-time model cannot make: every workspace is named by
|
|
4
|
+
convention, every imported workspace package is a declared dependency, every GitHub Action is pinned
|
|
5
|
+
to a commit SHA, no ESLint rule resolves to `warn`, no source file grows past a line cap, and more.
|
|
6
|
+
This is not an ESLint plugin; the rules run under the
|
|
7
|
+
[`@noctcore/harness`](https://www.npmjs.com/package/@noctcore/harness) `lint-meta` runner.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
contract published by `@noctcore/harness`: a pure function of an `IMetaCtx` returning `IViolation[]`.
|
|
9
|
+
**Docs:** [noctcore.github.io/eslint-plugins/packages/lint-meta-rules](https://noctcore.github.io/eslint-plugins/packages/lint-meta-rules/)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Not a good fit if you are not running the harness.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
registry. The `@noctcore/harness` `lint-meta` subcommand runs a *bounded eval* that executes only one
|
|
14
|
-
local `.nightcore/lint-meta/registry.js` and never resolves arbitrary imports — a deliberate security
|
|
15
|
-
boundary, since that file runs inside a foreign CI. So the intended integration point is nightcore's
|
|
16
|
-
harness **export pipeline**: it reads a rule's source here, inlines/transforms it, and emits flat
|
|
17
|
-
JavaScript directly into a consumer's `.nightcore/lint-meta/`. No consumer registry imports this
|
|
18
|
-
package at runtime.
|
|
13
|
+
## What it checks
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
- **config**: workspace package names and build fields, the workspace dependency graph against
|
|
16
|
+
imports and tsconfig references, ESLint severities.
|
|
17
|
+
- **source-text**: layering between packages, a file-size ratchet, agent-doc presence, colocated
|
|
18
|
+
tests, one home per helper, no cloned component folders, UI primitive shape.
|
|
19
|
+
- **testing**: every tested package is enrolled in the aggregate test script, and test runners are
|
|
20
|
+
not mixed within a package.
|
|
21
|
+
- **ci**: GitHub Actions pinned by SHA and to named runners, no template injection, least-privilege
|
|
22
|
+
permissions, container images pinned by digest, and the secret scanner pinned to one version.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Every check is listed in [Rules](#rules), with a page per rule.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
bun add -D @noctcore/harness @noctcore/lint-meta-rules # or npm i -D / pnpm add -D
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `/i18n`, `/resolved-config` and `/prisma` entry points also need the optional peer `eslint`, and
|
|
33
|
+
`/i18n` needs `@typescript-eslint/parser` (see [Rules](#rules)).
|
|
34
|
+
|
|
35
|
+
## Run with the harness
|
|
36
|
+
|
|
37
|
+
Each rule implements the [`IMetaRule`](https://www.npmjs.com/package/@noctcore/harness) contract
|
|
38
|
+
from `@noctcore/harness`: a pure function of an `IMetaCtx` returning `IViolation[]`. The harness
|
|
39
|
+
`lint-meta` subcommand runs them.
|
|
40
|
+
|
|
41
|
+
You do not `require()` this package from your repo. The harness executes only one local file,
|
|
42
|
+
`.nightcore/lint-meta/registry.js`, and never resolves arbitrary imports, because that file runs
|
|
43
|
+
inside CI. Its **export pipeline** reads a rule's source from this package, inlines it and writes
|
|
44
|
+
plain JavaScript into your repo's `.nightcore/lint-meta/`. The package is on npm for versioning and
|
|
45
|
+
discoverability.
|
|
46
|
+
|
|
47
|
+
### Every rule is a factory
|
|
48
|
+
|
|
49
|
+
`IMetaRule.run(ctx)` takes no config, so each rule is exported as a **factory**,
|
|
50
|
+
`createXRule(options): IMetaRule`. Anything project-specific (a workspace scope, a source root, a
|
|
51
|
+
rank table) is a typed option with a default. Some defaults, such as the `@nightcore` scope and the
|
|
52
|
+
400-line cap, are only starting points: set your own. A programmatic caller (or the export pipeline)
|
|
53
|
+
constructs each rule with your options:
|
|
30
54
|
|
|
31
55
|
```ts
|
|
32
56
|
import { createPackageShapeRule, createFileSizeRatchetRule } from '@noctcore/lint-meta-rules';
|
|
@@ -42,33 +66,43 @@ over the whole catalog.
|
|
|
42
66
|
|
|
43
67
|
## Rules
|
|
44
68
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
| Factory |
|
|
52
|
-
| --- | --- | --- | --- |
|
|
53
|
-
| [`
|
|
54
|
-
| [`
|
|
55
|
-
| [`
|
|
56
|
-
| [`
|
|
57
|
-
| [`
|
|
58
|
-
| [`
|
|
59
|
-
| [`
|
|
60
|
-
| [`
|
|
61
|
-
| [`
|
|
62
|
-
| [`
|
|
63
|
-
| [`
|
|
64
|
-
| [`
|
|
65
|
-
| [`
|
|
66
|
-
| [`
|
|
67
|
-
| [`
|
|
68
|
-
| [`
|
|
69
|
-
| [`
|
|
70
|
-
| [`
|
|
71
|
-
| [`
|
|
69
|
+
The main entry exports 19 factories. `createFileSizeRatchetRule` covers any number of capped areas:
|
|
70
|
+
create one instance per area, each with its own `id`.
|
|
71
|
+
|
|
72
|
+
<!-- begin generated rules -->
|
|
73
|
+
<!-- Generated by `bun run docs:readmes` from each rule's meta. Do not edit by hand. -->
|
|
74
|
+
|
|
75
|
+
| Rule | Description | Factory | Import from | Category | Fails CI by default |
|
|
76
|
+
| --- | --- | --- | --- | --- | --- |
|
|
77
|
+
| [`agents-doc-presence`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/agents-doc-presence/) | AGENTS.md must exist at the repo root, every surface, and every non-opted-out package. | `createAgentsDocPresenceRule` | `@noctcore/lint-meta-rules` | `source-text` | yes |
|
|
78
|
+
| [`canonical-helpers-single-home`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/canonical-helpers-single-home/) | Pure helpers must live in one canonical home (flag the same exported symbol appearing in multiple homes). | `createCanonicalHelpersSingleHomeRule` | `@noctcore/lint-meta-rules` | `source-text` | yes |
|
|
79
|
+
| [`dockerfile-base-image-digest-pin`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/dockerfile-base-image-digest-pin/) | Dockerfile FROM base images must be pinned by `@sha256:` digest (scratch and earlier build stages exempt). | `createDockerfileBaseImageDigestPinRule` | `@noctcore/lint-meta-rules` | `ci` | yes |
|
|
80
|
+
| [`file-size-ratchet`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/file-size-ratchet/) | Source files stay at or under 400 raw lines. Today's offenders are grandfathered by .nightcore/lint-meta/baselines/file-size-ratchet.json; a new/grown offender fails, and a stale/shrunk baseline entry demands tightening. | `createFileSizeRatchetRule` | `@noctcore/lint-meta-rules` | `source-text` | yes |
|
|
81
|
+
| [`github-actions-least-privilege-permissions`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/github-actions-least-privilege-permissions/) | GitHub Actions workflows declare a read-only top-level `permissions:` (no `write-all`/`read-all`, no `<scope>: write`); writes go on the job that needs them. | `createGithubActionsLeastPrivilegePermissionsRule` | `@noctcore/lint-meta-rules` | `ci` | yes |
|
|
82
|
+
| [`github-actions-no-template-injection`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/github-actions-no-template-injection/) | GitHub Actions `run:` and github-script bodies never expand attacker-controllable `${{ }}` context (issue/PR titles, comments, branch names); pass it through `env:` instead. | `createGithubActionsNoTemplateInjectionRule` | `@noctcore/lint-meta-rules` | `ci` | yes |
|
|
83
|
+
| [`github-actions-runner-pinned`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/github-actions-runner-pinned/) | GitHub Actions jobs must run on a pinned runner image (for example ubuntu-24.04), never a *-latest label. | `createGithubActionsRunnerPinnedRule` | `@noctcore/lint-meta-rules` | `ci` | yes |
|
|
84
|
+
| [`github-actions-sha-pinned`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/github-actions-sha-pinned/) | GitHub Actions `uses:` refs must be pinned to a 40-character commit SHA with a `# vN` comment (local ./ actions exempt). | `createGithubActionsShaPinnedRule` | `@noctcore/lint-meta-rules` | `ci` | yes |
|
|
85
|
+
| [`layer-rank`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/layer-rank/) | Fixed dependency direction by rank: a module imports only strictly-lower-ranked <scope> packages (equal/upward forbidden). | `createLayerRankRule` | `@noctcore/lint-meta-rules` | `source-text` | yes |
|
|
86
|
+
| [`no-cloned-component-folders`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/no-cloned-component-folders/) | A component folder name may exist under only ONE feature. Shared surfaces are hoisted; divergent ones get a divergent name. Today’s clones are frozen in a shrinking allowlist. | `createNoClonedComponentFoldersRule` | `@noctcore/lint-meta-rules` | `source-text` | yes |
|
|
87
|
+
| [`no-warn-severity`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/no-warn-severity/) | ESLint severity is 'error' or 'off', never 'warn'. A rule that matters is an error; a failure is fixed, not silenced. | `createNoWarnSeverityRule` | `@noctcore/lint-meta-rules` | `config` | yes |
|
|
88
|
+
| [`package-shape`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/package-shape/) | Every workspace is named <scope>/<dir>; library packages expose a barrel and point main/module/types/exports at the built output. | `createPackageShapeRule` | `@noctcore/lint-meta-rules` | `config` | yes |
|
|
89
|
+
| [`security-scanner-version-parity`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/security-scanner-version-parity/) | The gitleaks version pinned in the workflows must equal the one in scripts/ci/pre-push.sh, and the hook must compare a native gitleaks against it at run time. | `createSecurityScannerVersionParityRule` | `@noctcore/lint-meta-rules` | `ci` | yes |
|
|
90
|
+
| [`service-image-digest-pin`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/service-image-digest-pin/) | Workflow service and container images, and docker-compose images, must be pinned by `@sha256:` digest (a service that builds locally is exempt). | `createServiceImageDigestPinRule` | `@noctcore/lint-meta-rules` | `ci` | yes |
|
|
91
|
+
| [`test-runner-segregation`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/test-runner-segregation/) | Bun-side packages use 'bun:test'; foreign-side packages use 'vitest'. Never mix runners. | `createTestRunnerSegregationRule` | `@noctcore/lint-meta-rules` | `testing` | yes |
|
|
92
|
+
| [`test-sibling-enforcement`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/test-sibling-enforcement/) | Every source file matched by `include` must have a colocated sibling test. Pure helpers must ship a test. | `createTestSiblingEnforcementRule` | `@noctcore/lint-meta-rules` | `source-text` | yes |
|
|
93
|
+
| [`test-workspace-enrollment`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/test-workspace-enrollment/) | Every candidate package with test files must be enumerated in the root 'test:node' script. | `createTestWorkspaceEnrollmentRule` | `@noctcore/lint-meta-rules` | `testing` | yes |
|
|
94
|
+
| [`ui-primitive-shape`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/ui-primitive-shape/) | A folder primitive under the ui root must ship its proof siblings (test, stories); a flat primitive must not carry sibling proof files at the root. | `createUiPrimitiveShapeRule` | `@noctcore/lint-meta-rules` | `source-text` | yes |
|
|
95
|
+
| [`workspace-graph-parity`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/workspace-graph-parity/) | Imported <scope>/* specifiers must be declared workspace:* deps, and tsconfig references must mirror those deps. | `createWorkspaceGraphParityRule` | `@noctcore/lint-meta-rules` | `config` | yes |
|
|
96
|
+
| [`translation-dead-keys`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/translation-dead-keys/) | Every translation catalog key must be reachable from the source: named by a translation call, or spelled by some string in the code. | `createTranslationDeadKeysRule` | `@noctcore/lint-meta-rules/i18n` | `source-text` | yes |
|
|
97
|
+
| [`prisma-method-surface`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/prisma-method-surface/) | The Prisma reads and writes the rules police partition the generated client's <Model>Delegate method surface exactly, so a Prisma upgrade cannot add an unguarded method. | `createPrismaMethodSurfaceRule` | `@noctcore/lint-meta-rules/prisma` | `config` | yes |
|
|
98
|
+
| [`tenant-model-registry-parity`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/tenant-model-registry-parity/) | Every tenant-bearing Prisma model is scoped by the runtime extension or exempt with a reason, and the tenant lint rules resolve with exactly that registry. | `createTenantModelRegistryParityRule` | `@noctcore/lint-meta-rules/prisma` | `config` | yes |
|
|
99
|
+
| [`eslint-config-no-warn`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/eslint-config-no-warn/) | Every rule in the RESOLVED ESLint config is "error" or "off", never "warn", including severities a spread preset injects. | `createEslintConfigNoWarnRule` | `@noctcore/lint-meta-rules/resolved-config` | `config` | yes |
|
|
100
|
+
| [`session-epoch-captured`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/session-epoch-captured/) | Every call into the sign-in seam must pass the session epoch captured before the credential was read, or a revocation landing during the credential check loses the race. | `createSessionEpochCapturedRule` | `@noctcore/lint-meta-rules/session` | `source-text` | yes |
|
|
101
|
+
| [`session-kind-stamped`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/session-kind-stamped/) | Every call that mints a session must stamp the principal kind onto it, or sit in an allowlisted, provably single-kind flow; a session read without the kind falls back to a default and can silently promote one kind of account into another. | `createSessionKindStampedRule` | `@noctcore/lint-meta-rules/session` | `source-text` | yes |
|
|
102
|
+
| [`session-landing-declared`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/session-landing-declared/) | Every file that opens a door into a session must declare where it leaves the caller, and a door whose landing demands a return shape (the one that carries the principal kind to the client) must have it. | `createSessionLandingDeclaredRule` | `@noctcore/lint-meta-rules/session` | `source-text` | yes |
|
|
103
|
+
| [`session-mint-callers`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/session-mint-callers/) | The method that mints a session may only be called from allowlisted files; a new sign-in entry point must route through the gate in front of it so the gate cannot be bypassed. | `createSessionMintCallersRule` | `@noctcore/lint-meta-rules/session` | `source-text` | yes |
|
|
104
|
+
| [`idempotency-key-parity`](https://noctcore.github.io/eslint-plugins/rules/lint-meta-rules/idempotency-key-parity/) | Procedures carrying the idempotency middleware must have a client caller that sends an idempotency key, or no client caller at all. | `createIdempotencyKeyParityRule` | `@noctcore/lint-meta-rules/trpc` | `source-text` | yes |
|
|
105
|
+
<!-- end generated rules -->
|
|
72
106
|
|
|
73
107
|
Every factory is callable with no arguments (all options default), so `createAllRules()` and
|
|
74
108
|
per-factory defaults work out of the box; supply options to retarget a rule at your own repo.
|
|
@@ -81,20 +115,12 @@ The main entry never loads ESLint; this one needs the optional peers `eslint` an
|
|
|
81
115
|
`@typescript-eslint/parser`. Its factories are not part of `RULE_FACTORIES` / `createAllRules()`:
|
|
82
116
|
they are inert until you point them at your catalogs.
|
|
83
117
|
|
|
84
|
-
| Factory | Category | What it enforces |
|
|
85
|
-
| --- | --- | --- |
|
|
86
|
-
| [`createTranslationDeadKeysRule`](./docs/rules/translation-dead-keys.md) | source-text | Every catalog key is reachable: named by a translation call, or spelled by some string in the source. |
|
|
87
|
-
|
|
88
118
|
### `@noctcore/lint-meta-rules/resolved-config`
|
|
89
119
|
|
|
90
120
|
Checks over the RESOLVED ESLint config. They load ESLint and resolve configs through
|
|
91
121
|
`calculateConfigForFile`, which is async, so they implement the harness's `runAsync`
|
|
92
122
|
(`@noctcore/harness` 0.3.0 or newer) and need the optional peer `eslint`.
|
|
93
123
|
|
|
94
|
-
| Factory | Category | What it enforces |
|
|
95
|
-
| --- | --- | --- |
|
|
96
|
-
| [`createEslintConfigNoWarnRule`](./docs/rules/eslint-config-no-warn.md) | config | No rule RESOLVES to `warn`, including a severity a spread preset injects, which the text scan of `no-warn-severity` cannot see. |
|
|
97
|
-
|
|
98
124
|
### `@noctcore/lint-meta-rules/prisma`
|
|
99
125
|
|
|
100
126
|
Whole-repo Prisma guardrails that keep `@noctcore/eslint-plugin-prisma`'s inputs honest. They read
|
|
@@ -102,31 +128,15 @@ the plugin's method sets, schema parser and registry reconciliation, so the lint
|
|
|
102
128
|
checks cannot disagree. The registry parity check resolves an ESLint config (async, `runAsync`) and
|
|
103
129
|
needs the optional peer `eslint`. Neither is in `RULE_FACTORIES`: both need the project's paths.
|
|
104
130
|
|
|
105
|
-
| Factory | Category | What it enforces |
|
|
106
|
-
| --- | --- | --- |
|
|
107
|
-
| [`createTenantModelRegistryParityRule`](./docs/rules/tenant-model-registry-parity.md) | config | Every tenant-bearing schema model is scoped at runtime or exempt with a reason, and the tenant lint rules resolve with exactly that registry. |
|
|
108
|
-
| [`createPrismaMethodSurfaceRule`](./docs/rules/prisma-method-surface.md) | config | The reads and writes the rules police are exactly the generated client's delegate methods, so a Prisma upgrade cannot add an unguarded one. |
|
|
109
|
-
|
|
110
131
|
### `@noctcore/lint-meta-rules/session`
|
|
111
132
|
|
|
112
133
|
Fences around the one seam that turns an authenticated principal into a session. Each rule is inert
|
|
113
134
|
until you name that seam: the method that mints, the gate in front of it, the files allowed to call it,
|
|
114
135
|
the landings a sign-in can end in. None is in `RULE_FACTORIES`. They load nothing beyond the harness
|
|
115
|
-
contract.
|
|
116
|
-
|
|
117
|
-
| Factory | Category | What it enforces |
|
|
118
|
-
| --- | --- | --- |
|
|
119
|
-
| [`createSessionMintCallersRule`](./docs/rules/session-mint-callers.md) | source-text | The method that mints a session is called only from allowlisted files, so a new sign-in entry point cannot skip the gate in front of it. |
|
|
120
|
-
| [`createSessionKindStampedRule`](./docs/rules/session-kind-stamped.md) | source-text | Every mint stamps the principal kind onto the session, or sits in an allowlisted, provably single-kind flow. |
|
|
121
|
-
| [`createSessionEpochCapturedRule`](./docs/rules/session-epoch-captured.md) | source-text | Every call into the sign-in seam passes the revocation epoch captured before the credential was read. |
|
|
122
|
-
| [`createSessionLandingDeclaredRule`](./docs/rules/session-landing-declared.md) | source-text | Every file that opens a door into a session declares its landing, and a door whose landing demands a return shape has it. |
|
|
136
|
+
contract.
|
|
123
137
|
|
|
124
138
|
### `@noctcore/lint-meta-rules/trpc`
|
|
125
139
|
|
|
126
140
|
Cross-tree checks between tRPC routers and the clients that call them. The decorator shape defaults to
|
|
127
141
|
`nestjs-trpc`'s; the rule is inert until you name the middleware and the two trees. Not in
|
|
128
142
|
`RULE_FACTORIES`.
|
|
129
|
-
|
|
130
|
-
| Factory | Category | What it enforces |
|
|
131
|
-
| --- | --- | --- |
|
|
132
|
-
| [`createIdempotencyKeyParityRule`](./docs/rules/idempotency-key-parity.md) | source-text | A procedure guarded by an idempotency middleware has a client caller that sends the key, or no client caller at all. |
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> An agent-contract doc must exist at the repo root, every surface, and every non-opted-out package.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createAgentsDocPresenceRule` 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
|
An agent editing a boundary should read its guardrails first. Requiring an `AGENTS.md` (or whatever
|
|
@@ -17,7 +21,15 @@ Reports a missing doc at:
|
|
|
17
21
|
- every directory derived from `surfaceGlobs` (all surfaces), and
|
|
18
22
|
- every directory derived from `packageGlobs`, except those in `optOut`.
|
|
19
23
|
|
|
20
|
-
##
|
|
24
|
+
## What it does not flag
|
|
25
|
+
|
|
26
|
+
- A package directory listed in `optOut`.
|
|
27
|
+
- The repo root when `requireAtRoot` is `false`.
|
|
28
|
+
- Directories that no `surfaceGlobs` or `packageGlobs` entry matches (the defaults look one level deep
|
|
29
|
+
under `apps/` and `packages/`).
|
|
30
|
+
- The doc's content: an empty or stale file passes, since only its presence is checked.
|
|
31
|
+
|
|
32
|
+
## Options
|
|
21
33
|
|
|
22
34
|
```ts
|
|
23
35
|
createAgentsDocPresenceRule(options?: AgentsDocPresenceOptions): IMetaRule
|
|
@@ -32,9 +44,6 @@ createAgentsDocPresenceRule(options?: AgentsDocPresenceOptions): IMetaRule
|
|
|
32
44
|
| `optOut` | `string[]` | `[]` | Package directories exempt from the requirement. |
|
|
33
45
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
34
46
|
|
|
35
|
-
De-projected from nightcore, which hardcoded `AGENTS.md`, the root/apps/packages layout and a fixed
|
|
36
|
-
leaf opt-out set.
|
|
37
|
-
|
|
38
47
|
## When not to use it
|
|
39
48
|
|
|
40
49
|
If your repo does not adopt an agent-contract doc convention, skip it.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> A helper symbol must not be exported from two different helper homes.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createCanonicalHelpersSingleHomeRule` 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
|
When the same helper name is exported from multiple files, callers import inconsistent copies and the
|
|
@@ -15,7 +19,15 @@ extracts top-level exported identifiers (from `export function|const|let|var …
|
|
|
15
19
|
lists — keyed on the **local** name before any `as`), and flags any name that appears as an export in
|
|
16
20
|
more than one file. Strict, no baseline.
|
|
17
21
|
|
|
18
|
-
##
|
|
22
|
+
## What it does not flag
|
|
23
|
+
|
|
24
|
+
- A name exported from only one helper home, however many files import it.
|
|
25
|
+
- An `export { x as y }` alias under a new public name: the check keys on the local name `x`.
|
|
26
|
+
- Files outside `include`, and any path containing an `excludeContains` fragment (`/lib/` by default).
|
|
27
|
+
- `export default`, `export class`, `export type` and `export interface` declarations: only
|
|
28
|
+
`function`, `const`, `let`, `var` declarations and `export { … }` lists are read.
|
|
29
|
+
|
|
30
|
+
## Options
|
|
19
31
|
|
|
20
32
|
```ts
|
|
21
33
|
createCanonicalHelpersSingleHomeRule(options?: CanonicalHelpersSingleHomeOptions): IMetaRule
|
|
@@ -27,8 +39,6 @@ createCanonicalHelpersSingleHomeRule(options?: CanonicalHelpersSingleHomeOptions
|
|
|
27
39
|
| `excludeContains` | `string[]` | `['/lib/']` | Drop any matched path containing one of these fragments. |
|
|
28
40
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
29
41
|
|
|
30
|
-
De-projected from nightcore, which hardcoded `apps/web/src/**/*.utils.ts` and a `/lib/` exclusion.
|
|
31
|
-
|
|
32
42
|
## When not to use it
|
|
33
43
|
|
|
34
44
|
If your project intentionally re-exports the same symbol from several modules (barrels, façades), scope
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Dockerfile `FROM` base images are pinned by `@sha256:` digest.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createDockerfileBaseImageDigestPinRule` 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
|
`FROM node:22-slim` names whatever the registry serves at build time, so two builds of one commit can
|
|
@@ -25,7 +29,16 @@ FROM deps AS build
|
|
|
25
29
|
FROM scratch
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
##
|
|
32
|
+
## What it does not flag
|
|
33
|
+
|
|
34
|
+
- `FROM scratch` and `FROM <earlier stage>` (a name given by a previous `AS`).
|
|
35
|
+
- `FROM ${BASE}` whose `ARG BASE=<default>` before the first `FROM` is digest-pinned.
|
|
36
|
+
- Dockerfiles under any `skipDirs` segment (`node_modules`, `.git`, `dist`, `.turbo`, `coverage`) and
|
|
37
|
+
files the `dockerfileGlobs` do not match.
|
|
38
|
+
- A `FROM` split with a line continuation (`\`): it is not read. Add a `Containerfile` glob if you use
|
|
39
|
+
Podman naming.
|
|
40
|
+
|
|
41
|
+
## Options
|
|
29
42
|
|
|
30
43
|
```ts
|
|
31
44
|
createDockerfileBaseImageDigestPinRule(options?: DockerfileBaseImageDigestPinOptions): IMetaRule
|
|
@@ -37,7 +50,7 @@ createDockerfileBaseImageDigestPinRule(options?: DockerfileBaseImageDigestPinOpt
|
|
|
37
50
|
| `skipDirs` | `string[]` | `['node_modules', '.git', 'dist', '.turbo', 'coverage']` | A path with any of these segments is skipped. |
|
|
38
51
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
39
52
|
|
|
40
|
-
##
|
|
53
|
+
## When not to use it
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
If your images are built only for local development and you accept whatever a tag serves at build
|
|
56
|
+
time, skip it.
|
|
@@ -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.
|