npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@joedeleeuw/antidrift

v0.4.0

Published

Deterministic lint + policy guardrails against code and AI-agent drift: a custom ESLint plugin, shareable ESLint config, and a policy generator for agent instruction files and hooks.

Readme

antidrift

A custom ESLint plugin, shareable ESLint config, and a policy generator. It exists to catch the specific ways a codebase rots when an agent is the one writing it.

Regular linters check syntax and a handful of correctness rules. They don't notice when an agent redeclares a type that already ships with firebase, wires up a useEffect with no dependency array, or quietly swallows an error to turn a red test green. Those edits compile. They pass review when the reviewer is skimming. Then they drift. You end up with three slightly different User types, four copies of the same fetch logic, and a component that re-renders on every keystroke.

antidrift writes those patterns down as deterministic rules so the machine catches them instead of you.

The custom rule engine is ESLint plus typescript-eslint. That is intentional: the core rules need TypeScript's Program and TypeChecker, not just a parsed AST. Baseline lint coverage and bespoke semantic rules live in the ESLint layer exposed by createConfig.

The positive pattern behind the rules is one owner per concept: domain owns business vocabulary, contracts own wire schemas, API boundaries validate and authorize, gateways own SDKs, and UI consumes resource/result unions instead of local duplicate shapes.

Install

pnpm add -D @joedeleeuw/antidrift eslint typescript typescript-eslint @typescript-eslint/parser

ESLint, TypeScript, typescript-eslint, and the parser are peer dependencies, so you bring your own versions: ESLint 9+, TypeScript 5+, typescript-eslint 8+.

Stability

Experimental inventory commands, semantic fact payloads, registry metadata, and research rules are distributable evidence surfaces, not backward-compatible APIs. They may change between releases until promoted. Invalid configuration should fail loudly instead of falling back to weaker analysis.

Use the config

Your whole eslint.config.mjs is one call:

import { createConfig } from "@joedeleeuw/antidrift/eslint-config";

export default createConfig({ tsconfigRootDir: import.meta.dirname });

That gives you the type-aware base (typescript-eslint, architecture boundaries, react-hooks) plus every antidrift rule. It also includes the general monorepo hygiene layer: import grouping and spacing, sorted named imports, top-level import type declarations, package dependency checks, promise-misuse and unnecessary-condition checks, type-union/intersection sorting, React component/key conventions, JSX prop ordering, duplicate-import protection, import-cycle detection, and single-blank-line formatting. If you keep a policy/ directory with registries, createConfig reads them and wires up the domain-specific rules on its own.

If you wire @joedeleeuw/antidrift/eslint-plugin by hand instead of using createConfig, configure @typescript-eslint/parser with parser services (projectService or project). Fully type-aware antidrift rules report a configuration error when enabled without those services so missing type information cannot silently weaken the rule set. Hybrid rules such as antidrift/no-sql-string-concat still run their AST and local-flow proof without parser services, but imported escaper, configured safe-member, and configured declaration-source safe-template-member proofs are parser-service-only and are classified by the SQL benchmark.

To collect non-blocking semantic inventory facts, pass a fact sink through createConfig:

import { createConfig } from "@joedeleeuw/antidrift/eslint-config";
import { createMemoryFactSink } from "@joedeleeuw/antidrift/policy";

const sink = createMemoryFactSink();

export default createConfig({
  tsconfigRootDir: import.meta.dirname,
  semanticFacts: { repoRoot: import.meta.dirname, sink },
});

Generate policy artifacts when policy/agent-guardrails.yaml changes:

npx antidrift generate

That writes the agent instruction files and hook configs (more on those below). If you use the generated hooks/instructions, add the scripts they call to your root package:

{
  "scripts": {
    "policy:generate": "antidrift generate",
    "policy:check-generated": "antidrift check-generated",
    "guardrails:shell": "antidrift shell",
    "policy:check:changed": "antidrift check-changed",
    "policy:verify-session": "antidrift verify-session"
  }
}

For self-hosted rule packages, these additional checks keep the control plane honest:

