@anatolykhelmer/deep-cover
v0.6.1
Published
Agentic code coverage analyzer
Maintainers
Readme
DeepCover
An agentic code coverage analyzer that goes beyond line coverage. DeepCover combines deterministic AST analysis with bounded LLM reasoning to produce a "meaningful coverage" score — measuring whether your tests actually protect your code, not just execute it.
Why — DeepCover vs. Jest Coverage
Jest with Istanbul tells you what ran. DeepCover tells you what's actually protected.
100% line coverage doesn't mean your tests are good:
it('should create order', async () => {
const result = await service.createOrder(mockInput);
expect(result).toBeDefined(); // 100% line coverage, near-useless assertion
});Istanbul reports full coverage for createOrder. But that test would still pass if the method returned null, an empty object, or a completely wrong order. The assertion is too weak to catch any regression.
What each tool sees
| Question | Jest/Istanbul | DeepCover Extractor | |----------|:---:|:---:| | Was this line executed? | Yes | -- | | Was this branch hit? | Yes | -- | | Is the assertion meaningful? | -- | Yes | | Are all domain states tested? | -- | Yes | | Would tests catch a mutation? | -- | Yes | | Which untested code is riskiest? | -- | Yes | | What's mocked vs. real? | -- | Yes | | Dependency/transitive coverage? | -- | Yes |
What DeepCover adds on top of line coverage
- Assertion strength —
toBeDefined()is weak,toEqual(expected)is strong,toHaveBeenCalledWith(...)verifies interactions. Istanbul can't distinguish these. - Branch semantics — Istanbul knows a branch was hit; the Extractor knows it's a guard clause, error path, or retry condition — and feeds the exact condition expressions to the Reasoner for state discovery.
- Compound conditions —
if (a || b)is four things to test, not one. Istanbul'sbinary-exprcounters record how often each operand was evaluated, never which one was decisive, so a guard entered only throughastill reports as fully covered. The Extractor splits the chain into its operands and theuntested-condition-operanddetector flags the ones no test ever drives — the operands you could delete with the suite still green. - Domain states — the Reasoner identifies business scenarios, error conditions, and edge cases from branch conditions and test names. Istanbul can't tell you that "HTTP 429 rate limiting" is tested but "token expiry race condition" is not.
- Dependency graph — if Controller delegates to Service which delegates to Gateway, the Extractor traces transitive paths. Istanbul treats each file in isolation.
- Criticality ranking — a public method with high complexity, external calls, and error handling matters more than a simple getter. Istanbul counts all lines equally.
- Mock analysis — detects tests that mock away the very thing they claim to test.
Better together
DeepCover doesn't replace Jest coverage — it merges with it. When both are available, Istanbul provides ground-truth "did this code run?" and the Extractor answers "do the tests actually protect it?" Neither alone gives the full picture.
Architecture
Four-phase pipeline:
┌─────────────────────┐
│ npm test │
│ (Jest + Reporter) │
└────────┬────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
jest-runtime.json istanbul-coverage coverage-final
(pass/fail/dur) .json (line/branch) (Jest default)
│ │
Source + Tests ──► [Extractor] ──► CodeModel │
(ts-morph) │ │
▼ ▼
[Coverage Resolver]
│
▼
ResolvedCoverage
│
┌────────────────┼────────────────┐
▼ ▼
[Reasoner] [Scorer]
(LLM/Cursor) (4 sub-scores)
│ ▼
└──────────────────────────► ScoreResult- Extractor — Deterministic AST analysis: classes, methods, branches, dependencies, assertions, mocks
- Coverage Resolver — Merges static AST analysis with Jest/Istanbul runtime data into unified, class-qualified coverage
- Reasoner — LLM semantic analysis with enriched prompts: domain states (with branch conditions + test names + state taxonomy), assertion quality (with target method info), criticality (with blast radius from dependency graph), transitive coverage (with mock detection + intra-class call graph)
- Scorer — Deterministic formula: 4 sub-scores with bounded LLM influence (±20%)
Quick Start
Prerequisites: Node.js >= 18, a TypeScript project tested with Jest.
npm install -g @anatolykhelmer/deep-cover
# One-shot, deterministic — no API key
npx @anatolykhelmer/deep-cover run --root /path/to/project --module src/your-module --no-llm
# CI gating — fail if the score is below the threshold
npx @anatolykhelmer/deep-cover run --root /path/to/project --module src/your-module \
--no-llm --format score --min-score 60DeepCover runs as three stages that always execute in the same order and always
communicate through files in <root>/.deepcover/:
| Stage | Command | Writes | LLM? |
|---|---|---|---|
| 1 | deepcover extract --module <path> | code-model.json, prompts.json | no |
| 2 | deepcover reason | reasoner-output.json | yes — or a template for your agent |
| 3 | deepcover analyze | nothing (prints the report) | no |
deepcover run performs all three in one command. Stage 2 is the only stage that
involves an LLM, and it always says which Reasoner it used.
Strongly recommended: wire up the Jest reporter and run tests with --coverage before analyzing.
Limitations (honest): DeepCover targets TypeScript sources and Jest tests.
What's new in 0.6.1 (unreleased)
Bug detection
The unhandled-error-path detector now also scans standalone functions, not
just class methods — a function with a try/catch or that throws, and no
test that provokes the error path, now surfaces a signal the same way a
method does. This is new output on existing analyses; nothing else about
potentialBugs changed.
Coverage resolution
A criticality or assertion-quality rating naming a class method that does not exist could, in a narrow case, silently pick up the coverage of a same-named standalone function declared in the same file instead of being dropped. Fixed — such a lookup now correctly resolves to nothing, matching every other fail-closed path in the resolver.
What's new in 0.6.0
Config files are validated
deepcover.config.{ts,js,json} is now checked against a schema when it loads.
An unknown key, an invalid value, or a file that cannot be read or parsed
stops the run with exit code 1, naming the file and every offending key:
Invalid config in /project/deepcover.config.json:
✖ Unrecognized key: "resoner"
✖ Invalid option: expected one of "cursor"|"anthropic"|"mock"|"none"
→ at reasoner.provider
Fix the config, or delete it to run with defaults.DeepCover stops rather than falling back to defaults because the fallback
changes what it does — most sharply reasoner.provider, where a typo in one
field would quietly run the analysis against a different provider than the one
configured. In CI, where --min-score gates the build, silently-wrong numbers
are worse than a stopped run. The check happens before any work, so a failing
run writes no artifacts.
Having no config file is still perfectly normal and runs on defaults silently — this applies only to a config file that exists and cannot be honoured.
A partially specified section now keeps the defaults for the fields it does not
mention; previously weights: { assertionQuality: 0.5 } silently dropped the
other three weights.
State coverage
All four state consumers (aggregate state coverage, per-method scores, untested lists, gap generation) now read one StateCatalog — the union of statically extracted states and reasoner-discovered states, with testedness decided once. Scores will shift on re-analysis:
- The state metric is now applicable without an LLM run when the extractor finds static states.
- A reasoner state only counts as tested when the resolver confirms its method is covered (this floor previously applied per-method but not to the aggregate).
- A static state is tested per affected method, not when any affected method happens to be covered.
- State gaps are emitted per state×method with unified risk rules; the same state found by both sources yields one gap.
- Gap
scenariofor a state is now the bare state name (previously static gaps usedstate "X" (values)). - Reasoner states naming a method or class the code model does not declare are dropped from scoring entirely.
Migrating to 0.5.0
ResolvedCoverage's accessors — getMethodCoverage, isMethodCovered,
getTestsForMethod — now require the filePath third argument that 0.4.0
made optional.
// 0.4.0 — compiled, but fell back to a name-only lookup
coverage.getMethodCoverage('OrderService', 'create');
// 0.5.0 — the declaring file is part of the identity
coverage.getMethodCoverage('OrderService', 'create', 'src/order.service.ts');The optional argument was the problem: omitting it fell back to a
ClassName.methodName index that returns nothing once two files declare the
same class name, and every caller read that nothing differently — one as "no
coverage data", another as "untested". Requiring it makes each such site a
compile error instead.
If you only have a class name (for example when consuming Reasoner output, which names an owner but never a file), resolve it first:
import { buildClassFileOwners, resolveReasonerOwnerFile } from '@anatolykhelmer/deep-cover';
const owners = buildClassFileOwners(codeModel.modules);
const filePath = resolveReasonerOwnerFile(rating.className, owners);
// null → that class name is declared in several files; drop the judgment
// rather than scoring it against whichever declaration a lookup reaches firstBug detectors now also scan mod.functions, so standalone functions produce bug
signals, and every detector scopes test evidence to the declaring file.
Migrating to 0.4.0
Internal coverage identity is now file-qualified: class methods are keyed
filePath:ClassName.methodName (matching how standalone functions were already
keyed by file), so two files that both export a class with the same name no
longer overwrite each other's coverage.
Re-run deepcover extract (or run) after upgrading. A code-model.json
produced by 0.3.x keys class methods as ClassName.methodName; the 0.4.0
resolver looks them up file-qualified and would silently find no static
coverage in the old artifact.
For the API, ResolvedCoverage accessors (getMethodCoverage,
isMethodCovered, getTestsForMethod) gained an optional filePath third
argument. Without it, lookups of a class name declared in several files fail
closed (return nothing) rather than guess. 0.5.0 makes this argument
required — see above.
Known limitation: when duplicate class names exist, a test's credit is attributed via the file its spec imports the class from (barrel re-exports are followed to the declaring file). If the import cannot be resolved statically, the credit is dropped rather than guessed.
Migrating from 0.2.x
analyze and score no longer extract or call an LLM — they score the artifacts
on disk. The removed flags fail with the replacement command rather than being
ignored.
| 0.2.x | 0.3.0 |
|---|---|
| analyze --module X --no-llm | run --no-llm --module X |
| analyze --module X (API provider) | run --module X |
| analyze --reasoner-input f.json | analyze — .deepcover/reasoner-output.json is the default input |
| score --module X --no-llm --min-score 60 | run --no-llm --module X --format score --min-score 60 |
--min-score and --bug-threshold now work with every --format, so
analyze --format json --min-score 60 prints the full report and gates on it.
Install for Cursor (recommended)
DeepCover uses the Cursor agent as the Reasoner — no API key. The npm package and the Cursor skill are separate: installing the CLI does not install the skill.
1. Install the CLI
npm install -g @anatolykhelmer/deep-coverOr without a global install:
npx @anatolykhelmer/deep-cover --help2. Install the Cursor skill (once)
deepcover init --agent cursor
# same as: deepcover init
# → ~/.cursor/skills/deepcover/SKILL.mdShare with the team (commit the skill into the repo):
deepcover init --agent cursor --project
# → ./.cursor/skills/deepcover/SKILL.md| | Personal (deepcover init) | Project (--project) |
|---|---|---|
| Where | ~/.cursor/skills/deepcover/ | ./.cursor/skills/deepcover/ |
| Scope | All your projects | This repo only |
| Share | No | Yes — commit and push |
3. Run in Cursor Agent
Open the project in Cursor → Agent chat (not Ask) → ask:
run deepcover on src/your-module
The skill runs the equivalent of:
npx deepcover extract --root <PROJECT_ROOT> --module <MODULE_PATH> --bugs
# agent fills .deepcover/reasoner-output.json
npx deepcover analyze --root <PROJECT_ROOT> --bugsYou do not need to fill JSON by hand — the agent does that step for you.
Check it worked: after deepcover init, the skill file above should exist. If the agent ignores the skill, start a new Agent chat or reload Cursor so skills are picked up.
Install for Claude Code
Same skill workflow as Cursor — Claude Code is the Reasoner (uses your Claude Code / Anthropic subscription). No separate ANTHROPIC_API_KEY for the agent path; do not set reasoner.provider: 'anthropic' unless you want the CLI to call the API itself.
1. Install the CLI
npm install -g @anatolykhelmer/deep-cover2. Install the Claude Code skill (once)
deepcover init --agent claude
# → ~/.claude/skills/deepcover/SKILL.mdShare with the team:
deepcover init --agent claude --project
# → ./.claude/skills/deepcover/SKILL.md| | Personal | Project (--project) |
|---|---|---|
| Where | ~/.claude/skills/deepcover/ | ./.claude/skills/deepcover/ |
| Scope | All your projects | This repo only |
| Share | No | Yes — commit and push |
3. Run in Claude Code
In a Claude Code session on the project, ask:
run deepcover on src/your-module
The skill runs the equivalent of:
npx deepcover extract --root <PROJECT_ROOT> --module <MODULE_PATH> --bugs
# agent fills .deepcover/reasoner-output.json
npx deepcover analyze --root <PROJECT_ROOT> --bugsCheck it worked: the skill file above should exist. If Claude ignores it, restart Claude Code or run /reload-skills.
Install for Anthropic
Use this when you want the CLI to call Anthropic directly (CI, headless, no Cursor/Claude Code agent). No agent skill needed.
1. Install the peer dependency
In the project you analyze (or globally alongside the CLI):
npm install @anthropic-ai/sdkWithout the SDK, DeepCover prints a clear install error instead of failing at import time.
2. Set the API key
DeepCover reads ANTHROPIC_API_KEY from the environment (or reasoner.apiKey in config). It does not auto-load a .env file.
export ANTHROPIC_API_KEY=sk-ant-...3. Point config at Anthropic
Create or edit deepcover.config.ts in the project root:
export default {
reasoner: {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514', // optional — this is the default
// apiKey: process.env.ANTHROPIC_API_KEY, // optional if the env var is set
},
};4. Run (do not pass --no-llm)
npx deepcover run --root <PROJECT_ROOT> --module <MODULE_PATH> --bugsrun performs extract → reason (Anthropic Messages API) → analyze in one command.
| | Cursor / Claude Code | Anthropic API |
|---|---|---|
| Skill / Agent | yes (init --agent …) | no |
| API key | not needed for agent path | ANTHROPIC_API_KEY |
| How to run | Agent: «run deepcover…» | run in the terminal |
| --no-llm | skips LLM | skips LLM (no API calls) |
CLI
deepcover run
One-shot: extract, reason, and analyze in sequence. Equivalent to running the three stages below back to back.
| Flag | Description | Default |
|------|-------------|---------|
| --root <path> | Project root directory | Current directory |
| --module <path> | Module to analyze (relative to root) | — |
| --file <path> | Single file to analyze | — |
| --output <dir> | Artifact directory | .deepcover |
| --no-llm | Skip the reason stage (deterministic only) | false |
| --format <fmt> | Output: terminal, json, or score | terminal |
| --min-score <n> | Exit 1 if the composite score is below this | — |
| --bug-threshold <n> | Exit 1 if high-risk bugs >= n (requires --bugs) | — |
| --bugs | Enable bug analysis across all three stages | off |
deepcover extract
Extract the CodeModel and LLM prompts for Cursor-driven analysis.
| Flag | Description | Default |
|------|-------------|---------|
| --root <path> | Project root directory | Current directory |
| --module <path> | Module to analyze (relative to root) | — |
| --file <path> | Single file to analyze | — |
| --output <dir> | Artifact directory — see the note below | <root>/.deepcover |
| --bugs | Include 5th bug-finding prompt + write deterministic bug-signals.json | off |
--output is for tooling that reads the artifacts itself. analyze and
score always read <root>/.deepcover and have no counterpart flag, so
artifacts written elsewhere cannot be scored by DeepCover. (reason can be
pointed at a relocated model with --code-model, but it still writes and reads
the rest of <root>/.deepcover.) Omit --output for the normal
extract → reason → analyze flow.
Produces:
code-model.json— structured code model (classes, methods, branches, tests)prompts.json— LLM prompts for the Cursor agent (4, or 5 with--bugs)reasoner-output.json— empty template for the agent to fillbug-signals.json— (with--bugs) deterministic bug detector signals
deepcover reason
Run the LLM Reasoner via the configured provider and write reasoner-output.json (no scoring).
| Flag | Description | Default |
|------|-------------|---------|
| --root <path> | Project root directory | Current directory |
| --module <path> | Module to analyze (relative to root) | — |
| --file <path> | Single file to analyze | — |
| --code-model <file> | Existing CodeModel JSON (skips extract) | — |
| --output <file> | Output path | <root>/.deepcover/reasoner-output.json |
| --bugs | Include bug-finding (bugFindings) | off |
Staged CI example:
npx @anatolykhelmer/deep-cover extract --module src/orders
npx @anatolykhelmer/deep-cover reason --module src/orders --bugs
npx @anatolykhelmer/deep-cover score --min-score 60 --bugs--code-model .deepcover/code-model.json can replace --module on reason if extract already ran.
deepcover analyze
Score the artifacts already on disk in .deepcover/ and produce a report. Does not extract
or call an LLM — run extract (and reason, or fill reasoner-output.json yourself) first,
or use deepcover run for one-shot.
| Flag | Description | Default |
|------|-------------|---------|
| --root <path> | Project root directory | Current directory |
| --format <fmt> | Output: terminal, json, or score | terminal |
| --min-score <n> | Exit 1 if the composite score is below this | — |
| --bugs | Enable bug-finding (detectors + optional reasoner bugs) | off |
| --bug-threshold <n> | Exit 1 if high-risk bugs >= n (requires --bugs) | — |
deepcover score
Output only the composite score — alias for analyze --format score. Exits with code 1 if
below threshold. Same requirement as analyze: it reads artifacts already in .deepcover/.
| Flag | Description | Default |
|------|-------------|---------|
| --root <path> | Project root directory | Current directory |
| --min-score <n> | Minimum passing score (0-100) | 0 |
| --bugs | Enable bug-finding analysis | off |
| --bug-threshold <n> | Exit 1 if high-risk bugs >= n (requires --bugs) | — |
deepcover init
Install the agent skill (Cursor or Claude Code) so the agent can run extract → reason → analyze.
| Flag | Description | Default |
|------|-------------|---------|
| --agent <name> | Target agent: cursor or claude | cursor |
| --project | Install into ./.<agent>/skills/deepcover/ (commit and share) | off |
deepcover init --agent cursor # ~/.cursor/skills/deepcover/
deepcover init --agent cursor --project # ./.cursor/skills/deepcover/
deepcover init --agent claude # ~/.claude/skills/deepcover/
deepcover init --agent claude --project # ./.claude/skills/deepcover/Walkthroughs: Install for Cursor · Install for Claude Code.
Scoring Model
Four sub-scores combined with configurable weights:
| Sub-score | Weight | What it measures | |-----------|--------|-----------------| | Assertion Quality | 30% | Are assertions meaningful? (strong > medium > weak matchers, relative to method complexity) | | State Coverage | 30% | Are all meaningful domain states tested? (purely reasoner-driven — business scenarios, error conditions, edge cases) | | Mutation Resilience | 25% | Would tests catch subtle code changes? (branch coverage + assertion specificity) | | Criticality Weighting | 15% | Is the important code tested? (blast radius + business criticality) |
The LLM can adjust each sub-score by at most ±20%, scaled by its confidence. Without LLM (--no-llm), you get the deterministic base scores only.
Smart weight redistribution: When a sub-score doesn't apply (e.g. state coverage when the Reasoner has no discovered states), its weight is redistributed proportionally to the applicable sub-scores.
Per-method composite uses three factors: Istanbul line coverage baseline (up to 30 pts), state score (up to 35 pts), and assertion score (up to 35 pts). Methods with 100% Istanbul line coverage get the full baseline even without direct test assertions.
Per-method domain states come from two sources, unioned and deduplicated by state description:
- the static extractor, which derives states from enums, union types and parameter guards and attaches them to every method they affect. A static state counts as tested when any method it affects is covered.
- the Reasoner, whose
discoveredStatesare keyed by class and method. A reasoner state counts as tested when the Reasoner marked it tested and the resolver confirms the method is genuinely covered — a state inside a method no test reaches cannot have been exercised, whatever the model says.
Most services have no type-level states at all, so without the reasoner join a method's state score is 0 and its composite cannot exceed 65 no matter how well tested it is.
Matcher strength is one shared taxonomy (src/scorer/matchers.ts) used by every sub-score and by the per-method rollup:
| Strength | Matchers | Why |
|----------|----------|-----|
| Strong | toEqual, toStrictEqual, toBe, toMatchObject, toBeCloseTo, toThrow, toHaveBeenCalledWith, toHaveBeenLastCalledWith | Pins a concrete expected value or call shape |
| Medium | toContain, toMatch, toHaveLength, toHaveBeenCalledTimes | Pins a property of the value |
| Weak | toBeDefined, toBeTruthy, toBeFalsy, toBeNull | Pins only that something was produced |
The resolves, rejects and not modifiers are unwrapped, so await expect(p).resolves.toEqual(x) classifies as toEqual.
Transitive assertion credit: When an assertion targets a method call (e.g. expect(user.getName()).toBe('Alice')), the getName method gets credit even though the test targets createUser. This properly reflects how service-level tests transitively verify data-class methods.
Example Output
Without Jest runtime data:
DeepCover Report
════════════════
Composite Score: 47/100
Assertion Quality ██████░░░░ 62
State Coverage ████░░░░░░ 38
Mutation Resilience ████░░░░░░ 41
Criticality Weight █████░░░░░ 51
Per-method breakdown:
✅ OrderService.getOrders 72 (well-tested)
⚠️ OrderService.createOrder 23 (critical, weak tests)
❌ OrderService.deleteOrder 0 (no tests)
Top gaps:
#1 HIGH OrderService.deleteOrder — "has no test coverage"
#2 MED OrderService.createOrder — "only happy path tested"With Jest runtime data (run npm test -- --coverage first):
DeepCover Report (with Jest runtime data)
═══════════════════════════════════════════
Composite Score: 61/100
Assertion Quality ████████░░ 78
State Coverage █████░░░░░ 52
Mutation Resilience ██████░░░░ 63
Criticality Weight █████░░░░░ 54
Per-method breakdown:
✅ OrderService.getOrders 82 (92% lines, 4/5 branches)
⚠️ OrderService.createOrder 34 (41% lines, critical, weak tests)
❌ OrderService.deleteOrder 0 (no tests)
Top gaps:
#1 HIGH OrderService.deleteOrder — "has no test coverage"
#2 MED OrderService.createOrder — "partially covered (41% lines, 33% branches)"Configuration
Create deepcover.config.ts in your project root:
export default {
reasoner: {
provider: 'cursor', // 'cursor' | 'anthropic' | 'mock' | 'none'
// model: 'claude-sonnet-4-20250514', // when provider is anthropic
// apiKey: process.env.ANTHROPIC_API_KEY,
maxInfluence: 0.2, // cap LLM adjustment at ±20%
},
weights: {
assertionQuality: 0.30,
stateCoverage: 0.30,
mutationResilience: 0.25,
criticalityWeighting: 0.15,
},
thresholds: {
composite: 60,
},
};Also supports .js and .json config files.
The config is validated when it loads, and an invalid config stops the run with exit code 1 before any work happens. Unknown keys are errors — if DeepCover rejects a key you expect to work, check it against the fields above. Deleting the config file runs on the defaults shown here.
Anthropic: set reasoner.provider to 'anthropic' and provide a key. Full walkthrough: Install for Anthropic.
Using with Cursor
Setup: Install for Cursor (Claude Code: Install for Claude Code). After that, in the agent say "run deepcover on my webhooks module" — the skill handles the rest.
How it works
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ CLI extract │────▶│ Cursor Agent │────▶│ CLI analyze │
│ (Phase 1) │ │ (Phase 2) │ │ (Phase 3) │
│ CodeModel │ │ Reasoning │ │ Final Score │
└─────────────┘ └──────────────┘ └─────────────┘- Extract — CLI runs AST analysis, outputs CodeModel + prompts to
.deepcover/ - Reason — Cursor agent reads the CodeModel, performs semantic analysis (domain states, assertion quality, criticality, transitive coverage), writes
reasoner-output.json - Score — CLI combines deterministic metrics with agent insights, produces the final report
Manual pipeline (without the skill)
# Step 1: Extract
npx @anatolykhelmer/deep-cover extract \
--root . --module src/webhooks
# Step 2: Cursor agent fills .deepcover/reasoner-output.json
# Step 3: Score with insights
npx @anatolykhelmer/deep-cover analyze --root .Why Cursor over an API?
- Free — uses your existing Cursor subscription, no Anthropic/OpenAI key needed
- Context-aware — the agent already knows your codebase from the conversation
- Interactive — you can ask follow-up questions about the analysis
- Better insights — a warm agent with project context beats a cold API call
Project Structure
src/
├── extractor/ # Phase 1: AST analysis (ts-morph)
│ ├── class-analyzer # Classes, decorators, dependencies
│ ├── method-analyzer # Branches, external calls, async ops, line ranges
│ ├── test-analyzer # Assertions, mocks, test-to-source mapping
│ ├── dependency-graph# Dependency edges, transitive paths
│ └── index # Orchestrator → CodeModel
├── resolver/ # Phase 1.5: Coverage resolution
│ ├── types # IstanbulCoverageData, ResolvedCoverage, MethodCoverage
│ ├── istanbul-mapper # Maps Istanbul statement/branch data onto method line ranges
│ ├── runtime-matcher # Matches Jest runtime test names to extractor TestNodes
│ └── index # resolveCoverage() — merges static + Istanbul + runtime
├── reasoner/ # Phase 2: LLM semantic analysis
│ ├── prompts/ # Enriched prompt templates (4 jobs with cross-cutting context)
│ ├── providers/ # LLM adapters (mock, anthropic)
│ ├── types # Zod schemas for LLM output validation
│ └── index # Orchestrator → ReasonerOutput
├── scorer/ # Phase 3: Scoring engine
│ ├── assertion-quality, state-coverage, mutation-resilience, criticality
│ ├── composer # Weighted score composition
│ ├── gap-generator # Prioritized untested scenario list + partial coverage gaps
│ └── index # Orchestrator → ScoreResult
├── reporter/ # Jest custom reporter (runtime + Istanbul capture)
├── cli/ # Commander CLI (analyze, score, extract)
│ ├── commands/ # analyze, score, extract commands
│ ├── formatters/ # Terminal report formatter
│ └── config # Config file loader
└── types/ # Shared interfaces (CodeModel)
fixtures/
└── paradigms/ # Acceptance test fixtures (one per paradigm)
└── dont-test-getters-setters/ # Mini npm project with source, tests, expected.jsonJest Integration
DeepCover works without this section — the Extractor can score a module from static AST analysis alone. But configuring the Jest reporter and running tests with coverage is strongly recommended: it's the difference between DeepCover guessing which test covers which method and knowing, from real Istanbul line/branch data and real pass/fail results. This directly sharpens Assertion Quality, State Coverage, Mutation Resilience, and Criticality (see "How it works" below). Do this once per project and every analyze/score run after that benefits automatically.
Setup
Add the DeepCover reporter to your project's Jest config, and enable coverage — both are required, together:
{
"reporters": ["default", "@anatolykhelmer/deep-cover/reporter"],
"collectCoverage": true
}If you'd rather not turn on coverage by default, keep collectCoverage out of the config and always pass --coverage when running tests before a DeepCover analysis:
npm test -- --coverage
npx @anatolykhelmer/deep-cover run --root . --module src/your-moduleBoth pieces matter independently:
- Reporter only, no coverage →
jest-runtime.jsonis written (pass/fail, durations, assertion counts), butistanbul-coverage.jsonis silently skipped — no error, the file just won't exist and DeepCover falls back to heuristic line/branch estimates. - Coverage only, no reporter → Jest still writes
coverage/coverage-final.json, but DeepCover never sees runtime pass/fail data, and nothing gets copied into.deepcover/.
You want both configured together to get the full accuracy benefit.
What gets captured
After each test run, the reporter writes to .deepcover/:
| File | Contents |
|------|----------|
| jest-runtime.json | Per-test pass/fail status, duration, assertion counts |
| istanbul-coverage.json | Istanbul line/branch/function coverage (from coverage-final.json, requires collectCoverage: true) |
How it works
- Run tests —
npm test -- --coverageexecutes tests and produces both artifacts - Run DeepCover — the
analyze/scorecommands auto-detect.deepcover/jest-runtime.jsonandistanbul-coverage.json - Coverage Resolver merges the data:
- Istanbul data → ground-truth line/branch coverage per method (via line-range overlay)
- Runtime data → actual pass/fail, assertion counts, test durations
- Static extractor data → fallback when Jest data is unavailable
- Scorer uses the merged data for more accurate sub-scores:
- Assertion Quality filters out failed tests, detects runtime assertion count mismatches
- State Coverage scales by Istanbul branch coverage when available
- Mutation Resilience uses actual branch hit counts instead of heuristic estimates
- Criticality scales coverage proportionally by Istanbul line coverage
- Gap Generator reports "partially covered" methods (< 50% line/branch coverage)
Paradigm Testing
DeepCover includes acceptance tests that validate the quality of its analysis against known test-coverage paradigms. Each paradigm is a self-contained fixture project with expected qualitative outcomes.
Current paradigms
| Paradigm | What it validates |
|----------|------------------|
| dont-test-getters-setters | A data class with getters/setters and private helpers should score 100 when all methods are exercised through a consuming service's tests |
| bug-unhandled-error | A method with a try/catch should flag unhandled-error-path when only the happy path is tested |
| same-method-name-different-class | Two unrelated classes declaring a same-named method must be scored independently — the untested one must not inherit the other's test credit |
| compound-guard-operand | A guard built from four || operands that every test enters through the same one should flag untested-condition-operand, even though Istanbul reports the binary-expr fully covered |
| compound-guard-operand-covered | The same guard with a test for the second operand must report nothing — the false-positive guard for that detector |
Running paradigm tests
npm run test:paradigms # Fast — uses pre-computed Istanbul data (~1s)
npm run test:paradigms:e2e # Full — runs real Jest in fixture projects (~3s)Fast paradigm tests are included in the default npm test run. E2E tests run separately.
Adding a new paradigm
- Create
fixtures/paradigms/<paradigm-name>/with source, tests,jest.config.js,package.json - Run
npm testin the fixture to generatecoverage/coverage-final.json - Copy to
.deepcover/coverage-final.jsonand commit - Write
expected.jsonwith qualitative assertions - Both test levels pick it up automatically
Development
npm test # Unit + paradigm unit tests
npm run test:watch # Watch mode
npm run test:paradigms:e2e # E2E paradigm tests (real Jest runs)
npm run build # Compile TypeScriptSee CONTRIBUTING.md for setup and PR expectations.
Tech Stack
- TypeScript — strict mode
- ts-morph — AST analysis
- Commander — CLI framework
- Zod — LLM response validation
- Jest + ts-jest — testing
- @anthropic-ai/sdk — optional peer dependency for Anthropic provider
