@noctcore/lint-meta-rules 0.6.0 → 0.6.2

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 (40) hide show
  1. package/README.md +83 -73
  2. package/dist/{chunk-OYFQKSJN.js → chunk-KZ3NYHLH.js} +6 -0
  3. package/dist/i18n.js +1 -1
  4. package/dist/index.cjs +2 -2
  5. package/dist/index.js +3 -3
  6. package/dist/prisma.cjs +21 -2
  7. package/dist/prisma.js +19 -4
  8. package/dist/resolved-config.cjs +8 -1
  9. package/dist/resolved-config.js +4 -1
  10. package/dist/session.js +1 -1
  11. package/dist/trpc.js +1 -1
  12. package/docs/rules/agents-doc-presence.md +13 -4
  13. package/docs/rules/canonical-helpers-single-home.md +13 -3
  14. package/docs/rules/dockerfile-base-image-digest-pin.md +17 -4
  15. package/docs/rules/eslint-config-no-warn.md +22 -7
  16. package/docs/rules/file-size-ratchet.md +19 -9
  17. package/docs/rules/github-actions-least-privilege-permissions.md +20 -11
  18. package/docs/rules/github-actions-no-template-injection.md +21 -12
  19. package/docs/rules/github-actions-runner-pinned.md +19 -6
  20. package/docs/rules/github-actions-sha-pinned.md +19 -5
  21. package/docs/rules/idempotency-key-parity.md +13 -5
  22. package/docs/rules/layer-rank.md +17 -6
  23. package/docs/rules/no-cloned-component-folders.md +13 -4
  24. package/docs/rules/no-warn-severity.md +14 -3
  25. package/docs/rules/package-shape.md +13 -6
  26. package/docs/rules/prisma-method-surface.md +19 -4
  27. package/docs/rules/security-scanner-version-parity.md +16 -7
  28. package/docs/rules/service-image-digest-pin.md +21 -7
  29. package/docs/rules/session-epoch-captured.md +15 -9
  30. package/docs/rules/session-kind-stamped.md +15 -7
  31. package/docs/rules/session-landing-declared.md +13 -5
  32. package/docs/rules/session-mint-callers.md +11 -3
  33. package/docs/rules/tenant-model-registry-parity.md +18 -4
  34. package/docs/rules/test-runner-segregation.md +15 -5
  35. package/docs/rules/test-sibling-enforcement.md +12 -5
  36. package/docs/rules/test-workspace-enrollment.md +14 -6
  37. package/docs/rules/translation-dead-keys.md +33 -20
  38. package/docs/rules/ui-primitive-shape.md +13 -4
  39. package/docs/rules/workspace-graph-parity.md +15 -5
  40. package/package.json +4 -4
@@ -3,6 +3,10 @@
3
3
  > CI and the local pre-push hook pin the same secret-scanner version, and the hook checks it at run
4
4
  > time.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createSecurityScannerVersionParityRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  ## Why
7
11
 
