Skip to content

PrecisionUtilityGuild/culprits

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

culprits

Test fails, 40 lines changed — these 3 lines are the culprits.

culprits runs your project's existing Vitest or Jest suite once with per-test coverage and prints a small ranked list of the source lines most likely to be the bug — the lines covered disproportionately by failing tests versus passing ones. Every suspect comes with evidence: its scores and the failing tests that hit it.

No config, no daemon, no index, no API key, no LLM. Fully deterministic.

npx @precisionutilityguild/culprits

Example

A four-test suite where clamp high fails:

culprits · runner: vitest · tests: 4 (3 passed, 1 failed)

failing tests
  F1  test/math.test.js > clamp high

top 2 suspects (all lines with nonzero score; ranked by dstar2)

src/math.js
  #1  L6  ochiai 0.707  dstar2 1.000  ef 1/1 (F1)  ep 1/3  [all-fail]
        if (x > hi) return lo; // bug: should return hi
  #2  L5  ochiai 0.577  dstar2 0.500  ef 1/1 (F1)  ep 2/3  [all-fail]
        if (x < lo) return lo;

The bug is ranked #1, with the failing test that proves it.

Usage

culprits                 # human-readable ranked list (default top 10)
culprits --json          # machine format, byte-stable on stdout
culprits -n 20           # show the top 20
culprits --all           # every line with a nonzero score
culprits --runner jest   # override automatic runner detection
culprits -- test/area    # everything after -- is passed through to the runner

The runner's own reporter goes to stderr; the culprits report is the only thing on stdout, so culprits --json > out.json and piping into an agent just work. Exit codes are the contract: 0 ranking produced, 3 suite is green, 2 usage error/refusal, 1 runtime failure. Full contract and flag details: pkg/README.md.

Runner support

Runner Versions Configuration Multi-project
Vitest 4+ Existing config is merged; aliases/plugins/includes survive Refused
Jest 30+ Existing config stays active; environment is injected by CLI Refused

Detection prefers an explicit Jest config or package.json jest field, then a sole declared Jest dependency; otherwise it keeps the Vitest default. Override with --runner vitest|jest.

What to expect from the ranking

Measured on real repos with seeded bugs (receipts in notes/):

  • Several failing unit tests → the right line.
  • One failing unit test → the right function: the suspects fill the culprit's neighborhood, but a single failing test cannot discriminate between lines on its own always-executed path.
  • Integration-heavy full-suite runs → module-level signal at best; scope the run to the affected area (culprits -- test/that-area) for line-level signal.

The tool is honest about its ceiling: a wide tie means "the coverage evidence cannot narrow this further," and it says so rather than faking confidence.

For coding agents

The second consumer is a coding agent mid-debug: instead of re-reading a 40-line diff to guess, it runs culprits --json and jumps to the top suspects. The JSON gives failingTests and suspects[] with per-line scores, counters, and covering-test IDs.

This repo ships a Claude Code skill (skills/culprits/) that teaches the agent when to reach for the tool, how to scope runs, and how to read ties — including the measured cases where scoping hurts the ranking.

How it works

A runner adapter snapshots V8 precise coverage around every test. Vitest uses a custom runner injected through a merged synthetic config; Jest uses a custom jest-circus test environment selected on the command line with --runInBand --maxConcurrency=1. Both paths inherit the project's own transforms, aliases, and test matching. Per line, culprits counts failing/passing tests covering it and ranks by DStar2, breaking ties with Ochiai. If any test file fails to collect, it refuses rather than ranking a partial run.

The technique is spectrum-based fault localization — a 1997-lineage method (Reps/Ball/Das/Larus FSE '97; Tarantula, Jones et al.; Ochiai, Abreu et al.; DStar, Wong et al.) that is standard tooling in Java and was simply never shipped for JS/TS. DStar2-as-default is a measured choice, not a preference: on a 24-seeded-bug benchmark across 7 repos it was never worse than Ochiai and strictly better in 7 trials. Scoring derivation: spec/output-samples/DERIVATION.md.

Limitations

  • Vitest 4+ and Jest 30+ only. Use --runner vitest|jest when detection is ambiguous.
  • Module-scope (top-level) code is not ranked — it runs once at import time and belongs to no single test.
  • Vitest workspace / test.projects and Jest projects configs are refused — run inside an individual project directory instead.
  • Jest transforms must provide a usable source map; files whose executed code cannot be mapped safely are excluded rather than assigned guessed line numbers.
  • Jest currently requires jest-circus with its default Node environment. Custom/per-file environments and test retries are refused when they break one-record-per-test attribution.
  • test.concurrent tests are serialized so their coverage can be attributed.

Repo layout

Part of a family

Three tools, one idea: answers grounded in what your tests actually execute — evidence an agent can't hallucinate. Pick by the question you're holding:

Your question Tool
"A test is failing — which line is the bug?" culprits — spectrum fault localization (npm)
"Where is feature X implemented?" recon — feature location by coverage diff (npm)
"My uncommitted diff broke the tests — which hunks?" diffbisect — delta debugging below commit granularity (npm)

MIT.

About

Test fails, 40 lines changed — these 3 lines are the culprits. Spectrum-based fault localization (SBFL) for vitest: one command, no config, no LLM.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages