@noctcore/lint-meta-rules
v0.6.2
Published
Portable, parameterized lint-meta rules — whole-repo / cross-file invariants ESLint cannot reach — for the @noctcore/harness lint-meta runner.
Maintainers
Readme
@noctcore/lint-meta-rules
Whole-repo checks that ESLint's one-file-at-a-time model cannot make: every workspace is named by
convention, every imported workspace package is a declared dependency, every GitHub Action is pinned
to a commit SHA, no ESLint rule resolves to warn, no source file grows past a line cap, and more.
This is not an ESLint plugin; the rules run under the
@noctcore/harness lint-meta runner.
Docs: noctcore.github.io/eslint-plugins/packages/lint-meta-rules
Not a good fit if you are not running the harness.
What it checks
- config: workspace package names and build fields, the workspace dependency graph against imports and tsconfig references, ESLint severities.
- source-text: layering between packages, a file-size ratchet, agent-doc presence, colocated tests, one home per helper, no cloned component folders, UI primitive shape.
- testing: every tested package is enrolled in the aggregate test script, and test runners are not mixed within a package.
- ci: GitHub Actions pinned by SHA and to named runners, no template injection, least-privilege permissions, container images pinned by digest, and the secret scanner pinned to one version.
Every check is listed in Rules, with a page per rule.
Install
bun add -D @noctcore/harness @noctcore/lint-meta-rules # or npm i -D / pnpm add -DThe /i18n, /resolved-config and /prisma entry points also need the optional peer eslint, and
/i18n needs @typescript-eslint/parser (see Rules).
Run with the harness
Each rule implements the IMetaRule contract
from @noctcore/harness: a pure function of an IMetaCtx returning IViolation[]. The harness
lint-meta subcommand runs them.
You do not require() this package from your repo. The harness executes only one local file,
.nightcore/lint-meta/registry.js, and never resolves arbitrary imports, because that file runs
inside CI. Its export pipeline reads a rule's source from this package, inlines it and writes
plain JavaScript into your repo's .nightcore/lint-meta/. The package is on npm for versioning and
discoverability.
Every rule is a factory
IMetaRule.run(ctx) takes no config, so each rule is exported as a factory,
createXRule(options): IMetaRule. Anything project-specific (a workspace scope, a source root, a
rank table) is a typed option with a default. Some defaults, such as the @nightcore scope and the
400-line cap, are only starting points: set your own. A programmatic caller (or the export pipeline)
constructs each rule with your options:
import { createPackageShapeRule, createFileSizeRatchetRule } from '@noctcore/lint-meta-rules';
const rules = [
createPackageShapeRule({ scope: '@acme' }),
createFileSizeRatchetRule({ id: 'web-file-size-ratchet', roots: ['apps/web/src'], cap: 400 }),
];RULE_FACTORIES (an id → factory map), RULE_IDS, and createAllRules() are exported for iteration
over the whole catalog.
Rules
The main entry exports 19 factories. createFileSizeRatchetRule covers any number of capped areas:
create one instance per area, each with its own id.
| Rule | Description | Factory | Import from | Category | Fails CI by default |
| --- | --- | --- | --- | --- | --- |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| test-runner-segregation | Bun-side packages use 'bun:test'; foreign-side packages use 'vitest'. Never mix runners. | createTestRunnerSegregationRule | @noctcore/lint-meta-rules | testing | yes |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
Every factory is callable with no arguments (all options default), so createAllRules() and
per-factory defaults work out of the box; supply options to retarget a rule at your own repo.
@noctcore/lint-meta-rules/i18n
Whole-program translation checks live on a separate entry point, because they run ESLint's parser
and scope analysis (reusing the i18n visitor behind noctcore-contracts/translation-key-exists).
The main entry never loads ESLint; this one needs the optional peers eslint and
@typescript-eslint/parser. Its factories are not part of RULE_FACTORIES / createAllRules():
they are inert until you point them at your catalogs.
@noctcore/lint-meta-rules/resolved-config
Checks over the RESOLVED ESLint config. They load ESLint and resolve configs through
calculateConfigForFile, which is async, so they implement the harness's runAsync
(@noctcore/harness 0.3.0 or newer) and need the optional peer eslint.
@noctcore/lint-meta-rules/prisma
Whole-repo Prisma guardrails that keep @noctcore/eslint-plugin-prisma's inputs honest. They read
the plugin's method sets, schema parser and registry reconciliation, so the lint rules and these
checks cannot disagree. The registry parity check resolves an ESLint config (async, runAsync) and
needs the optional peer eslint. Neither is in RULE_FACTORIES: both need the project's paths.
@noctcore/lint-meta-rules/session
Fences around the one seam that turns an authenticated principal into a session. Each rule is inert
until you name that seam: the method that mints, the gate in front of it, the files allowed to call it,
the landings a sign-in can end in. None is in RULE_FACTORIES. They load nothing beyond the harness
contract.
@noctcore/lint-meta-rules/trpc
Cross-tree checks between tRPC routers and the clients that call them. The decorator shape defaults to
nestjs-trpc's; the rule is inert until you name the middleware and the two trees. Not in
RULE_FACTORIES.
