ts-anti-patterns
v1.0.3
Published
CRAP (Change Risk Anti-Patterns) for TypeScript/JavaScript - inspired by minikin/cargo-crap
Downloads
571
Maintainers
Readme
ts-anti-patterns
Token-Aware Refactoring for AI Agents
Every file an agent reads costs tokens. On a 500-file TSX codebase that adds up
to millions of input tokens before a single line changes.
ts-anti-patterns cuts that bill: it ranks the riskiest functions first so the
agent opens only what matters - not the whole tree.
The math
Sample: 300 TSX components averaged ~10k tokens/file (~1 token per 3.2–3.5 chars).
Claude Code meters the same tokens as the API. Claude Sonnet 4.6 (typical coding model): $3 / MTok input, $15 / MTok output — Anthropic pricing. Input costs below; output not included (refactor replies add on top).
| Approach | Input (≈) | vs. full read | Cost (≈) | Saved |
|---|---:|---|---:|---:|
| Read all 500 files | 5M | 💰 baseline | $15.00 | — |
| ts-anti-patterns report → pick targets | ~60k | 83× less 💰 | $0.18 | $14.82 💰 |
| Report + 20 worst files (whole) | ~260k | 19× less 💰 | $0.78 | $14.22 💰 |
| Report + 20 function slices (file:line) | ~90k | 55× less 💰 | $0.27 | $14.73 💰 |
One function slice ≈ 1.5k tokens. Fat components can hit 30k+ each — CRAP score finds the hotspot inside, line count doesn't.
Profit: one scouting pass with ts-anti-patterns instead of reading every file keeps ~$15 in input cost — before you pay for a single line of agent output. On a 10-pass refactor that's ~$150 of input you never burn just finding where to start.
Why run it before an agent pass
- Spend tokens on complexity, not volume - fix ranked hotspots, not 500 full files
- Precise entry points -
file:linelets the agent read a 1.5k-token slice, not a 30k-token component - Machine-readable output - JSON / Markdown / HTML parsed before the agent opens a single file
What is CRAP?
Change Risk Anti-Patterns combines cyclomatic complexity with test coverage into one score - high CRAP = high complexity + low coverage = exactly where bugs hide.
CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m)When no coverage data is provided, ts-anti-patterns falls back to a CC-only mode and uses cyclomatic complexity as the score.

Install
npm install -g ts-anti-patternsOr use npx without installing (recommended for one-off checks and CI):
npx ts-anti-patterns@latest ./srcUsing with npx
Run from your project root (where package.json and src/ live). No global install required.
CC-only (default, no coverage)
# Scan ./src (auto-detected if you omit the path)
npx ts-anti-patterns@latest
# Explicit path
npx ts-anti-patterns@latest ./src
# HTML report in the current directory
npx ts-anti-patterns@latest ./src --format html --output crap-report.html
# CI gate (exit 1 if anything is above threshold)
npx ts-anti-patterns@latest ./src --threshold 30 --fail-above --summaryFull CRAP (with coverage)
# One command: try to run tests with coverage, then analyze
npx ts-anti-patterns@latest --full
# Or generate coverage yourself, then analyze
npm run test:coverage
npx ts-anti-patterns@latest --cov
# Explicit coverage file
npx ts-anti-patterns@latest --lcov coverage/lcov.info --format html --output crap-report.htmlCustom coverage command (monorepos, pnpm, turbo, etc.)
npx ts-anti-patterns@latest --full --coverage-command "pnpm test -- --coverage"Pin a version (CI)
npx [email protected] ./src --threshold 30 --fail-aboveInstall agent skill (Cursor / compatible agents)
ts-anti-patterns ships a bundled SKILL.md so agents know how to run analysis. Install it with npx (no global install needed):
# User-wide (recommended once per machine)
npx ts-anti-patterns@latest skill install
# Only for the current repo (commit .agents/skills/ts-anti-patterns/SKILL.md if you want the team to share it)
npx ts-anti-patterns@latest skill install --projectOther skill commands:
# Show where the skill file would be / is installed
npx ts-anti-patterns@latest skill path
npx ts-anti-patterns@latest skill path --project
# Print bundled skill content
npx ts-anti-patterns@latest skill show
# Remove installed skill
npx ts-anti-patterns@latest skill uninstall
npx ts-anti-patterns@latest skill uninstall --projectPaths:
| Scope | Location |
|---|---|
| Global | ~/.agents/skills/ts-anti-patterns/SKILL.md |
| Project | ./.agents/skills/ts-anti-patterns/SKILL.md |
Cursor: agents read skills from .agents/skills/. After skill install, open a new chat so the skill is picked up. Some setups also use ~/.cursor/skills/ - if needed, symlink:
ln -s ~/.agents/skills/ts-anti-patterns ~/.cursor/skills/ts-anti-patternsLocal development (this repo)
If you are hacking on ts-anti-patterns itself, use the built CLI:
npm run build
node dist/cli.js ./src --format html --output crap-report.htmlOr from another project, point npx at the folder:
npx /path/to/ts-anti-patterns/dist/cli.js ./srcQuick Start - CC-only (no coverage needed)
# Scan a folder
ts-anti-patterns ./src
# Multiple paths (shell glob expansion)
ts-anti-patterns ./src/* ./packages/*/src
# CI gate
ts-anti-patterns ./src --threshold 30 --fail-aboveThe CLI auto-detects ./src if no path is given.
By default, reports show the top 20 worst results; override with --top.
Quick Start - full CRAP mode
One command (auto-generate coverage, then analyze):
ts-anti-patterns --fullGenerate coverage with any standard runner - then enable it with --cov
for auto-detect (or pass an explicit file):
coverage/lcov.info, coverage/coverage-final.json,
coverage/coverage-summary.json, coverage/clover.xml, or
coverage/cobertura-coverage.xml:
# Vitest
npx vitest run --coverage --coverage.reporter=lcov
# or c8
npx c8 --reporter=lcov npm test
# or Jest
npx jest --coverage --coverageReporters=lcov
# Then run one of:
ts-anti-patterns --cov # auto-detects and scores CRAP
ts-anti-patterns --run-coverage --cov # generate coverage first, then score
ts-anti-patterns --full # shorthand: run coverage + CRAP analysis
ts-anti-patterns --full --coverage-command "pnpm test -- --coverage" # custom command
ts-anti-patterns --lcov coverage/lcov.info # explicit path (exit 2 if missing)
ts-anti-patterns # default CC-only (no coverage)
ts-anti-patterns --source-map auto # for transpiled coverage (dist → src)Coverage priority: branch (BRDA) > function (FN/FNDA) > line-range
fallback. Each row carries a confidence indicator: ● exact, ◐ range,
○ none. When some functions have no coverage data, the --missing
policy chooses how to score them:
| Policy | Effect |
| ------------- | -------------------------------------------------- |
| pessimistic | unmatched = 0 % (default - punishes blind spots) |
| optimistic | unmatched = 100 % (kind to noisy excludes) |
| skip | drop unmatched rows from the report |
Options
| Flag | Default | Purpose |
|---|---|---|
| [...paths] | ./src (auto) | One or more files/dirs to analyze |
| --threshold <n> | 30 | Score above which a function is flagged |
| --fail-above | off | Exit 1 if any function ≥ severity threshold |
| --fail-above-severity <sev> | warning | info | warning | error |
| --top <n> | 20 | Show only N worst offenders |
| --min <n> | - | Hide entries below this score |
| --format <fmt> | human | human | json | html | markdown | github | sarif | pr-comment |
| --output <path> | stdout | Write to file instead of stdout |
| --exclude <glob> | - | Exclude glob (repeatable, .gitignore-aware) |
| --allow <glob> | - | Allow-list glob (repeatable) |
| --cov | off | Enable auto-detection from coverage/* |
| --full | off | One-command mode: generate coverage then analyze |
| --run-coverage | off | Generate coverage before analysis |
| --coverage-command <cmd> | auto | Custom command used by --run-coverage / --full |
| --lcov <path> | - | Explicit LCOV file (exit 2 if missing) |
| --coverage <path> | - | Explicit coverage file (lcov/json-summary/clover/cobertura) |
| --source-map <auto\|dir> | - | Translate coverage through source maps (dist/*.js → src/*.ts) |
| --missing <policy> | pessimistic | pessimistic | optimistic | skip |
| --no-cov | on | Force CC-only mode |
| --skip-anonymous | off | Hide <arrow@N> / <fn@N> rows |
| --count-nullish-coalescing | off | Count ?? as a branch in CC + cognitive |
| --no-cognitive | - | Skip cognitive-complexity computation |
| --no-hints | - | Suppress per-function actionable hints |
| --baseline <path> | - | Compare against a saved JSON report |
| --fail-regression | off | Exit 1 when any regression vs baseline is detected |
| --epsilon <n> | 0.01 | Score delta treated as unchanged for --baseline |
| --summary | off | Print only the aggregate headline (no table) |
| --diagnose <file> | - | Debug one file: every AST function + why it was kept/filtered |
| --workspace | off | Scan each package in package.json#workspaces |
| --watch | off | Re-render in human format on changes (debounced 200ms) |
| --no-cache | - | Disable the .ts-anti-patterns-cache/ AST cache |
| --jobs <n> | os.cpus() | Parallel file-analysis concurrency |
| --config <path> | discovered | Path to a config file |
Subcommands
| Command | Description |
|---|---|
| ts-anti-patterns skill <install\|uninstall\|show\|path> | Install bundled agent skill (--project for repo-local). |
| ts-anti-patterns init | Create .ts-anti-patterns.json and add an crap script to package.json. Idempotent. |
| ts-anti-patterns explain <term> | Print the glossary entry for crap, cc, cognitive, coverage, confidence, severity, missing, or pragma. |
| ts-anti-patterns explain | List every glossary term. |
Output Formats
human(default) - colorized terminal table with summary, hints, and a stickyΔ since last runline.json-report-v1envelope: stable shape suitable for--baselinelater.html- single self-contained file, no network. Severity colors, threshold slider, search, suppressed/no-coverage toggles, glossary popovers, CSV export, dark mode, print stylesheet.markdown- GFM table with severity emoji. Drops into PR descriptions cleanly.github- Actions annotations (::error,::warning,::notice).sarif- SARIF 2.1.0 for GitHub Code Scanning.pr-comment- markdown with<!-- ts-anti-patterns-report -->marker so a bot can update the same comment in place. With--baseline, regressions go on top.
Documentation Index
.github/docs/architecture.md- architecture and design decisions..github/docs/contributing.md- tests, PR checklist, release smoke.examples/github-actions-ci.yml- CI gate + SARIF + PR comment.examples/lefthook.yml- local pre-push gate.schemas/report-v1.json- JSON report contract.schemas/delta-v1.json- baseline diff contract.
Programmatic API
import { analyze, renderHtml, computeCrap, GLOSSARY } from "ts-anti-patterns"
const { entries, meta } = await analyze({
paths: ["src"],
threshold: 30,
noCov: true,
})
const html = await renderHtml(entries, meta, { threshold: 30 })Every renderer (renderHuman, renderJson, renderHtml, renderMarkdown, renderGithub, renderSarif, renderPrComment) and scoring primitive (computeCrap, scoreOf, severityOf) is re-exported from the package root.
CI Quick Start
- PR gate:
ts-anti-patterns --threshold 30 --fail-above --summary - Regression gate:
ts-anti-patterns --cov --baseline baseline.json --fail-regression --format json - Code Scanning:
ts-anti-patterns --cov --format sarif --output ts-anti-patterns.sarif - PR comment:
ts-anti-patterns --cov --baseline baseline.json --format pr-comment --output ts-anti-patterns.md
For complete workflows, copy examples from examples/.
Severity Bands
| Severity | Score | Default behavior |
|---|---|---|
| ok | ≤ threshold/2 | clean |
| info | ≤ threshold | "borderline, watch it" |
| warning | ≤ 2 × threshold | triggers --fail-above |
| error | > 2 × threshold | triggers --fail-above |
Config File
Discovered via cosmiconfig in this order: .ts-anti-patterns.json, .ts-anti-patterns.yaml, ts-anti-patterns.config.{js,cjs,mjs,json}, or a ts-anti-patterns key in package.json.
{
"threshold": 30,
"exclude": ["**/legacy/**"],
"skipAnonymous": false,
"countNullishCoalescing": false,
"failAboveSeverity": "warning"
}CLI flags always override the config.
cargo-crap - Rust implementation of the CRAP metric.
License
MIT