8
12
  Different scanner versions ship different rulesets, so a push can pass the local hook and fail CI (or
@@ -20,8 +24,6 @@ With the defaults (gitleaks):
20
24
  - a hook `GITLEAKS_IMAGE="...gitleaks:vX.Y.Z"` tag that differs from CI,
21
25
  - a hook that never runs `gitleaks version`.
22
26
 
23
- Dormant when neither the workflows nor the hook mention the scanner.
24
-
25
27
  ```yaml
26
28
  # .github/workflows/security.yml
27
29
  env:
@@ -39,7 +41,15 @@ GITLEAKS_VERSION="8.29.0"
39
41
  gitleaks git .
40
42
  ```
41
43
 
42
- ## Factory
44
+ ## What it does not flag
45
+
46
+ - A repo where neither the workflows nor the hook mention the scanner: the rule is dormant.
47
+ - Whether the hook's version comparison is correct: it checks only that the hook's text runs
48
+ `<scanner> version`.
49
+ - Versions not written as `x.y.z`, and any hook other than `hookFile`. A repo with several hooks needs
50
+ one rule instance per hook.
51
+
52
+ ## Options
43
53
 
44
54
  ```ts
45
55
  createSecurityScannerVersionParityRule(options?: SecurityScannerVersionParityOptions): IMetaRule
@@ -54,8 +64,7 @@ createSecurityScannerVersionParityRule(options?: SecurityScannerVersionParityOpt
54
64
  | `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
55
65
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
56
66
 
57
- ## Limits
67
+ ## When not to use it
58
68
 
59
- It checks that the hook's text runs `<scanner> version`, not that the comparison is correct, and it
60
- reads only `x.y.z` versions. One hook file is checked; a repo with several hooks needs one rule
61
- instance per hook.
69
+ If you run the secret scanner in only one place (CI or a local hook, not both), there is no second pin
70
+ to keep in step.
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Workflow service/container images and docker-compose images are pinned by `@sha256:` digest.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createServiceImageDigestPinRule` 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 (`postgres:17-alpine`) or `latest` names whatever the registry serves today, so a green build
@@ -11,9 +15,9 @@ can change under an unchanged commit, and a local run cannot promise the bytes C
11
15
  ## What it flags
12
16
 
13
17
  - In workflow files: `image:` under a job's `services:`, and a job's `container:` in scalar or mapping
14
- form. An action step's `with: image:` input is not an image pull and is ignored.
15
- - In compose files: every service's `image:`, except a service that also has `build:` (there `image:`
16
- tags an image built locally, which has no upstream digest).
18
+ form.
19
+ - In compose files: every service's `image:`, including one behind an env default
20
+ (`${REDIS_IMAGE:-redis:7}`), which is reported as written since the text does not show a digest.
17
21
 
18
22
  Each violation carries the 1-indexed line.
19
23
 
@@ -32,7 +36,17 @@ services:
32
36
  image: app-api:local
33
37
  ```
34
38
 
35
- ## Factory
39
+ ## What it does not flag
40
+
41
+ - An image pinned as `tag@sha256:<digest>`.
42
+ - An action step's `with: image:` input: it is not an image pull.
43
+ - A compose service that also has `build:`: there `image:` tags an image built locally, which has no
44
+ upstream digest.
45
+ - An exact ref listed in `allowUnpinned`, and compose files under a `skipDirs` segment.
46
+
47
+ The check is line-based text, not a YAML parse.
48
+
49
+ ## Options
36
50
 
37
51
  ```ts
38
52
  createServiceImageDigestPinRule(options?: ServiceImageDigestPinOptions): IMetaRule
@@ -46,7 +60,7 @@ createServiceImageDigestPinRule(options?: ServiceImageDigestPinOptions): IMetaRu
46
60
  | `allowUnpinned` | `string[]` | `[]` | Exact image refs allowed unpinned, for a ref the registry cannot serve a digest for. Only that exact ref passes; a bumped tag is checked again. |
47
61
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
48
62
 
49
- ## Limits
63
+ ## When not to use it
50
64
 
51
- Line-based text, not a YAML parse. An image behind an env default (`${REDIS_IMAGE:-redis:7}`) is
52
- reported as written, since the text does not show a digest.
65
+ If your workflows and compose files only run throwaway local services where you accept whatever a tag
66
+ serves today, skip it.
@@ -3,6 +3,10 @@
3
3
  > Every call into the sign-in seam passes the session epoch it captured before reading the
4
4
  > credential.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createSessionEpochCapturedRule` from `@noctcore/lint-meta-rules/session` · Category `source-text` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  Import it from the `session` entry point:
7
11
 
8
12
  ```ts
@@ -25,9 +29,11 @@ never mention the epoch.
25
29
  ## What it flags
26
30
 
27
31
  Each `.<call>(...)` call in a file matched by `sourceGlobs` whose argument text (found by balancing
28
- parentheses) does not mention `field` as a whole word.
32
+ parentheses) does not mention `field` as a whole word. The parenthesis scanner reads each call on its
33
+ own, so a call written on one line is checked too, and an unstamped call cannot borrow the epoch of
34
+ the next one.
29
35
 
30
- ## What it leaves alone
36
+ ## What it does not flag
31
37
 
32
38
  - `epochless: true`, or any identifier that merely contains the field: whole words only.
33
39
  - Files in `exempt` (the seam's own home, which defines the method and its default).
@@ -35,11 +41,7 @@ parentheses) does not mention `field` as a whole word.
35
41
 
36
42
  With no `call` the rule is inert.
37
43
 
38
- Ported from Settly, whose rule matched calls with a regex that stopped at the first `\n );` after the
39
- call. That missed a call written on one line, and let an unstamped one-line call borrow the epoch of
40
- the next multi-line call. The parenthesis scanner here reads each call on its own.
41
-
42
- ## Factory
44
+ ## Options
43
45
 
44
46
  ```ts
