ESLint plugin

Lint for meaning.

Rules that are plain-English questions about each function — does the name promise what the body does, does the comment still hold, would this error message help anyone — judged by TypeSafe’s Jev and thresholded in your ESLint config.

Try the demo
Cold run
934 ms
Same file again
13 ms
10-case seed bench
$0.0002

Three functions, the run recorded for the demo, cache on disk. Full bench numbers.

Demo

A recording of a real run

Every warning below came out of the plugin against the live model. Pick an example, run it, then fix it and run it again.

 
npx eslint

How it works

Three steps, no AST of your own

Collect every function

ESLint already walks the file, so the plugin takes each function’s name, signature, leading comment and body — one function at a time, no surrounding file.

Ask one question

Every enabled rule’s question is bundled into a single request per function, with written true and false criteria. Jev answers with a probability instead of prose.

Threshold it in code

The rule compares that probability to the threshold in your config and reports at the name, the comment, or the throwing line. Answers are cached on disk by content hash.

Install

Configure locally

Two steps. The three built-in rules run at warn; with no key found anywhere they produce no diagnostics at all and your editor behaves exactly as before.

Terminal
npm i -D @shahriarbijoy/eslint-plugin-jev @typescript-eslint/parser   # only if you lint TypeScript
echo 'TYPESAFE_API_KEY=...' >> .env                    # get a key from the TypeSafe console: https://console.typesafe.ai
# echo 'OPENROUTER_API_KEY=...' >> .env                # alternative: a key from https://openrouter.ai/keys

With both keys set, TypeSafe is used unless settings.jev.provider says otherwise; with neither set, the rules stay off and print one warning naming both TYPESAFE_API_KEY and OPENROUTER_API_KEY.

eslint.config.js
// eslint.config.js
import { defineConfig } from "eslint/config";
import tsParser from "@typescript-eslint/parser";
import jev from "@shahriarbijoy/eslint-plugin-jev";

export default defineConfig([
  { files: ["**/*.ts"], languageOptions: { parser: tsParser } }, // drop this line for plain JavaScript
  ...jev.configs.recommended,
]);

Where the key comes from

Each key is looked up independently, in this order, first hit wins:

  1. in the environment
  2. in a .env file in the directory ESLint runs from
  3. in ~/.config/jev/config.json, under apiKey for TypeSafe or openrouterApiKey for OpenRouter

TypeSafe reads TYPESAFE_API_KEY; OpenRouter reads OPENROUTER_API_KEY. The key is read inside a worker thread and never appears in a diagnostic, a log line, or the cache.

VS Code

The ESLint extension runs a language server that does not inherit your shell’s environment, so a key exported in .zshrc will not reach it. That is why step two writes the key to .env: the plugin reads .env from the workspace root itself, so VS Code works with no extra configuration.

If you keep your key somewhere else, hand it to the server directly with { "eslint.execArgv": ["--env-file=.env"] } in .vscode/settings.json.

Settings

Everything below is optional. Put it under settings.jev in any config object.

eslint.config.js
settings: {
  jev: {
    model: "jev-latest",     // pin a version once thresholds are tuned
    timeoutMs: 8000,         // per file, across all its functions; on timeout the file is skipped
    maxFunctionTokens: 6000, // estimated at 4 chars/token; larger functions are skipped with a diagnostic
    concurrency: 6,          // parallel Jev requests per file
    cacheDir: "node_modules/.cache/eslint-plugin-jev",
    strict: false,           // true: a missing key or transport failure is reported as a lint problem at line 1, at the rule's configured severity
    ignoreNames: ["^use[A-Z]", "^on[A-Z]", "^handle[A-Z]", "^toJSON$"],  // regexes, for name-matches-body
    provider: "auto",        // "auto" (default) | "typesafe" | "openrouter" — which backend answers questions
  }
}

"auto" uses TypeSafe when TYPESAFE_API_KEY is set and falls back to OpenRouter otherwise; through OpenRouter the model id is sent unchanged, so use jev-latest or an OpenRouter id such as typesafe/jev-1.13; TypeSafe's three-part ids such as jev-1.13.0 are TypeSafe-only. The model's listed per-token price is the same through either route, and OpenRouter's own credit fees are separate. Through TypeSafe that price is $0.042 per million input tokens, output free.

Rules

Four questions

