@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
package/README.md CHANGED
@@ -1,32 +1,56 @@
1
1
  # @noctcore/lint-meta-rules
2
2
 
3
- Portable, parameterized **lint-meta** rules — whole-repo / cross-file invariants that ESLint's
4
- per-file AST model cannot reach (every `package.json` name matches a convention, every imported
5
- workspace package is a declared dependency, file-size ratchets, agent-doc presence, and more).
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
- Each rule implements the portable [`IMetaRule`](https://www.npmjs.com/package/@noctcore/harness)
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
- ## How this package is consumed
11
+ Not a good fit if you are not running the harness.
11
12
 
12
- This is a **versioned source catalog**, not a runtime dependency you `require()` from a consumer's
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
- Publishing to npm is for versioning and discoverability; it is not a "`npm install` this and
21
- `require()` it" pitch.
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
- ## Why factories
24
+ Every check is listed in [Rules](#rules), with a page per rule.
24
25
 
25
- `IMetaRule.run(ctx)` takes no config, so every rule that hardcoded a nightcore-specific anchor (a
26
- `@nightcore` scope, an `apps/web/src` root, a rank table) is exported as a **factory** —
27
- `createXRule(options): IMetaRule` — with those anchors lifted to typed options carrying sensible
28
- defaults. A programmatic caller (or the export pipeline) constructs each rule with the consumer's own
29
- options:
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
- 13 nightcore lint-meta rules are ported as 12 factories — nightcore's `web-file-size-ratchet` and
46
- `engine-file-size-ratchet` were byte-identical logic and collapse into a single
47
- `createFileSizeRatchetRule` (instantiated once per capped area). Five CI-hygiene rules (category
48
- `ci`) are ported from a production NestJS + Vite monorepo, one factory each, and two GitHub Actions
49
- security rules (also `ci`) are written for this catalog, for 19 factories in all.
50
-
51
- | Factory | Source rule(s) | Category | What it enforces |
52
- | --- | --- | --- | --- |
53
- | [`createNoWarnSeverityRule`](./docs/rules/no-warn-severity.md) | `no-warn-severity` | config | ESLint severity is `error`/`off`, never `warn`. |
54
- | [`createPackageShapeRule`](./docs/rules/package-shape.md) | `package-shape` | config | Every workspace is named `<scope>/<dir>`; libraries expose a barrel and point build fields at `dist/`. |
55
- | [`createWorkspaceGraphParityRule`](./docs/rules/workspace-graph-parity.md) | `workspace-graph-parity` | config | Imported `<scope>/*` specifiers are declared `workspace:*` deps and mirrored in tsconfig references. |
56
- | [`createLayerRankRule`](./docs/rules/layer-rank.md) | `layer-rank` | source-text | A module imports only strictly-lower-ranked `<scope>` packages (no sideways/upward edges). |
57
- | [`createFileSizeRatchetRule`](./docs/rules/file-size-ratchet.md) | `web-file-size-ratchet`, `engine-file-size-ratchet` | source-text | Source files stay under a line cap, with a one-way self-tightening baseline ratchet. |
58
- | [`createAgentsDocPresenceRule`](./docs/rules/agents-doc-presence.md) | `agents-doc-presence` | source-text | An agent-contract doc exists at the root, every surface, and every non-opted-out package. |
59
- | [`createTestSiblingEnforcementRule`](./docs/rules/test-sibling-enforcement.md) | `test-sibling-enforcement` | source-text | Every source file matched by `include` has a colocated sibling test. |
60
- | [`createCanonicalHelpersSingleHomeRule`](./docs/rules/canonical-helpers-single-home.md) | `canonical-helpers-single-home` | source-text | A helper symbol is not exported from two different helper homes. |
61
- | [`createNoClonedComponentFoldersRule`](./docs/rules/no-cloned-component-folders.md) | `no-cloned-component-folders` | source-text | A component folder name exists under only one feature (shrinking allowlist). |
62
- | [`createUiPrimitiveShapeRule`](./docs/rules/ui-primitive-shape.md) | `ui-primitive-shape` | source-text | A folder primitive ships its proof siblings; a flat primitive carries none at the ui root. |
63
- | [`createTestWorkspaceEnrollmentRule`](./docs/rules/test-workspace-enrollment.md) | `test-workspace-enrollment` | testing | Every tested package is enumerated in the aggregate test script. |
64
- | [`createTestRunnerSegregationRule`](./docs/rules/test-runner-segregation.md) | `test-runner-segregation` | testing | Bun-side and foreign-side test runners are never mixed within a package. |
65
- | [`createGithubActionsShaPinnedRule`](./docs/rules/github-actions-sha-pinned.md) | `github-actions-sha-pinned` | ci | Workflow `uses:` refs are pinned to a full commit SHA with a `# vN` comment. |
66
- | [`createGithubActionsRunnerPinnedRule`](./docs/rules/github-actions-runner-pinned.md) | `github-actions-runner-pinned` | ci | Workflow jobs run on a named runner image, never a `*-latest` label. |
67
- | [`createGithubActionsNoTemplateInjectionRule`](./docs/rules/github-actions-no-template-injection.md) | `github-actions-no-template-injection` | ci | `run:` and github-script bodies never expand attacker-controllable `${{ }}` context (issue/PR titles, comments, branch names). |
68
- | [`createGithubActionsLeastPrivilegePermissionsRule`](./docs/rules/github-actions-least-privilege-permissions.md) | `github-actions-least-privilege-permissions` | ci | A workflow's top-level `permissions:` exists, is not `write-all`/`read-all`, and grants no write. |
69
- | [`createServiceImageDigestPinRule`](./docs/rules/service-image-digest-pin.md) | `service-image-digest-pin` | ci | Workflow service/container images and compose images are pinned by `@sha256:` digest. |
70
- | [`createDockerfileBaseImageDigestPinRule`](./docs/rules/dockerfile-base-image-digest-pin.md) | `dockerfile-base-image-digest-pin` | ci | Dockerfile `FROM` base images are pinned by `@sha256:` digest. |
71
- | [`createSecurityScannerVersionParityRule`](./docs/rules/security-scanner-version-parity.md) | `security-scanner-version-parity` | ci | CI and the local pre-push hook pin the same secret-scanner version, and the hook checks it at run time. |
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 &lt;scope&gt; 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 &lt;scope&gt;/&lt;dir&gt;; 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 &lt;scope&gt;/* 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 &lt;Model&gt;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. Ported from a production NestJS app, where each one exists because its bug shipped once.
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. |
@@ -6,6 +6,11 @@ function baseName(pathLike) {
6
6
  const parts = pathLike.split("/").filter(Boolean);
7
7
  return parts[parts.length - 1] ?? pathLike;
8
8
  }
9
+ function stripTrailingSlashes(path) {
10
+ let end = path.length;
11
+ while (end > 0 && path[end - 1] === "/") end -= 1;
12
+ return path.slice(0, end);
13
+ }
9
14
  function escapeRegExp(value) {
10
15
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
16
  }
@@ -58,6 +63,7 @@ function readSourceText(read, rel) {
58
63
  export {
59
64
  dirOf,
60
65
  baseName,
66
+ stripTrailingSlashes,
61
67
  escapeRegExp,
62
68
  countLines,
63
69
  recursiveGlobs,
package/dist/i18n.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_SKIP_DIRS,
3
3
  escapeRegExp,
4
4
  globFiles
5
- } from "./chunk-OYFQKSJN.js";
5
+ } from "./chunk-KZ3NYHLH.js";
6
6
 
7
7
  // src/i18n/translation-dead-keys.ts
8
8
  import path from "path";
package/dist/index.cjs CHANGED
@@ -1312,7 +1312,7 @@ function createUiPrimitiveShapeRule(options = {}) {
1312
1312
  run(ctx) {
1313
1313
  const violations = [];
1314
1314
  for (const barrel of ctx.glob(`${uiRoot}/*/${barrelFile}`)) {
1315
- const dir = barrel.replace(new RegExp(`/${barrelFile}$`), "");
1315
+ const dir = dirOf(barrel, barrelFile);
1316
1316
  const name = baseName(dir);
1317
1317
  for (const role of roles) {
1318
1318
  const rel = `${dir}/${name}.${role}${extension}`;
@@ -1326,7 +1326,7 @@ function createUiPrimitiveShapeRule(options = {}) {
1326
1326
  }
1327
1327
  }
1328
1328
  for (const flat of ctx.glob(`${uiRoot}/[A-Z]*${extension}`)) {
1329
- const name = baseName(flat).replace(new RegExp(`${extension.replace(/\./g, "\\.")}$`), "");
1329
+ const name = baseName(flat).replace(new RegExp(`${escapeRegExp(extension)}$`), "");
1330
1330
  for (const role of roles) {
1331
1331
  const sibling = `${uiRoot}/${name}.${role}${extension}`;
1332
1332
  if (ctx.exists(sibling)) {
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  recursiveGlobs,
11
11
  stripYamlComment,
12
12
  unquote
13
- } from "./chunk-OYFQKSJN.js";
13
+ } from "./chunk-KZ3NYHLH.js";
14
14
 
15
15
  // src/rules/agents-doc-presence.ts
16
16
  function createAgentsDocPresenceRule(options = {}) {
@@ -1228,7 +1228,7 @@ function createUiPrimitiveShapeRule(options = {}) {
1228
1228
  run(ctx) {
1229
1229
  const violations = [];
1230
1230
  for (const barrel of ctx.glob(`${uiRoot}/*/${barrelFile}`)) {
1231
- const dir = barrel.replace(new RegExp(`/${barrelFile}$`), "");
1231
+ const dir = dirOf(barrel, barrelFile);
1232
1232
  const name = baseName(dir);
1233
1233
  for (const role of roles) {
1234
1234
  const rel = `${dir}/${name}.${role}${extension}`;
@@ -1242,7 +1242,7 @@ function createUiPrimitiveShapeRule(options = {}) {
1242
1242
  }
1243
1243
  }
1244
1244
  for (const flat of ctx.glob(`${uiRoot}/[A-Z]*${extension}`)) {
1245
- const name = baseName(flat).replace(new RegExp(`${extension.replace(/\./g, "\\.")}$`), "");
1245
+ const name = baseName(flat).replace(new RegExp(`${escapeRegExp(extension)}$`), "");
1246
1246
  for (const role of roles) {
1247
1247
  const sibling = `${uiRoot}/${name}.${role}${extension}`;
1248
1248
  if (ctx.exists(sibling)) {
package/dist/prisma.cjs CHANGED
@@ -41,6 +41,11 @@ module.exports = __toCommonJS(prisma_exports);
41
41
  var import_eslint_plugin_prisma = require("@noctcore/eslint-plugin-prisma");
42
42
 
43
43
  // src/rules/shared.ts
44
+ function stripTrailingSlashes(path2) {
45
+ let end = path2.length;
46
+ while (end > 0 && path2[end - 1] === "/") end -= 1;
47
+ return path2.slice(0, end);
48
+ }
44
49
  function globFiles(glob, globs, skipDirs = []) {
45
50
  const skip = new Set(skipDirs);
46
51
  const found = /* @__PURE__ */ new Set();
@@ -59,7 +64,21 @@ var DEFAULT_CLIENT_GLOBS = [
59
64
  "node_modules/.prisma/client/index.d.ts"
60
65
  ];
61
66
  function blankComments(source) {
62
- return source.replace(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replace(/[^\n]/gu, " ")).replace(/(^|[^:])\/\/.*$/gmu, "$1");
67
+ let out = "";
68
+ let from = 0;
69
+ for (let open = source.indexOf("/*"); open !== -1; open = source.indexOf("/*", from)) {
70
+ const close = source.indexOf("*/", open + 2);
71
+ if (close === -1) break;
72
+ out += source.slice(from, open) + source.slice(open, close + 2).replace(/[^\n]/gu, " ");
73
+ from = close + 2;
74
+ }
75
+ return (out + source.slice(from)).split("\n").map(stripLineComment).join("\n");
76
+ }
77
+ function stripLineComment(line) {
78
+ for (let at = line.indexOf("//"); at !== -1; at = line.indexOf("//", at + 1)) {
79
+ if (at === 0 || line[at - 1] !== ":") return line.slice(0, at) + (line.endsWith("\r") ? "\r" : "");
80
+ }
81
+ return line;
63
82
  }
64
83
  function parseDelegateSurfaces(source, file = "") {
65
84
  const lines = blankComments(source).split("\n");
@@ -271,7 +290,7 @@ function parseObjectLiteralKeys(source, exportName) {
271
290
  return null;
272
291
  }
273
292
  function readSchema(ctx, schemaPath) {
274
- const base = schemaPath.replace(/\/+$/u, "");
293
+ const base = stripTrailingSlashes(schemaPath);
275
294
  const files = ctx.glob(`${base}/**/*.prisma`).sort();
276
295
  const texts = files.length > 0 ? files.map((file) => ctx.read(file) ?? "") : [ctx.read(base)];
277
296
  if (texts.some((text) => text === null)) return null;
package/dist/prisma.js CHANGED
@@ -4,8 +4,9 @@ import {
4
4
  severityOf
5
5
  } from "./chunk-VFCX3QKZ.js";
6
6
  import {
7
- globFiles
8
- } from "./chunk-OYFQKSJN.js";
7
+ globFiles,
8
+ stripTrailingSlashes
9
+ } from "./chunk-KZ3NYHLH.js";
9
10
 
10
11
  // src/prisma/prisma-method-surface.ts
11
12
  import { PRISMA_READ_METHODS, PRISMA_WRITE_METHODS } from "@noctcore/eslint-plugin-prisma";
@@ -15,7 +16,21 @@ var DEFAULT_CLIENT_GLOBS = [
15
16
  "node_modules/.prisma/client/index.d.ts"
16
17
  ];
17
18
  function blankComments(source) {
18
- return source.replace(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replace(/[^\n]/gu, " ")).replace(/(^|[^:])\/\/.*$/gmu, "$1");
19
+ let out = "";
20
+ let from = 0;
21
+ for (let open = source.indexOf("/*"); open !== -1; open = source.indexOf("/*", from)) {
22
+ const close = source.indexOf("*/", open + 2);
23
+ if (close === -1) break;
24
+ out += source.slice(from, open) + source.slice(open, close + 2).replace(/[^\n]/gu, " ");
25
+ from = close + 2;
26
+ }
27
+ return (out + source.slice(from)).split("\n").map(stripLineComment).join("\n");
28
+ }
29
+ function stripLineComment(line) {
30
+ for (let at = line.indexOf("//"); at !== -1; at = line.indexOf("//", at + 1)) {
31
+ if (at === 0 || line[at - 1] !== ":") return line.slice(0, at) + (line.endsWith("\r") ? "\r" : "");
32
+ }
33
+ return line;
19
34
  }
20
35
  function parseDelegateSurfaces(source, file = "") {
21
36
  const lines = blankComments(source).split("\n");
@@ -186,7 +201,7 @@ function parseObjectLiteralKeys(source, exportName) {
186
201
  return null;
187
202
  }
188
203
  function readSchema(ctx, schemaPath) {
189
- const base = schemaPath.replace(/\/+$/u, "");
204
+ const base = stripTrailingSlashes(schemaPath);
190
205
  const files = ctx.glob(`${base}/**/*.prisma`).sort();
191
206
  const texts = files.length > 0 ? files.map((file) => ctx.read(file) ?? "") : [ctx.read(base)];
192
207
  if (texts.some((text) => text === null)) return null;
@@ -34,6 +34,13 @@ __export(resolved_config_exports, {
34
34
  });
35
35
  module.exports = __toCommonJS(resolved_config_exports);
36
36
 
37
+ // src/rules/shared.ts
38
+ function stripTrailingSlashes(path2) {
39
+ let end = path2.length;
40
+ while (end > 0 && path2[end - 1] === "/") end -= 1;
41
+ return path2.slice(0, end);
42
+ }
43
+
37
44
  // src/resolved-config/resolve.ts
38
45
  var import_node_fs = require("fs");
39
46
  var import_node_path = __toESM(require("path"), 1);
@@ -84,7 +91,7 @@ var DEFAULT_PROBES = [
84
91
  function findConfigDirs(ctx, packages, configFiles) {
85
92
  const byDir = /* @__PURE__ */ new Map();
86
93
  for (const pattern of packages) {
87
- const base = pattern.replace(/\/+$/u, "");
94
+ const base = stripTrailingSlashes(pattern);
88
95
  for (const name of configFiles) {
89
96
  const matches = base === "." || base === "" ? ctx.exists(name) ? [name] : [] : ctx.glob(`${base}/${name}`);
90
97
  for (const configFile of matches) {
@@ -2,6 +2,9 @@ import {
2
2
  resolveRules,
3
3
  severityOf
4
4
  } from "./chunk-VFCX3QKZ.js";
5
+ import {
6
+ stripTrailingSlashes
7
+ } from "./chunk-KZ3NYHLH.js";
5
8
 
6
9
  // src/resolved-config/eslint-config-no-warn.ts
7
10
  var DEFAULT_ID = "eslint-config-no-warn";
@@ -16,7 +19,7 @@ var DEFAULT_PROBES = [
16
19
  function findConfigDirs(ctx, packages, configFiles) {
17
20
  const byDir = /* @__PURE__ */ new Map();
18
21
  for (const pattern of packages) {
19
- const base = pattern.replace(/\/+$/u, "");
22
+ const base = stripTrailingSlashes(pattern);
20
23
  for (const name of configFiles) {
21
24
  const matches = base === "." || base === "" ? ctx.exists(name) ? [name] : [] : ctx.glob(`${base}/${name}`);
22
25
  for (const configFile of matches) {
package/dist/session.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  escapeRegExp,
4
4
  globFiles,
5
5
  readSourceText
6
- } from "./chunk-OYFQKSJN.js";
6
+ } from "./chunk-KZ3NYHLH.js";
7
7
 
8
8
  // src/session/scan.ts
9
9
  var DEFAULT_EXCLUDE_SUFFIXES = [
package/dist/trpc.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  escapeRegExp,
4
4
  globFiles,
5
5
  readSourceText
6
- } from "./chunk-OYFQKSJN.js";
6
+ } from "./chunk-KZ3NYHLH.js";
7
7
 
8
8
  // src/trpc/idempotency-key-parity.ts
9
9
  var DEFAULT_ID = "idempotency-key-parity";
@@ -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
- ## Factory
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
- ## Factory
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
- ## Factory
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
- ## Limits
53
+ ## When not to use it
41
54
 
42
- A `FROM` split with a line continuation (`\`) is not read. Add a `Containerfile` glob if you use
43
- Podman naming.
55
+ If your images are built only for local development and you accept whatever a tag serves at build
56
+ time, skip it.