npx antidrift check-registries
npx antidrift check-rule-surface
npx antidrift shell
npx antidrift shell test
npx antidrift semantic-manifest
npx antidrift rule-status
pnpm package:verify
pnpm policy:validate-corpus
pnpm policy:validate-chaski
pnpm policy:benchmark-sql-queries
pnpm policy:inventory-change-contract
pnpm policy:validate-change-contract-evidence
pnpm policy:inventory-diff-scoped-adapters
pnpm policy:inventory-defensive-shape
pnpm policy:inventory-declaration-clone
pnpm policy:inventory-declaration-clone-source-fleet
pnpm policy:inventory-react-state
pnpm policy:inventory-schema-roundtrip
pnpm policy:inventory-underchecked-predicate
npx antidrift repo-corpus --slice current-work --rules import-x/no-cycle

The first two validate registry-backed rule facts and verify every custom rule exported by the plugin is configured and covered by RuleTester. shell runs the packaged ast-grep shell guardrails against the current project. It is opt-in source lint for shell scripts, not an ESLint rule and not an automatic hook installer. antidrift shell test validates the packaged ast-grep rule tests. semantic-manifest prints the composed semantic adapter/fact contract registry as JSON, so downstream tools can discover proof buckets, owned associations, and emitted fact kinds without importing source internals. Use --adapter, --rule, --proof-bucket, --fact-adapter, or --fact-kind to print a filtered adapter slice. rule-status prints a normalized view of policy/registries/rules.yaml, including active, retired, research, and policy-review rows, so experimental rules can ship with explicit maturity and delegation metadata. Use --kind, --status, --semantic-adapter, or --proof-bucket to print a filtered manifest. Add --semantic-summary to print joined summaries for the filtered rows. Proof-bucket filtering includes both semantic-adapter contracts and registry promotion.proofBucket rows. The policy subpath exposes the same helpers plus joined rule semantic summaries for downstream tooling. oxlint is an optional direct CLI for the packaged local complexity budget. It uses the bundled config from this package, disables nested project oxlint config, ignores generated/test/declaration paths, and enforces only complexity, max-depth, and max-params when a project explicitly wires it. package:verify packs the npm tarball, installs it in a throwaway consumer workspace, type-checks every public export under Bundler and NodeNext resolution, imports every runtime export, runs ESLint through the shipped config, proves the packaged oxlint policy exports are available, proves SEMANTIC_FACT_KINDS and the public semantic adapters are available to consumer tooling, proves the CLI exposes the composed semantic manifest and normalized rule-status registry, and proves a configured semantic fact sink receives a generated-source structuralMatch fact. check-rule-surface is only meaningful in this source repository layout; installed consumers can use verify-session, check-generated, and normal ESLint runs without carrying antidrift's own rule tests. policy:validate-corpus lints the maintained project inventory with every custom rule, while repo-corpus can narrow the evidence to the rules changed in a slice. policy:validate-chaski is an optional local corpus gate: it runs explicit assertions against real Chaski frontend/BFF files when CHASKI_REPO or /Users/sushi/code/chaski is available, and skips otherwise so consumers do not need the private corpus. policy:benchmark-sql-queries runs antidrift/no-sql-string-concat on real SQL programs and emits parserServiceDeltas: extra-only non-type-aware identifier reports are inventory, while missing non-type-aware findings or parser errors block promotion. policy:inventory-change-contract runs the inventory-only change-contract spine. Missing contracts exit 0, invalid contracts fail loudly, and present contracts compare merge-base change surfaces against declared paths, dependencies, exports, and optional module graph radius (--tsconfig is required when graph entrypoints are declared). policy:validate-change-contract-evidence replays the documented change-contract MVP gold true-positive and true-negative commits against local sudocode-main and chaski clones. It fails loudly when a required repo or SHA is unavailable, writes reports/change-contract-evidence.json, and is a source-repo evidence gate rather than a consumer requirement. policy:inventory-diff-scoped-adapters runs existing ESLint adapters against changed JS/TS files and filters diagnostics plus semantic facts to changed patch hunks. It is an inventory proof filter over the diff, not a blocking gate. policy:inventory-defensive-shape is a non-blocking sunset inventory for no-defensive-shape-probing. It compares the default-off custom rule with adjacent TypeScript ESLint unsafe rules under parser services and records syntax pressure separately from diagnostics. policy:inventory-declaration-clone is a non-blocking research inventory for duplicate object contract declarations. It uses the TypeScript checker to group interface declarations and literal object type aliases by exact declared-member name/type/optional/readonly fingerprints, and separates generated-only, mixed generated/source, and source-only clone groups. policy:inventory-declaration-clone-source-fleet runs the same inventory against configured local real-code corpora for promotion evidence mining. policy:inventory-react-state is a non-blocking semantic fact inventory for React state co-mutation. It classifies broad setter co-mutation separately from no-handrolled-resource-lifecycle-cells diagnostics so broad inventory cannot become accidental enforcement. Pass --repo and --targets "src/**/*.{ts,tsx}" to scan a specific checkout; target splitting preserves brace globs. policy:inventory-schema-roundtrip is a non-blocking research inventory for same-schema .parse({ ...typedState }) shapes; it classifies real anchors instead of failing the build. policy:inventory-underchecked-predicate is a non-blocking search inventory for no-underchecked-type-predicate. It counts type-predicate syntax pressure separately from broad-input contract-laundering diagnostics and records adjacent TypeScript ESLint unsafe-rule overlap.

Publishing

The normal release path is .github/workflows/publish-antidrift.yml using npm Trusted Publishing. Configure the package on npmjs.com after the package exists:

  • Publisher: GitHub Actions
  • Organization or user: joedeleeuw
  • Repository: antidrift
  • Workflow filename: publish-antidrift.yml (filename only, not .github/workflows/publish-antidrift.yml)
  • Environment name: npm-publish
  • Allowed action: npm publish

Release flow:

  1. Bump tooling/antidrift/package.json to the version being released.
  2. Publish a GitHub release tagged antidrift-v<version>.
  3. The workflow verifies the tag matches the package version, runs the package checks, packs the tarball, and publishes from tooling/antidrift.

The workflow uses OIDC trusted publishing rather than a long-lived NPM_TOKEN. npm generates provenance automatically for trusted publishes from a public GitHub-hosted workflow.

For the initial publication, if npm view @joedeleeuw/antidrift still returns 404 and npmjs.com does not expose package settings yet, do one interactive owner publish from tooling/antidrift after pnpm package:verify, then immediately configure the Trusted Publisher above and restrict token-based publishing in npm package settings.

What's in the box

Public entry points, one package:

  • @joedeleeuw/antidrift — package primitives: createConfig, eslintPlugin, policy rendering, and registry loading
  • @joedeleeuw/antidrift/brandBrand<T, Name>, Unbrand<T>, and brand(name, check)
  • @joedeleeuw/antidrift/eslint-config — the createConfig factory above
  • @joedeleeuw/antidrift/eslint-plugin — the raw plugin, if you'd rather wire rules by hand
  • @joedeleeuw/antidrift/policy — policy check APIs, rule-status registry helpers, semantic fact sinks, and shipped SEMANTIC_FACT_KINDS contracts for advanced tooling
  • @joedeleeuw/antidrift/semantic-adapters — aggregate semantic adapter registry and contracts for tooling that wants the full shared proof surface
  • @joedeleeuw/antidrift/semantic-adapters/async-control-flow — async array callback and Promise collection-flow helpers shared by no-async-array-method
  • @joedeleeuw/antidrift/semantic-adapters/auth-boundary — route-param/authz frame tracking shared by require-authz-check
  • @joedeleeuw/antidrift/semantic-adapters/broad-input — broad Object.entries mini-parser classifiers shared by no-defensive-shape-probing
  • @joedeleeuw/antidrift/semantic-adapters/parse-input — JSON.parse input provenance and local string-boundary proof shared by no-unsafe-deserialize
  • @joedeleeuw/antidrift/semantic-adapters/react-state — React state graph adapter primitives for tooling that needs the same lifecycle proof used by no-handrolled-resource-lifecycle-cells
  • @joedeleeuw/antidrift/semantic-adapters/schema-provenance — Zod parse/provenance helpers shared by no-redundant-zod-parse
  • @joedeleeuw/antidrift/semantic-adapters/sql — SQL context, identifier-token, safe-member, and import/declaration-source safe-template-tag classifiers shared by no-sql-string-concat
  • @joedeleeuw/antidrift/semantic-adapters/tuple-shape — tuple nullish-slot classifiers shared by no-nullable-positional-tuple
  • @joedeleeuw/antidrift/semantic-adapters/type-owner — TypeChecker-backed owner candidate collectors for generated, domain, and installed-package structural authority
  • antidrift — the CLI binary for generate/check/report commands, opt-in shell guardrails, plus semantic-manifest and rule-status for machine-readable metadata

The rule worth installing this for

