@sebassdc/crap4ts
v0.1.1
Published
CRAP metric CLI for TypeScript: combines cyclomatic complexity and test coverage to find risky code, with a bundled cross-agent AI skill.
Maintainers
Readme
crap4ts
CRAP (Change Risk Anti-Pattern) metric for TypeScript projects.
Combines cyclomatic complexity with test coverage to identify functions that are both complex and under-tested — the riskiest code to change.
Quick Start
Install from npm:
npm install -g @sebassdc/crap4tsOr from source:
git clone https://github.com/sebassdc/crap4ts.git
cd crap4ts
npm install
npm run build
npm install -g .Configure your test runner to emit Istanbul JSON coverage:
Vitest (vitest.config.ts):
export default defineConfig({
test: {
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'json'],
},
},
});Jest (jest.config.ts):
export default {
coverageReporters: ['text', 'json'],
};Run from your project root (where src/ lives):
crap4tscrap4ts automatically deletes stale coverage data, runs your test suite with coverage, and prints the report.
Output
CRAP Report
===========
Function Module CC Cov% CRAP
-------------------------------------------------------------------------------------
complexFn my.module 12 45.0% 130.2
simpleFn my.module 1 100.0% 1.0CLI Options
crap4ts --help # show usage and available options
crap4ts --version # print version number
crap4ts --src lib # analyze from lib/ instead of src/
crap4ts --exclude dist # exclude paths containing "dist"
crap4ts --timeout 120 # set analysis timeout to 120 secondsConfiguration File
Instead of passing flags every time, create a crap4ts.config.json (or .crap4tsrc.json) in your project root:
{
"src": "lib",
"exclude": ["dist", "fixtures"],
"output": "json",
"failOnCrap": 30,
"timeout": 120
}File Discovery
crap4ts looks for config files in the current working directory in this order:
crap4ts.config.json(preferred).crap4tsrc.json(fallback)
The first file found is used. If neither exists, all options use their defaults.
To load a config file from a custom path, use the --config flag:
crap4ts --config configs/crap4ts.jsonCLI Override Precedence
CLI flags always take precedence over config file values. For example, if your config file sets "src": "lib" but you run crap4ts --src app, the app directory is used.
Supported Keys
| Key | Type | Description | Default |
|----------------------|------------|--------------------------------------------------|---------|
| src | string | Source directory to analyze | "src" |
| exclude | string[] | Exclude paths containing these patterns | [] |
| output | string | Output format: "text", "json", "markdown", or "csv" | "text"|
| runner | string | Test runner: "vitest" or "jest" | auto |
| coverageCommand | string | Custom shell command to generate coverage | none |
| failOnCrap | number | Fail if any CRAP score >= this value | none |
| failOnComplexity | number | Fail if any cyclomatic complexity >= this value | none |
| failOnCoverageBelow| number | Fail if any function coverage < this % (0-100) | none |
| top | number | Show only the top N entries | all |
| timeout | number | Analysis timeout in seconds | 600 |
Unknown keys are silently ignored, so config files are forward-compatible with future versions.
Programmatic API
crap4ts can be used as a library in your own tools and scripts. The API assumes coverage data already exists (run your test suite with coverage first).
import { generateReport, crapScore, extractFunctions } from '@sebassdc/crap4ts';
// High-level: analyze an entire source tree against existing coverage
const { entries } = generateReport({
srcDir: 'src',
coverageDir: 'coverage',
});
entries.forEach(e => console.log(`${e.name}: CRAP ${e.crap}`));generateReport(options)
Finds source files, parses coverage, analyzes each file, and returns entries sorted by CRAP score. This does not run your test suite -- it reads from an existing coverage-final.json.
| Option | Type | Description | Default |
|---------------|------------|--------------------------------------------------|---------|
| srcDir | string | Source directory to scan for .ts files | -- |
| coverageDir | string | Directory containing coverage-final.json | -- |
| filters | string[] | Only include files matching these substrings | [] |
| excludes | string[] | Exclude files whose path contains these substrings| [] |
Low-level exports
For fine-grained control, individual functions are also exported:
import {
extractFunctions, // parse a TS source string into FunctionInfo[]
parseCoverage, // read coverage-final.json from a directory
coverageForRange, // get coverage % for a line range
sourceToModule, // convert file path to dotted module name
crapScore, // compute CRAP score from complexity and coverage
sortByCrap, // sort CrapEntry[] by CRAP descending
formatReport, // render text table from CrapEntry[]
formatJsonReport, // render JSON string from CrapEntry[]
formatMarkdownReport, // render markdown table from CrapEntry[]
formatCsvReport, // render CSV string from CrapEntry[]
findSourceFiles, // find all .ts files in a directory
filterSources, // filter file list by substring patterns
analyzeFile, // analyze a single file against coverage data
} from '@sebassdc/crap4ts';TypeScript types CrapEntry, FunctionInfo, CoverageData, and FileCoverageData are also exported.
CI Integration
Use threshold flags to fail CI when code quality drops below acceptable levels:
# Fail if any function has CRAP >= 30 or coverage below 70%
crap4ts --fail-on-crap 30 --fail-on-coverage-below 70
# Fail if any function has complexity >= 15, show only top 10
crap4ts --fail-on-complexity 15 --top 10Multiple thresholds can be combined. The report is always printed before any failure.
The --top flag limits displayed entries but all entries are evaluated against thresholds.
Exit Codes
| Code | Meaning | |------|---------| | 0 | Pass -- no threshold violations | | 1 | Threshold violated or runtime error | | 2 | Usage error (invalid flags or arguments) |
Output Formats
crap4ts supports four output formats:
crap4ts # default text table
crap4ts --json # JSON (shorthand for --output json)
crap4ts --output markdown # Markdown table
crap4ts --output csv # CSVText (default)
CRAP Report
===========
Function Module CC Cov% CRAP
-------------------------------------------------------------------------------------
complexFn my.module 12 45.0% 130.2
simpleFn my.module 1 100.0% 1.0JSON
{
"tool": "crap4ts",
"entries": [
{
"name": "complexFn",
"module": "my.module",
"complexity": 12,
"coverage": 45,
"crap": 130.2
}
]
}Markdown
# CRAP Report
| Function | Module | CC | Cov% | CRAP |
|---|---|---:|---:|---:|
| complexFn | my.module | 12 | 45.0% | 130.2 |
| simpleFn | my.module | 1 | 100.0% | 1.0 |CSV
Function,Module,CC,Coverage,CRAP
complexFn,my.module,12,45.0,130.2
simpleFn,my.module,1,100.0,1.0Text output is the default. Use --json as a shorthand or --output <format> for any format.
Excluding Paths
Use --exclude to filter out files whose path contains a given substring. The flag is repeatable:
# Skip dist and fixtures directories
crap4ts --exclude dist --exclude fixtures
# Analyze lib/ but skip generated code
crap4ts --src lib --exclude __generated__
# Combine with other options
crap4ts --src packages/core/src --exclude __mocks__ --exclude .stories --jsonFiltering
Pass module path fragments as arguments to filter:
crap4ts parser validator # only files matching those stringsCRAP Formula
CRAP(fn) = CC² × (1 - coverage)³ + CC- CC = cyclomatic complexity (decision points + 1)
- coverage = fraction of statements covered by tests
| Score | Risk | |-------|------| | 1–5 | Low — clean code | | 5–30 | Moderate — refactor or add tests | | 30+ | High — complex and under-tested |
What It Counts
Decision points that increase cyclomatic complexity:
if/ ternary (c ? a : b)else if(each adds 1)for/for...of/for...inwhile/do...whilecatchclauses (each adds 1)caseclauses inswitch(eachcaseadds 1;defaultdoes not)&&/||/??operators (each operator adds 1)
Nested functions and class bodies are skipped — only the enclosing function's body is analyzed.
Compatibility
| Layout | Status | Notes |
|--------|--------|-------|
| Standard (src/) | Supported | Default, no config needed |
| Custom source dir | Supported | Use --src <dir> |
| Monorepo workspace | Supported | Point --src to package source |
| Multiple src dirs | Supported | Use --exclude to filter |
| Windows paths | Supported | Normalized internally |
| Istanbul JSON coverage | Required | Other formats not supported |
| Branch coverage | Not used | Statement coverage only |
Limitations
- Only TypeScript (
.ts) files are analyzed —.tsx,.js, and.jsxfiles are ignored. - Only functions found within the configured source directory (default:
src/) are scanned. - Coverage data must be in Istanbul JSON format (
coverage-final.json). Other coverage formats are not supported. - Runner detection is heuristic: crap4ts checks for Vitest config files first, then Jest config files, then falls back to the
scriptsfield inpackage.json. Use--runner vitest|jestto override. - Nested functions are attributed to their enclosing function rather than being extracted as separate symbols.
- Dynamic or computed method names (e.g.,
[Symbol.iterator]()or["methodName"]()) are not extracted. - Only statement coverage is used when computing the coverage fraction — branch and function coverage are ignored.
- Coverage is calculated using statement-to-function overlap: a statement is attributed to a function if its line range overlaps the function's line range. This is an approximation; a multi-line statement that spans a function boundary may be counted for both the enclosing and the adjacent function.
For advanced usage patterns, see docs/advanced-usage.md.
Extracted Symbols
- Top-level
functiondeclarations - Top-level
const f = () => {}andconst f = function() {} - Class
constructor, methods, getters, and setters (named asClassName.methodName) - Object literal methods, getters, and setters in top-level variable declarations (named as
varName.methodNameorvarName['string-key'])
Nested functions (functions defined inside other functions, methods, or arrows) are intentionally excluded. They are not extracted as separate symbols; their complexity is attributed to the enclosing function.
Cross-Agent Skill
crap4ts ships a bundled SKILL.md that you can install into the cross-agent
skill directory consumed by Claude Code, Codex, Pi, and any harness that reads
.agents/skills/.
# Global install for the current user (~/.agents/skills/crap4ts/SKILL.md)
crap4ts skill install
# Project-local install (./.agents/skills/crap4ts/SKILL.md)
crap4ts skill install --project
# Print the bundled skill
crap4ts skill show
# Print where the skill is (or would be) installed
crap4ts skill path
crap4ts skill path --project
# Remove
crap4ts skill uninstall
crap4ts skill uninstall --projectThe bundled skill lives inside the published package at src/skill/SKILL.md
and is shipped via the files field in package.json.
Claude Code
Claude Code reads skills from ~/.claude/skills/, not ~/.agents/skills/.
After installing, symlink the skill so both directories stay in sync:
crap4ts skill install
ln -s ~/.agents/skills/crap4ts ~/.claude/skills/crap4tsFor project-local installs, symlink into .claude/skills/ at the repo root:
crap4ts skill install --project
ln -s .agents/skills/crap4ts .claude/skills/crap4tsRunner Configuration
crap4ts supports three ways to run your test suite for coverage, applied in this order of precedence:
1. --coverage-command (highest priority)
Run an arbitrary shell command instead of the built-in runner logic. The command is executed with shell: true, so pipes, environment variables, and shell syntax all work.
# Monorepo: run tests only for a specific package
crap4ts --coverage-command "npm run test:api -- --coverage"
# Custom script with environment variables
crap4ts --coverage-command "CI=1 yarn test --coverage --coverageReporters=json"
# Turborepo / Nx workspace
crap4ts --coverage-command "npx turbo run test -- --coverage"The command must produce a coverage/coverage-final.json file in Istanbul JSON format.
2. --runner vitest|jest (skip auto-detection)
Use the built-in runner invocation for Vitest or Jest, but skip the config-file heuristic:
# Force Jest even if a vitest.config.ts exists
crap4ts --runner jest
# Force Vitest in a project without a vitest.config file
crap4ts --runner vitest3. Auto-detection (default)
When neither flag is provided, crap4ts detects the runner automatically:
- If any
vitest.config.*file exists, use Vitest. - If any
jest.config.*file exists, use Jest. - If
package.jsonlistsjestas a dependency, use Jest. - Otherwise, default to Vitest.
Troubleshooting
| Error | Fix |
|-------|-----|
| Source directory 'src' not found | Use --src <dir> to point to your source directory |
| No TypeScript files found | Verify your source directory contains .ts files |
| No files match the filters | Check your filter arguments match actual file paths |
| Unable to parse package.json | Fix your package.json or use --runner vitest\|jest |
| Coverage run failed | Ensure your test suite passes independently before running crap4ts |
| No coverage-final.json found | Configure your test runner to output Istanbul JSON coverage (see Quick Start) |
| Coverage run timed out | Increase timeout with --timeout <seconds> |
Development
npm install
npm test # run tests
npm run build # compile to dist/
npm run coverage # run tests with coverageInspiration
This project was inspired by crap4clj by Uncle Bob.
License
MIT