45
47
  createSessionEpochCapturedRule(options?: SessionEpochCapturedOptions): IMetaRule
@@ -58,7 +60,11 @@ createSessionEpochCapturedRule(options?: SessionEpochCapturedOptions): IMetaRule
58
60
  | `hint` | `string` | none | Appended to every message. |
59
61
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
60
62
 
61
- ## Worked example: Settly
63
+ ### Worked example: an app with a post-credential sign-in seam
64
+
65
+ Every sign-in entry point (password, OAuth, second factor) ends by calling one seam,
66
+ `beginOrEstablish`, which a two-factor challenge service defines. Each entry point reads the epoch with
67
+ `sessionService.readEpoch(userId)` before it checks the credential, and passes it in:
62
68
 
63
69
  ```ts
64
70
  createSessionEpochCapturedRule({
@@ -74,7 +80,7 @@ createSessionEpochCapturedRule({
74
80
  });
75
81
  ```
76
82
 
77
- Two Settly flows mint without a captured epoch and are outside the rule by construction, because they
83
+ Two flows in such an app mint without a captured epoch and are outside the rule by construction, because they
78
84
  never call the seam: signup (the account did not exist when the request began) and password change (it
79
85
  revokes first and then re-issues, so the current counter is the right one).
80
86
 
@@ -3,6 +3,10 @@
3
3
  > Every call that mints a session stamps the principal's kind onto it, or sits in an allowlisted,
4
4
  > provably single-kind flow.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createSessionKindStampedRule` from `@noctcore/lint-meta-rules/session` · Category `source-text` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  Import it from the `session` entry point:
7
11
 
8
12
  ```ts
@@ -17,7 +21,7 @@ mint that wrote it. A session minted without the field reads as whatever the rea
17
21
  fence between the two kinds that is a silent promotion: a portal account whose session lost its stamp
18
22
  is a staff caller inside its own tenant, and the tenant boundary waves it through as legitimate.
19
23
 
20
- This is not hypothetical. In the project this rule was extracted from, two re-issue paths (a password
24
+ This is not hypothetical. In one production codebase, two re-issue paths (a password
21
25
  change and a revoke-other-sessions action, both reachable by a portal account) shipped without the
22
26
  stamp while every gate was green, because nothing mechanical looked.
23
27
 
@@ -32,10 +36,7 @@ parentheses, so a multi-line options object with nested calls is read whole):
32
36
  somewhere. Coarser, but no silent hole.
33
37
  3. Otherwise the file must be in `allowUnstamped`.
34
38
 
35
- Residual gap, stated rather than hidden: a file that stamps the field on one delegated mint and forgets
36
- it on a second delegated mint passes clause 2.
37
-
38
- ## What it leaves alone
39
+ ## What it does not flag
39
40
 
40
41
  - `.<mintCall>Something(`, and the method's definition.
41
42
  - `kindless` or `kinds`: the field must appear as a whole word.
@@ -43,7 +44,10 @@ it on a second delegated mint passes clause 2.
43
44
 
44
45
  With no `mintCall` the rule is inert.
45
46
 
46
- ## Factory
47
+ Residual gap, stated rather than hidden: a file that stamps the field on one delegated mint and forgets
48
+ it on a second delegated mint passes clause 2.
49
+
50
+ ## Options
47
51
 
48
52
  ```ts
49
53
  createSessionKindStampedRule(options?: SessionKindStampedOptions): IMetaRule
@@ -62,7 +66,11 @@ createSessionKindStampedRule(options?: SessionKindStampedOptions): IMetaRule
62
66
  | `hint` | `string` | none | Appended to every message. |
63
67
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
64
68
 
65
- ## Worked example: Settly
69
+ ### Worked example: an app with staff and portal accounts
70
+
71
+ One sign-in serves staff accounts and invite-only portal accounts, and every session mint goes
72
+ through `establishSession`. Self-signup only ever creates staff accounts, so its file is allowed to
73
+ mint without a stamp:
66
74
 
67
75
  ```ts
68
76
  createSessionKindStampedRule({
@@ -3,6 +3,10 @@
3
3
  > Every file that opens a door into a session declares where it leaves the caller, and a door whose
4
4
  > landing demands a return shape has it.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createSessionLandingDeclaredRule` from `@noctcore/lint-meta-rules/session` · Category `source-text` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  Import it from the `session` entry point:
7
11
 
8
12
  ```ts
@@ -15,8 +19,8 @@ With two shells behind one sign-in (staff and a customer portal, say), the clien
15
19
  account to the right one if the response that ends the sign-in says which kind it is. The first door
16
20
  (the password sign-in) usually does. A second door that finishes a sign-in from a different service (a
17
21
  second-factor challenge) can return the same union and still have its landing unwired, and nothing
18
- fails: the account signs in, lands in the wrong shell, and bounces off its guard. In the project this
19
- rule was extracted from, every portal account with 2FA enabled did exactly that.
22
+ fails: the account signs in, lands in the wrong shell, and bounces off its guard. In one production
23
+ codebase, every portal account with 2FA enabled did exactly that.
20
24
 
21
25
  Two sibling rules fence the same seam, [`session-mint-callers`](./session-mint-callers.md) (who may
22
26
  mint) and [`session-kind-stamped`](./session-kind-stamped.md) (the session carries the kind). Neither
@@ -34,7 +38,7 @@ can see this, because both are about the session and this is about the RESPONSE.
34
38
  3. **The declarations.** A door whose `landing` is not a key of `landings`, or whose `because` is empty.
35
39
  4. **Staleness.** A door whose file no longer exists, or that `sourceGlobs` do not reach.
36
40
 
37
- ## What it leaves alone
41
+ ## What it does not flag
38
42
 
39
43
  - A file that only defines a door method (the leading dot is required).
40
44
  - A door whose landing maps to `null`: a re-issue that replaces the cookie of a caller already inside a
@@ -43,7 +47,7 @@ can see this, because both are about the session and this is about the RESPONSE.
43
47
 
44
48
  With no `doorCalls` the rule is inert.
45
49
 
46
- ## Factory
50
+ ## Options
47
51
 
48
52
  ```ts
49
53
  createSessionLandingDeclaredRule(options?: SessionLandingDeclaredOptions): IMetaRule
@@ -61,7 +65,11 @@ createSessionLandingDeclaredRule(options?: SessionLandingDeclaredOptions): IMeta
61
65
  | `hint` | `string` | none | Appended to every message. |
62
66
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
63
67
 
64
- ## Worked example: Settly
68
+ ### Worked example: an app with two sign-in shells
69
+
70
+ Staff and portal accounts share one sign-in and land in different shells. Sessions open through
71
+ `establishSession` or the second-factor gate `beginOrEstablish`. The two doors either kind can reach
72
+ must return `Promise<ILoginResult>`; the rest are staff-only or re-issues:
65
73
 
66
74
  ```ts
67
75
  const AUTH = 'apps/api/src/modules/auth';
@@ -3,6 +3,10 @@
3
3
  > The method that mints a session is callable only from an allowlist of files, so a new sign-in
4
4
  > entry point cannot skip the gate in front of it.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createSessionMintCallersRule` from `@noctcore/lint-meta-rules/session` · Category `source-text` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  Import it from the `session` entry point:
7
11
 
8
12
  ```ts
@@ -23,7 +27,7 @@ session, so this rule fences the door by caller.
23
27
  A file matched by `sourceGlobs` that calls `.<mintCall>(` (whitespace before the paren allowed) and is
24
28
  not in `allowedCallers`.
25
29
 
26
- ## What it leaves alone
30
+ ## What it does not flag
27
31
 
28
32
  - The method's definition (`async establishSession(`): the leading dot is required.
29
33
  - `.<mintCall>Something(`: only whitespace may sit between the name and the paren.
@@ -33,7 +37,7 @@ not in `allowedCallers`.
33
37
 
34
38
  With no `mintCall` the rule is inert.
35
39
 
36
- ## Factory
40
+ ## Options
37
41
 
38
42
  ```ts
39
43
  createSessionMintCallersRule(options?: SessionMintCallersOptions): IMetaRule
@@ -51,7 +55,11 @@ createSessionMintCallersRule(options?: SessionMintCallersOptions): IMetaRule
51
55
  | `hint` | `string` | none | Appended to every message, e.g. a pointer to your auth docs. |
52
56
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
53
57
 
54
- ## Worked example: Settly
58
+ ### Worked example: an app with a gated session mint
59
+
60
+ `establishSession` mints the session and `beginOrEstablish` is the second-factor gate every sign-in
61
+ must pass through. Only the mint's home, the gate, signup and the two re-issue flows may call the mint
62
+ directly:
55
63
 
56
64
  ```ts
57
65
  const AUTH = 'apps/api/src/modules/auth/services';
@@ -3,6 +3,10 @@
3
3
  > Every tenant-bearing Prisma model is scoped by the runtime tenant extension or exempt with a
4
4
  > reason, and the tenant lint rules resolve with exactly that registry.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createTenantModelRegistryParityRule` 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
@@ -45,7 +49,17 @@ cannot be resolved.
45
49
 
46
50
  Async: it implements the harness's `runAsync` (`@noctcore/harness` 0.3.0 or newer).
47
51
 
48
- ## Factory
52
+ ## What it does not flag
53
+
54
+ - A schema model that carries only some of the `tenantFields` columns: a model must carry ALL of them
55
+ to be tenant-bearing.
56
+ - A tenant-bearing model listed in `unscopedByDesign`, once it names its hand-scope columns in
57
+ `handScopedModels` or records why it cannot in `handScopePending`.
58
+ - Hand-scope gaps when `requireHandScope` is `false`.
59
+ - The resolved ESLint config when `eslint` is `false`; the hand-scope option when `handScopedRule` is
60
+ `null` or the registry has no hand-scoped models.
61
+
62
+ ## Options
49
63
 
50
64
  ```ts
51
65
  createTenantModelRegistryParityRule(options?: TenantModelRegistryParityOptions): IMetaRule
@@ -77,10 +91,10 @@ createTenantModelRegistryParityRule(options?: TenantModelRegistryParityOptions):
77
91
 
78
92
  Keys are Prisma delegate accessors (`invoice` for `model Invoice`).
79
93
 
80
- ## Worked example: Settly
94
+ ### Worked example: a multi-tenant monorepo
81
95
 
82
- Settly scopes on `tenantId`, splits its schema per domain, keeps the runtime map in its API's tenant
83
- extension and the exemption maps in a registry module its ESLint config also reads:
96
+ A project that scopes on `tenantId`, splits its schema per domain, keeps the runtime map in its API's
97
+ tenant extension and the exemption maps in a registry module its ESLint config also reads:
84
98
 
85
99
  ```ts
86
100
  import { createTenantModelRegistryParityRule } from '@noctcore/lint-meta-rules/prisma';
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Bun-side and foreign-side test runners are never mixed within a package.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createTestRunnerSegregationRule` from `@noctcore/lint-meta-rules` · Category `testing` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  A package that mixes two test runners (e.g. `bun:test` and Vitest) has tests that run under one
@@ -15,10 +19,18 @@ and the **foreign set** (`vitestDirs`). For every `*.test.ts(x)` file:
15
19
 
16
20
  - **bun-side** — must import the bun runner (`bunRunnerImport`) and must NOT import the foreign runner
17
21
  (`foreignRunnerImport`);
18
- - **foreign-side** — must NOT import the bun runner. (A direct foreign-runner import is not required —
19
- it may arrive transitively via shared test-utils.)
22
+ - **foreign-side** — must NOT import the bun runner.
23
+
24
+ ## What it does not flag
20
25
 
21
- ## Factory
26
+ - A foreign-side test with no direct foreign-runner import: the runner may arrive transitively via
27
+ shared test-utils.
28
+ - A bun-side test that imports `bunRunnerImport` and not the foreign runner.
29
+ - Anything in a `vitestDirs` directory for a missing bun import (those dirs leave the bun set).
30
+ - Files that are not `*.test.ts` / `*.test.tsx` (setup files, helpers), and `require()` or dynamic
31
+ imports: only a `from '<runner>'` import is read.
32
+
33
+ ## Options
22
34
 
23
35
  ```ts
24
36
  createTestRunnerSegregationRule(options?: TestRunnerSegregationOptions): IMetaRule
@@ -33,8 +45,6 @@ createTestRunnerSegregationRule(options?: TestRunnerSegregationOptions): IMetaRu
33
45
  | `foreignRunnerImport` | `string` | `'vitest'` | The foreign runner import specifier. |
34
46
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
35
47
 
36
- De-projected from nightcore, which hardcoded `bun:test` vs `vitest` and the two dir sets.
37
-
38
48
  ## When not to use it
39
49
 
40
50
  If your repo uses a single test runner everywhere, this rule has nothing to segregate.
@@ -2,10 +2,14 @@
2
2
 
3
3
  > Every source file matched by `include` must have a colocated sibling test.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createTestSiblingEnforcementRule` 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
  A pure helper with no test is a silent liability — the kind of code that drifts because nothing pins
8
- its behavior. Requiring a colocated test for a chosen class of files (nightcore's `.utils.ts` sidecars)
12
+ its behavior. Requiring a colocated test for a chosen class of files (`.utils.ts` helper files, say)
9
13
  makes "did you test this?" a mechanical check rather than a review-time hope.
10
14
 
11
15
  ## What it flags
@@ -14,7 +18,13 @@ For each file matched by an `include` glob, the sibling test is the same path wi
14
18
  extension replaced by each configured test extension. If none of those siblings exists, the source
15
19
  file is flagged. Strict — there is no baseline; the pattern is opt-in via which files `include` selects.
16
20
 
17
- ## Factory
21
+ ## What it does not flag
22
+
23
+ - A matched file with a sibling for any one of `testExtensions` (`foo.utils.test.ts` or
24
+ `foo.utils.test.tsx` for `foo.utils.ts`).
25
+ - Files outside `include` (only `apps/web/src/**/*.utils.ts` by default).
26
+
27
+ ## Options
18
28
 
19
29
  ```ts
20
30
  createTestSiblingEnforcementRule(options?: TestSiblingEnforcementOptions): IMetaRule
@@ -26,9 +36,6 @@ createTestSiblingEnforcementRule(options?: TestSiblingEnforcementOptions): IMeta
26
36
  | `testExtensions` | `string[]` | `['.test.ts', '.test.tsx']` | Accepted colocated-test extensions (replace the source extension). |
27
37
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
28
38
 
29
- De-projected from nightcore, which hardcoded `apps/web/src/**/*.utils.ts` and the `.utils.test.ts(x)`
30
- sibling shape.
31
-
32
39
  ## When not to use it
33
40
 
34
41
  If you colocate tests loosely or centralize them in a `__tests__` tree, retune `include`/`testExtensions`
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Every tested package must be enumerated in the aggregate test script.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createTestWorkspaceEnrollmentRule` from `@noctcore/lint-meta-rules` · Category `testing` · Fails CI by default: yes
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  When the CI test command is a hardcoded list of workspace paths, a package added without editing that
@@ -13,10 +17,17 @@ cannot silently escape CI.
13
17
 
14
18
  Reads the root manifest's `scriptName` command. For each candidate directory (from `packageGlobs`
15
19
  plus `extraDirs`, minus `excludeDirs`) that contains at least one file matching `testGlobSuffix`, if
16
- the directory string does not appear in the script command, it is flagged. A missing or invalid
17
- manifest is a no-op.
20
+ the directory string does not appear in the script command, it is flagged.
21
+
22
+ ## What it does not flag
18
23
 
19
- ## Factory
24
+ - A candidate package with no file matching `testGlobSuffix`.
25
+ - Directories in `excludeDirs` (tested by a different runner or script).
26
+ - Anything when the root manifest is missing or is not valid JSON: the rule is a no-op.
27
+ - A directory whose path appears anywhere in the script text: the check is a substring match, not a
28
+ parse of the command.
29
+
30
+ ## Options
20
31
 
21
32
  ```ts
22
33
  createTestWorkspaceEnrollmentRule(options?: TestWorkspaceEnrollmentOptions): IMetaRule
@@ -32,9 +43,6 @@ createTestWorkspaceEnrollmentRule(options?: TestWorkspaceEnrollmentOptions): IMe
32
43
  | `testGlobSuffix` | `string` | `'**/*.test.ts'` | Glob suffix (per dir) that detects the presence of tests. |
33
44
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
34
45
 
35
- De-projected from nightcore, which hardcoded the `test:node` script, a `packages/*` + `apps/sidecar`
36
- dir list, and a vitest exclusion set.
37
-
38
46
  ## When not to use it
39
47
 
40
48
  If your test runner discovers packages automatically (no hardcoded path list), this rule is
@@ -3,6 +3,10 @@
3
3
  > Every translation catalog key is reachable from the source: named by a translation call, or spelled
4
4
  > by some string in the code.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createTranslationDeadKeysRule` from `@noctcore/lint-meta-rules/i18n` · Category `source-text` · Fails CI by default: yes
8
+ <!-- end generated rule header -->
9
+
6
10
  Import it from the `i18n` entry point, which (unlike the main one) loads ESLint:
7
11
 
8
12
  ```ts
@@ -21,7 +25,27 @@ This rule resolves call sites with the same visitor and catalog loader as
21
25
  two checks never disagree on which key a call means. It then adds the data routes a per-file rule
22
26
  cannot see.
23
27
 
24
- ## What counts as reached
28
+ ## What it flags
29
+
30
+ Every key in `catalogs` that no scanned source reaches (the routes are listed under
31
+ [What it does not flag](#what-it-does-not-flag)). What is left was named by no call and spelled by no
32
+ string. On a production app with 1,824 keys this reported 55, and every one of them had zero
33
+ references outside the catalogs.
34
+
35
+ ### Blind spots
36
+
37
+ It is conservative, not complete. It reports a reached key as dead when the key arrives by a route it
38
+ cannot see:
39
+
40
+ - **Keys that never appear in the scanned source**: server-sent codes, a CMS, another app, JSON
41
+ config. List them in `allow`, or add the files that spell them to `sourceGlobs`.
42
+ - **A variable key under a `keyPrefix` binding**: `useTranslation('ns', { keyPrefix: 'form' })` then
43
+ `t(field)` with `field = 'name'` reaches `form.name`, but no string spells `form.name`. (A static key
44
+ under a `keyPrefix` is resolved correctly.)
45
+ - **Keys built by anything other than `+` or a template literal**: `[a, b].join('.')`,
46
+ `` `${a}` `` split across variables, `String.prototype.concat`.
47
+
48
+ ## What it does not flag
25
49
 
26
50
  A key is reached, and never reported, when ANY of these holds:
27
51
 
@@ -36,29 +60,13 @@ A key is reached, and never reported, when ANY of these holds:
36
60
  `` `nav.${id}.label` `` and `'errors.' + code` are patterns, not just call arguments;
37
61
  - it matches an `allow` pattern.
38
62
 
39
- And it reports **no dead key at all** when a scanned file cannot be analysed (a parse error), or when
63
+ It reports **no dead key at all** when a scanned file cannot be analysed (a parse error), or when
40
64
  `sourceGlobs` match nothing: in both cases the rule would otherwise call reached keys dead.
41
65
 
42
- What is left was named by no call and spelled by no string. On a production app with 1,824 keys this
43
- reported 55, and every one of them had zero references outside the catalogs.
44
-
45
- ## Blind spots
46
-
47
- It is conservative, not complete. It reports a reached key as dead when the key arrives by a route it
48
- cannot see:
49
-
50
- - **Keys that never appear in the scanned source**: server-sent codes, a CMS, another app, JSON
51
- config. List them in `allow`, or add the files that spell them to `sourceGlobs`.
52
- - **A variable key under a `keyPrefix` binding**: `useTranslation('ns', { keyPrefix: 'form' })` then
53
- `t(field)` with `field = 'name'` reaches `form.name`, but no string spells `form.name`. (A static key
54
- under a `keyPrefix` is resolved correctly.)
55
- - **Keys built by anything other than `+` or a template literal**: `[a, b].join('.')`,
56
- `` `${a}` `` split across variables, `String.prototype.concat`.
57
-
58
66
  It also misses some dead keys, by design: any string that happens to equal a key (in any namespace)
59
67
  keeps it alive, and a broad pattern such as `` `${x}.title` `` keeps every `*.title` alive.
60
68
 
61
- ## Factory
69
+ ## Options
62
70
 
63
71
  ```ts
64
72
  createTranslationDeadKeysRule(options?: TranslationDeadKeysOptions): IMetaRule
@@ -92,7 +100,12 @@ createTranslationDeadKeysRule({
92
100
  });
93
101
  ```
94
102
 
95
- ## Requirements
103
+ ### Requirements
96
104
 
97
105
  The `i18n` entry needs the optional peers `eslint` (>= 9) and `@typescript-eslint/parser`. Scanned
98
106
  files are parsed as TypeScript with JSX enabled, without type information.
107
+
108
+ ## When not to use it
109
+
110
+ If most keys reach the app from outside the scanned source (server-sent codes, a CMS, another app),
111
+ `allow` would have to list most of the catalog and the rule proves little, so skip it.
@@ -2,6 +2,10 @@
2
2
 
3
3
  > A folder primitive must ship its proof siblings; a flat primitive must carry none at the ui root.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ Runs under `@noctcore/harness`, not ESLint · Factory `createUiPrimitiveShapeRule` 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
  The primitives root is the one place exempt from folder-per-component: flat single files are fine for
@@ -18,7 +22,15 @@ tested + storied.
18
22
  `<Name>.<role><extension>` at the ui root is flagged: those proof files belong inside a `<Name>/`
19
23
  folder.
20
24
 
21
- ## Factory
25
+ ## What it does not flag
26
+
27
+ - A bare flat primitive (`<uiRoot>/Button.tsx`) with no proof sibling at the ui root.
28
+ - A folder primitive that ships every configured role (`Dialog/Dialog.test.tsx`,
29
+ `Dialog/Dialog.stories.tsx`).
30
+ - A folder without the `barrelFile`, and flat files whose name does not start with a capital letter.
31
+ - Anything outside `uiRoot`.
32
+
33
+ ## Options
22
34
 
23
35
  ```ts
24
36
  createUiPrimitiveShapeRule(options?: UiPrimitiveShapeOptions): IMetaRule
@@ -32,9 +44,6 @@ createUiPrimitiveShapeRule(options?: UiPrimitiveShapeOptions): IMetaRule
32
44
  | `extension` | `string` | `'.tsx'` | Extension of primitive and proof files. |
33
45
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
34
46
 
35
- De-projected from nightcore, which hardcoded `apps/web/src/components/ui`, the `test`/`stories` roles,
36
- and the `.tsx` extension.
37
-
38
47
  ## When not to use it
39
48
 
40
49
  If you do not maintain a flat-vs-folder primitive convention or do not colocate stories/tests, skip
@@ -3,6 +3,10 @@
3
3
  > Imported `<scope>/*` specifiers must be declared `workspace:*` deps, and tsconfig `references` must
4
4
  > mirror those deps.
5
5
 
6
+ <!-- begin generated rule header -->
7
+ Runs under `@noctcore/harness`, not ESLint · Factory `createWorkspaceGraphParityRule` 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 cross-package edge is real in three places at once: the import in source, the `workspace:*` entry in
@@ -14,13 +18,21 @@ so an edge can never be half-wired.
14
18
 
15
19
  For each workspace `package.json`:
16
20
 
17
- - **(a) imported ⊆ declared** — every `<scope>/<pkg>` imported under the source dir (test files
18
- excluded, self-imports ignored) must appear as a `"<scope>/<pkg>": "workspace:*"` dependency.
21
+ - **(a) imported ⊆ declared** — every `<scope>/<pkg>` imported under the source dir must appear as a
22
+ `"<scope>/<pkg>": "workspace:*"` dependency.
19
23
  - **(b) references mirror deps** — the tsconfig `references` must reference exactly the declared
20
24
  workspace deps: a declared dep missing from references, or a referenced package that is not a
21
25
  declared dep, is a violation.
22
26
 
23
- ## Factory
27
+ ## What it does not flag
28
+
29
+ - A package importing itself by its own `<scope>/<dir>` name.
30
+ - Imports in test files (`.test.ts`, `.test.tsx`) and files outside `srcDir`.
31
+ - Imports without a `from` clause (`import()`, `require()`, a side-effect `import`): only
32
+ `from '<scope>/<pkg>'` is read.
33
+ - Check (b) for a package with no `tsconfig.json`.
34
+
35
+ ## Options
24
36
 
25
37
  ```ts
26
38
  createWorkspaceGraphParityRule(options?: WorkspaceGraphParityOptions): IMetaRule
@@ -33,8 +45,6 @@ createWorkspaceGraphParityRule(options?: WorkspaceGraphParityOptions): IMetaRule
33
45
  | `srcDir` | `string` | `'src'` | Source directory (per package) scanned for imports. |
34
46
  | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
35
47
 
36
- De-projected from nightcore, which hardcoded the `@nightcore` scope and a `src` source directory.
37
-
38
48
  ## When not to use it
39
49
 
40
50
  If your project does not use the `workspace:*` protocol, or does not use TypeScript project