antidrift/no-structural-type-fork asks the TypeScript type checker, not a list of names, whether the type you just hand-wrote is structurally a copy of a configured generated-source owner. Installed package matches are semantic inventory until a project accepts the package owner as authority.

// A configured generated owner already exports this exact shape.
// antidrift flags it: import or derive from that owner instead.
type ReleaseRow = {
  id: string;
  appId: string;
  version: string;
  status: "draft" | "submitted" | "released";
  createdAt: number;
};

Generated-source and first-party domain owners come from policy registries. Installed packages are scanned only for proposal facts when a semantic fact sink is configured; they do not block by default. Alias an imported type (type X = UserInfo) and it stays quiet, because that's a reference and not a fork. All-optional projection DTOs stay quiet because they are usually boundary drafts or patches, not full model redeclarations.

The scoped rules that motivated this package go after the usual agent tells:

  • require-effect-deps — a useEffect with no dependency array runs on every render, and exhaustive-deps won't say a word about it
  • react-max-component-props — JSX-returning React components with too many locally-owned accepted props
  • no-contract-appeasement-projection — internal helpers that project one owned value contract into another explicit return contract without construction or validation
  • no-nullable-positional-tuple — tuple types with multiple nullable or optional slots where a named object or state union should carry meaning
  • no-appeasement-castany / unknown casts that paper over missing validation
  • no-underchecked-type-predicate — default-off inventory for broad-input type predicates that assert object contracts without checking required asserted fields
  • no-canonical-model-fork — configured first-party model redeclarations that should import or derive from the canonical owner
  • no-handrolled-resource-lifecycle-cells — behavior-based detection for hand-rolled async resource lifecycle state machines, with broad multi-setter co-mutation emitted as inventory only
  • no-unsafe-deserializeJSON.parse of any / unknown instead of parsing at a schema boundary
  • no-defensive-shape-probing — deterministic broad-value extractor cases backed by real corpus evidence, not ordinary boolean predicates
  • import-x/no-cycle — import cycles caught through maintained import-graph coverage

Other existing baseline rules may still ship in the config, but they are not the current roadmap.

Run eslint and read the messages. Each rule says what to do instead.

Shell guardrails

antidrift shell runs Antidrift's packaged ast-grep rule pack against the current project. Install ast-grep with your OS/toolchain package manager and keep it on PATH, or pass AST_GREP_BIN / --ast-grep-bin when your environment pins binaries elsewhere:

npx antidrift shell

The first packaged shell rule is no-swallowed-command-substitution-status. It flags captured command output whose producing command status is erased:

status="$(curl -sS "$url")" || true
status="$(curl -sS "$url" || true)"

Preserve the exit status explicitly instead:

if status="$(curl -sS "$url")"; then
  rc=0
else
  rc=$?
fi

Projects can wire this into their own hook runner or CI:

{
  "scripts": {
    "guardrails:shell": "antidrift shell"
  },
  "simple-git-hooks": {
    "pre-commit": "pnpm guardrails:shell"
  }
}

The package does not auto-install hooks. For pre-commit users, call the same command from a local hook entry.

Brand values

Use the brand kit when a value must be validated before it can enter the domain:

import { brand, type Brand } from "@joedeleeuw/antidrift/brand";

const UserId = brand(
  "UserId",
  (value): value is string =>
    typeof value === "string" && value.startsWith("user_"),
);

type UserId = Brand<string, "UserId">;

const id = UserId.make(raw);

Consumer code should obtain branded values from make, safe, is, or a schema boundary. The package keeps the brand utility, but the former custom brand-cast lint rule is retired until real consumer adoption and non-test forgery evidence justify reopening it.

The part that isn't lint

antidrift generate reads one file, policy/agent-guardrails.yaml, and writes the instruction files your coding agents actually read:

  • CLAUDE.md, AGENTS.md, .github/copilot-instructions.md
  • .cursor/rules/*.mdc
  • .claude/settings.json and .codex/hooks.json, including pre/post-tool hooks that block edits to generated files and run repo verification

One source of truth, regenerated on demand. antidrift check-generated fails CI if any of them have drifted from the policy.

Requirements

Node 22+, ESLint 9+ (flat config), TypeScript 5+, typescript-eslint 8+, and @typescript-eslint/parser 8+.

Status

This is 0.2.0, and I'll be honest about what that means. The rules have local regression tests and a real-corpus validation ledger, but some package-surface rules remain under-proven until they have source-code evidence outside reduced examples. Pin the version.

MIT.