Use when a team can write only a handful of new tests and must pick which uncovered code matters most, or when some modules lose coverage while the total looks fine. Ranks uncovered branches in the diff by complexity, churn and test cost, and tracks per-file coverage drift.
78%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
100% coverage isn't the goal; risk-adjusted coverage is. A 50-line uncovered helper that hasn't been changed in 4 years is lower priority than a 10-line uncovered branch on an access-control function that gets edited every week.
This skill builds a ranked list of uncovered branches, scoring each by:
The output is 5 - 10 specific test targets the team can take in one PR - not a 200-line "everything uncovered" dump.
coverage-diff-reporter
for the "what regressed" + "what to add" combo.Two required + two optional inputs:
| Input | Required | Source |
|---|---|---|
| Coverage report | ✓ | LCOV / Cobertura / Jest JSON / coverage.py JSON / JaCoCo XML |
| Source tree | ✓ | Project root checkout |
git log history | opt | For churn weighting (Step 3). |
| PR diff | opt | For PR-touch weighting (Step 4). |
The shape (after parsing per upstream parser):
{
'path': 'src/checkout/cart.ts',
'uncovered_branches': [
{ 'line': 42, 'condition': 'if (item.stock < quantity)', 'arms_uncovered': 1, 'arms_total': 2 },
{ 'line': 78, 'condition': 'switch (status)', 'arms_uncovered': 3, 'arms_total': 5 },
],
'uncovered_lines': [33, 34, 35, 92, 93],
'covered_pct': 65.4,
}Per language tool:
BRDA:<line>,<block>,<branch>,<taken> records;
taken == '-' is uncovered. Use lcov-analysis.<line branch="true" condition-coverage="50% (1/2)"/>;
use the Cobertura reference in lcov-analysis.b field: arrays of arm hit counts; uncovered = 0.
Use js-unit-tests (qa-unit-tests-js).<counter type="BRANCH" missed="N" covered="M"/>
per method, then read source lines for context.
Use jacoco-analysis.def risk_weight(file_path, branch):
cyclomatic = mccabe_complexity(file_path, branch.line)
churn = git_churn(file_path, days=90)
return (
normalize(cyclomatic, 1, 20) * 0.5 # 50% complexity weight
+ normalize(churn, 0, 50) * 0.5 # 50% churn weight
)Per cyclomatic: McCabe's formula is M = E - N + 2
(edges − nodes + 2). The threshold convention:
"1 - 10: Simple procedure, little risk" "11 - 20: More complex, moderate risk" "21 - 50: Complex, high risk" ">50: Untestable code, very high risk" (cyclomatic)
Compute via per-language tools:
# Python
radon cc src/ -a
# JavaScript / TypeScript
npx complexity-report src/ -f json
# Java
# JaCoCo's COMPLEXITY counter (already in jacoco.xml per jacoco-analysis)Function with cyclomatic >10 + uncovered branch = strong target.
# Commits in the last 90 days touching this file
git log --since='90 days ago' --format= -- src/checkout/cart.ts | wc -lHigh-churn files are where bugs accumulate; uncovered branches in high-churn files are the highest-priority targets.
Per cyclomatic: "reducing the cyclomatic complexity of code is not proven to reduce the number of errors or bugs in that code." Use complexity as a risk indicator, not a goal.
If the PR diff (Step 1 optional input) changed a file, boost its risk weight by 1.5×. Newly-edited code is strictly higher regression risk than untouched code.
def pr_boost(file_path, pr_changed_files):
return 1.5 if file_path in pr_changed_files else 1.0
final_risk = risk_weight(...) * pr_boost(...)Per test-pyramid, the canonical layers (Cohn 2009):
| Layer | Cost (relative) | Recommend for |
|---|---|---|
| Unit | 1× | Pure functions, business-logic branches. |
| Service / API | 3× | Cross-module integration, controllers, repos. |
| UI / E2E | 10× | User flows, browser interactions. |
Per test-pyramid: "you should have many more low-level UnitTests than high level BroadStackTests running through a GUI ... UI tests are brittle, expensive to write, and time consuming to run."
The targeter classifies each candidate branch by file path heuristic:
def layer(path):
if any(s in path for s in ['/components/', '/views/', '/pages/', '/e2e/']):
return 'ui-or-e2e'
if any(s in path for s in ['/api/', '/routes/', '/controllers/', '/services/']):
return 'service'
return 'unit'COST_BY_LAYER = {'unit': 1, 'service': 3, 'ui-or-e2e': 10}
for candidate in candidates:
candidate['score'] = candidate['risk'] / COST_BY_LAYER[candidate['layer']]
candidates.sort(key=lambda c: c['score'], reverse=True)Score = risk / cost. A unit-layer branch with risk 0.6 (score 0.60) beats a UI-layer branch with risk 0.9 (score 0.09).
Take the top 5 - 10 candidates (configurable). More than 10 is a to-do list, not a recommendation.
## Uncovered branches - recommended targets (top 7)
This PR adds 12 uncovered branches in code touched by the PR. The
prioritized list focuses test budget on high-risk × low-cost
targets per the [test pyramid][tp].
| # | File | Line | Branch | Layer | Risk | Recommendation |
|---|-------------------------------------|-----:|-------------------------------------|-------|-----:|----------------|
| 1 | `src/checkout/promo.ts` | 42 | `if (codeIsValid && !expired)` | unit | 0.91 | Add a unit test for the expired-code path. |
| 2 | `src/checkout/promo.ts` | 78 | `switch (codeType)` arm `'BOGO'` | unit | 0.85 | Add a unit test per arm; BOGO arm uncovered. |
| 3 | `src/api/shipping.ts` | 134 | `if (carrier === 'ups')` | service | 0.82 | Add an integration test for the UPS path. |
| 4 | `src/checkout/cart.ts` | 21 | `if (item.stock < quantity)` | unit | 0.78 | Add a unit test for stock-shortfall edge case. |
| 5 | `src/api/shipping.ts` | 200 | `try / catch` (catch arm) | service | 0.65 | Add a test that triggers the failure path. |
| 6 | `src/components/CheckoutModal.tsx` | 88 | `if (showPromoBanner)` | unit | 0.55 | Add a snapshot/component test for the banner-hidden case. |
| 7 | `src/api/orders.ts` | 156 | `if (status === 'fulfilled')` | service | 0.48 | Add an integration test for the fulfilled-status path. |
### Skipped (low score)
5 candidates skipped - see `coverage-targets.json` for the full
list. Highest-risk skipped: UI-layer test that would catch a 0.9
risk branch but cost 10× a unit test (score 0.09).
### Approach
For each target above, the recommended test shape:
- **Unit (5)**: Add a per-branch test in the existing `*.spec.ts`;
use `expect.fail` if no path triggers the branch yet.
- **Service (2)**: Use `testcontainers`
to bring up a real Postgres + a stubbed carrier API
(`wiremock-stubs`); assert the branch outcome.This skill is advisory, not gating. Post the recommendation as
a PR comment via coverage-diff-reporter's sticky-comment mechanism
(see coverage-diff-reporter
Step 7) but don't fail the build on it:
- name: Generate coverage targets
if: github.event_name == 'pull_request'
run: |
python scripts/coverage_targets.py \
--coverage current.json \
--diff <(git diff --name-only origin/${{ github.base_ref }}...HEAD) \
--top 7 \
> targets.md
- name: Post sticky comment
uses: marocchino/sticky-pull-request-comment@v2
with:
header: coverage-targets
path: targets.mdAggregate coverage hides per-file decay: a repo can sit at 82% overall while the auth module slides from 95% to 60% across 30 PRs, none of which crossed a gate threshold. The debt ledger is the weekly, informational (never gating) companion to the per-PR targeting above - it walks a rolling window of persisted coverage history and flags files on three axes:
| Axis | Signal |
|---|---|
| Falling | line% (or branch%) dropped >M pp (default 5) from its window peak. |
| Stale | Coverage flat (<1pp variance) while churn is high (≥10 commits / 90 days). |
| Orphan | Lost its last covering test (every covering test was deleted). |
Mechanics:
coverage-history/<sha>-<timestamp>.json
({sha, timestamp, files: [{path, line_pct, branch_pct}]});
~90 days retention is enough to catch quarterly drift.peak = max(pct for _, _, pct in series) # over the last ~30 runs
drop = peak - series[-1][2]
if drop >= 5.0: # FALL_THRESHOLD_PP
flag('falling', path, peak, drop)git log --since='90 days ago' -- <path> churn ≥10 (checkout with
fetch-depth: 0 or every file looks low-churn).regression-suite-selector in qa-test-impact-analysis): a file
whose covering tests were all deleted is at 0% now, however fine
the aggregate looks.Each ledger row then feeds the targeting workflow above: the falling / orphan file becomes the Step 1 input, and Steps 2-7 turn it into specific file:line test targets.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Recommending coverage targets without considering test layer | Suggests UI tests for branches that should be unit-tested; cost ignored. | Score = risk / cost (Step 6). |
| Listing every uncovered branch | 200-row list; team ignores. | Top 5 - 10 (Step 6). |
| Pure-coverage chase (every uncovered line is a target) | Hits 100% by writing low-value tests; signal-to-noise collapses. | Risk-weight by complexity + churn (Step 3). |
| Treating cyclomatic complexity as a goal | Per cyclomatic: reducing complexity isn't proven to reduce defects. | Use complexity as a risk indicator only (Step 3). |
| Ignoring PR-changed files | A new function added in the PR with 0 coverage isn't surfaced. | PR-touch boost (Step 4). |
Recommending tests for code marked # pragma: no cover / istanbul ignore | The team explicitly excluded that code. | Skip files / lines with explicit ignores. |
| Blocking the build on the recommendation | Recommendation is opinion; gating turns it into bureaucracy. | Advisory comment only (Step 8); the actual gate is lcov-analysis (LCOV or Cobertura). |
services/ directory might
contain pure functions (unit) and DB-touching code (integration).
Tune the heuristic per project.git log. Shallow CI clones (default
actions/checkout with no fetch-depth) lack the history;
fetch-depth: 0 is needed.coverage-targets.snoozed.json checked into the repo.M = E - N + 2, threshold convention (1 - 10 / 11 - 20 / 21 - 50 /
50), the warning that "reducing the cyclomatic complexity of code is not proven to reduce the number of errors or bugs".
lcov-analysis (LCOV + Cobertura),
jacoco-analysis,
coverage-py-analysis, and
js-unit-tests (qa-unit-tests-js) for Jest/Vitest -
upstream parsers this skill consumes.coverage-diff-reporter -
sibling reporter; the diff identifies what regressed, the
targeter identifies what to add next.