feat(lint): make anti-slop a package instead of a per-repo copy #381

Merged
gmackie merged 1 commit from feat/oxlint-anti-slop-package into main 2026-08-18 02:32:42 +00:00
Owner

The anti-slop plugin lives as a copied tools/oxlint/anti-slop/ directory in ~35 repos. Fixing one false positive today meant re-copying three files into 27 of them.

And the copies drift. Repos from the worktree wave never received the tsconfig exclude the clone wave added, so classcheck-app carried a latent TS5097 that only surfaced months later when its CI was un-shadowed.

This makes it a package: @forgegraph/oxlint-anti-slop, following the same shape as @forgegraph/health — main/types at src for workspace use, publishConfig redirecting to dist for consumers, files: ["dist"].

Two constraints I had to establish, not assume

A package cannot ship TypeScript sources. Node refuses to strip types under node_modules:

ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING

so oxlint cannot load a .ts plugin from a dependency, however the specifier is written — bare or path. The package must ship compiled JS. Had I designed this by analogy with the existing packages, it would have failed at the first consumer.

The rules import each other with explicit .ts extensions, which oxlint requires when loading from source. tsconfig.build.json sets rewriteRelativeImportExtensions, so emit rewrites them:

import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.js";

Verified as a consumer sees it

Simulated a consumer holding only dist/ plus the published package.json, then linted a probe file:

::error   anti-slop(no-reflect-apply)      Replace `Reflect.apply` with a typed function call.
::warning anti-slop(no-unknown-parameters) Parameter `e` leaves input unparsed.
Found 1 warning and 1 error.

A bare "specifier": "@forgegraph/oxlint-anti-slop" resolves, and both severities work. The monorepo keeps loading from source (./packages/oxlint-anti-slop/src/index.ts), which needs no build before lint — so there is no build-before-lint ordering problem in CI.

What this retires

Consuming repos lose their tools/ directory entirely, and with it the whole class of problems it caused today:

  • app typechecks picking up plugin sources (TS5097)
  • formatter disagreements over vendored files
  • a rule fix costing 27 commits instead of a version bump

Repo-wide typecheck (12/12) and lint (0 errors) green.

Follow-up, not in this PR

Publishing needs the registry-scope decision still open from #340 — whether CI reads npm.forgegraf.com with a token or the scope goes anonymous-readable. Migrating the ~35 consuming repos is a separate wave once the package is published.

The anti-slop plugin lives as a **copied `tools/oxlint/anti-slop/` directory in ~35 repos**. Fixing one false positive today meant re-copying three files into 27 of them. And the copies drift. Repos from the worktree wave never received the `tsconfig` exclude the clone wave added, so classcheck-app carried a latent **TS5097** that only surfaced months later when its CI was un-shadowed. This makes it a package: `@forgegraph/oxlint-anti-slop`, following the same shape as `@forgegraph/health` — `main`/`types` at `src` for workspace use, `publishConfig` redirecting to `dist` for consumers, `files: ["dist"]`. ### Two constraints I had to establish, not assume **A package cannot ship TypeScript sources.** Node refuses to strip types under `node_modules`: ``` ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING ``` so oxlint cannot load a `.ts` plugin from a dependency, however the specifier is written — bare or path. The package must ship compiled JS. Had I designed this by analogy with the existing packages, it would have failed at the first consumer. **The rules import each other with explicit `.ts` extensions**, which oxlint requires when loading from source. `tsconfig.build.json` sets `rewriteRelativeImportExtensions`, so emit rewrites them: ```js import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.js"; ``` ### Verified as a consumer sees it Simulated a consumer holding only `dist/` plus the published `package.json`, then linted a probe file: ``` ::error anti-slop(no-reflect-apply) Replace `Reflect.apply` with a typed function call. ::warning anti-slop(no-unknown-parameters) Parameter `e` leaves input unparsed. Found 1 warning and 1 error. ``` A bare `"specifier": "@forgegraph/oxlint-anti-slop"` resolves, and both severities work. The monorepo keeps loading from source (`./packages/oxlint-anti-slop/src/index.ts`), which needs no build before lint — so there is no build-before-lint ordering problem in CI. ### What this retires Consuming repos lose their `tools/` directory entirely, and with it the whole class of problems it caused today: - app typechecks picking up plugin sources (**TS5097**) - formatter disagreements over vendored files - a rule fix costing **27 commits** instead of a version bump Repo-wide typecheck (12/12) and lint (0 errors) green. ### Follow-up, not in this PR Publishing needs the registry-scope decision still open from #340 — whether CI reads `npm.forgegraf.com` with a token or the scope goes anonymous-readable. Migrating the ~35 consuming repos is a separate wave once the package is published.
feat(lint): make anti-slop a package instead of a per-repo copy
All checks were successful
CI / gitleaks (pull_request) Successful in 7s
forgegraph/ci CI passed
CI / ci (pull_request) Successful in 9m6s
ead1b79a7f
The plugin lives as a copied tools/oxlint/anti-slop/ directory in ~35 repos.
Fixing one false positive today meant re-copying three files into 27 of them,
and the copies drift: repos on the worktree wave never got the tsconfig exclude
the clone wave added, so classcheck-app carried a latent TS5097 that only
surfaced when its CI was un-shadowed months later.

This moves it to packages/oxlint-anti-slop, published as
@forgegraph/oxlint-anti-slop, following the same shape as @forgegraph/health:
main/types point at src for workspace use, publishConfig redirects to dist for
consumers, and files: ["dist"] keeps sources out of the tarball.

Two things had to be established rather than assumed:

  * A package CANNOT ship TypeScript sources. Node refuses to strip types under
    node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so oxlint cannot
    load a .ts plugin from a dependency however the specifier is written. The
    package must ship compiled JS.

  * The rules import each other with explicit .ts extensions, which oxlint needs
    when loading from source. tsconfig.build.json sets
    rewriteRelativeImportExtensions so emit turns them into .js and the
    published bundle resolves at runtime.

Verified against a simulated consumer holding only dist/ and the published
package.json: a bare specifier of "@forgegraph/oxlint-anti-slop" loads the
plugin, and both error and warn severities report. The monorepo keeps loading
from source (./packages/oxlint-anti-slop/src/index.ts), which needs no build
before lint.

Consuming repos lose their tools/ directory entirely, which retires the whole
class of problems it caused: app typechecks picking up plugin sources (TS5097),
formatter disagreements over vendored files, and a rule fix costing 27 commits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gmackie deleted branch feat/oxlint-anti-slop-package 2026-08-18 02:32:42 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gmackie/ForgeGraph!381
No description provided.