The threshold is the probability at which a rule speaks up. Raise it to hear less, lower it to hear more. Every message prints both the probability and the threshold, so you tune from real output.

jev/name-matches-body

“Does function.name promise a different action, object, or result than function.body performs?”

threshold 0.80 Rule docs

jev/comment-matches-code

“Does function.comment state something about this function that function.body contradicts?” — and separately, “does function.body perform a side effect that function.comment never mentions?” Two defects, two messages. Behaviour carried out by a helper the body calls still counts as the function’s own.

threshold 0.80 Rule docs

jev/helpful-error-message

“Would a developer reading the error message in function.throws.<id>.message in a log be unable to tell what went wrong or what to check next?”

threshold 0.85 Rule docs

jev/check

Your own question, one sentence, asked of every function. The plugin appends “Judge only function in the state.” and, unless you turn it off, asks a second question to locate the offending line.

threshold 0.80 Rule docs

jev/too-large

Not a question. A function over maxFunctionTokens is never sent, and silence would read as a clean bill of health — so the one function nothing checked says so itself. Asks nothing, costs nothing.

no threshold Rule docs

Phrase your own question so that yes means the code is bad. That is the only wording rule.

Agent setup

Set up with an AI agent

If you work with a coding agent, hand it this prompt instead of doing the install by hand. It detects your package manager, edits the ESLint config in place, keeps your key out of git, and runs the linter once so you see real output.

Prompt
Set up eslint-plugin-jev in this repository.

1. Detect the package manager from the lockfile: pnpm-lock.yaml means pnpm,
   yarn.lock means yarn, otherwise npm.
2. Install @shahriarbijoy/eslint-plugin-jev as a dev dependency, plus eslint (^9 or ^10) if
   the repository does not already have it. If the repository contains any
   .ts files, also install @typescript-eslint/parser as a dev dependency.
3. If eslint.config.js, .mjs, .cjs or .ts exists, add
   `import jev from "@shahriarbijoy/eslint-plugin-jev";` at the top and spread
   `...jev.configs.recommended` after the existing entries (use `require`
   and `module.exports` instead if the file is CommonJS). If none exists,
   create eslint.config.mjs containing:
     import jev from "@shahriarbijoy/eslint-plugin-jev";
     export default [...jev.configs.recommended];
   Only if the repository has .ts files, also add
   `import tsParser from "@typescript-eslint/parser";` and put
   `{ files: ["**/*.ts"], languageOptions: { parser: tsParser } }` before
   the spread.
4. If the user gave you a TypeSafe key, append TYPESAFE_API_KEY=<key> to .env
   in the directory ESLint runs from (normally the repository root). If they
   gave you an OpenRouter key instead, append OPENROUTER_API_KEY=<key> there.
   Otherwise add both TYPESAFE_API_KEY= and OPENROUTER_API_KEY= to
   .env.example, and tell the user to get a TypeSafe key at
   https://console.typesafe.ai or an OpenRouter key at https://openrouter.ai/keys,
   then put that key in .env.
5. Make sure .env is listed in .gitignore. Add it if it is missing.
6. Run `npx eslint <one source file>` and paste the warnings verbatim. If it
   prints nothing and no key was given, say the rules are inactive until a
   key is set.
7. Never commit .env. Never print the key.
8. Report what you changed in five lines.

The same text lives at agent-setup.md, and llms.txt points an agent at the rest.

Honesty

What it is not

  • Not for anything countable. Function length, cyclomatic complexity, unused variables — a normal ESLint rule already does those, exactly, for free. Keep them.
  • Not offline, and not private. It needs the network and a key, and the functions it lints are sent to TypeSafe’s API — name, signature, leading comment and body, one function at a time, no surrounding file. Scope it away from anything you cannot send.
  • Not exhaustive about failures. A function whose request hits a rate limit, the per-file deadline, or the token budget is skipped without a diagnostic; only a missing or rejected key and network failures are reported.
  • Not an autofixer. A judgment with a probability attached is not something to apply to your source automatically. name-matches-body offers a rename as a suggestion you accept by hand; nothing changes under --fix.
  • Not a proof. It judges. That is why it defaults to warn and prints the number it judged on.

Input tokens are $0.042 per million and output is free, so a 200-function repository costs well under a cent to lint cold, and nothing at all warm — the cache means you only pay for functions you changed.