--- name: verifying-change-coverage description: 'Verifies that the lines and branches a change actually touched are executed by tests, using LCOV or Cobertura diff coverage instead of whole-repo percentages, and escalates uncovered high-risk changes into a blocking finding. Use when a pull request needs a coverage gate that unrelated tests cannot satisfy, when total coverage looks healthy but the diff is untested, when wiring diff coverage into CI, or when someone claims a change is covered because the suite is green.' argument-hint: 'Base ref, the coverage artifact path (LCOV/Cobertura) or the command that produces it, and the diff or PR under review' user-invocable: true --- # Verifying Change Coverage Use this skill to answer one question honestly: **were the lines this change introduced actually executed by a test?** Repo-wide coverage cannot answer it. A repo at 84% can merge an untested payment path and stay at 84%, because a big denominator absorbs a small numerator. Diff coverage changes the denominator to *the lines you just wrote*, which is the only denominator that maps to the risk you just added. The distinction that runs through this skill: **configured is not verified.** A test runner in `package.json` proves the practice exists. Executed lines prove this change was tested. Report which of the two you measured. ## When to Use - a PR gate needs a coverage signal that cannot be gamed by unrelated tests - total coverage is stable but defects keep escaping in new code - diff coverage has to be wired into CI for the first time - a reviewer needs to know which specific new lines nothing executes - an agent wrote tests and someone needs to know whether they exercise the new code - coverage numbers are being quoted in a release decision and nobody knows what they measure ## Operating Principles - **Executed lines, not configured tools.** The whole point. Say plainly which one you have. - **No artifact, no verdict.** Without a coverage report you have static linkage - "a test file exists that imports this module" - which is weaker evidence and must be labelled as such. Never present it as coverage. - **Branches matter more than lines on changed code.** New code is where new conditionals live. A line hit once with the `if` never taken is half-tested. - **Uncovered is a question, not a verdict.** Some uncovered lines are fine: logging, type guards, unreachable defaults. The output is a list of uncovered changed lines with a judgement per group, not a percentage with a pass stamp. - **Coverage proves execution, never correctness.** An assertion-free test covers everything and verifies nothing. Pair this with `unslop-tests` - a diff-coverage gate is exactly the pressure that produces coverage theatre. - **Escalate by risk, not by count.** Three uncovered lines in auth outrank thirty in a formatter. - **The gate belongs on changed lines only.** Demanding a repo-wide floor on a legacy codebase produces a permanently red gate that gets disabled within a month. ## Workflow ### Phase 0: Get the two inputs **The changed lines.** ```bash git diff ...HEAD --unified=0 -- '***.ts' '***.tsx' ``` `--unified=0` gives added-line ranges with no context, which is what you want to intersect against coverage. Keep the file → line-numbers map; discard removed lines, generated files, and anything the contract excludes. **The coverage artifact.** Produce one if it does not exist: ```bash npx vitest run --coverage --coverage.reporter=lcov # → coverage/lcov.info npx jest --coverage --coverageReporters=lcov npx playwright test # + c8/istanbul instrumentation for E2E coverage ``` See `./resources/coverage-artifacts.md` for the formats, the merge problem across suites, and the monorepo path pitfalls. **If no artifact can be produced:** stop and say so. Report static linkage instead, labelled as static linkage, and state what it cannot tell you. Then make producing an artifact the first remediation item - this is the single highest-leverage change to a coverage story. ### Phase 1: Intersect For each changed source file, intersect its added-line numbers with the executed lines in the report. Produce, per file: - **changed lines** - added or modified executable lines (exclude blank lines, comments, pure type declarations, and `import` statements) - **covered** - changed lines the report marks as executed at least once - **uncovered** - changed lines with zero hits - **partial branches** - changed lines with branch data where at least one path was never taken Diff coverage = covered ÷ changed executable lines, per file and overall. Compute branch coverage on changed lines separately; do not average it into the line number. The exclusions matter and are the most common source of a wrong figure - see `./resources/coverage-artifacts.md` for what counts as an executable line in TypeScript once it has been compiled or transformed. ### Phase 2: Classify each uncovered group Never hand over a raw list. Group contiguous uncovered lines and judge each group: | Class | What it is | Action | | --- | --- | --- | | **Untested logic** | branches, calculations, validation, error paths | write the test - this is the finding | | **Untested integration** | code only reachable with a real dependency | integration test, or a documented exception | | **Defensive** | `default:` on an exhaustive switch, `never` guards, invariant throws | acceptable; record why | | **Instrumentation** | logging, metrics, tracing calls | acceptable | | **Unreachable** | dead code | delete it rather than test it | | **Hard to reach** | needs an exotic environment or a failure that cannot be simulated | exception with an owner and an expiry | The first two are findings. The rest are exceptions and must be recorded, not silently subtracted - see `./resources/exceptions-and-thresholds.md`. ### Phase 3: Set the verdict Combine diff coverage with the risk tags from `scoping-change-relevance`: | Condition | Verdict | | --- | --- | | Changed lines below threshold **and** the file carries an escalation tag (`auth`, `critical-path`, `db-migration`, `new-endpoint`) | **BLOCK** | | Changed lines below threshold on ordinary source | **WARN** | | Threshold met but changed branches materially uncovered | **WARN** | | Threshold met, uncovered lines all classified as acceptable exceptions | **OK** with the exceptions listed | | No coverage artifact available | **INFO** - static linkage only, artifact required | The escalation rule is the reason this beats a flat percentage: 70% on a logging module and 70% on session handling are not the same result, and one number cannot say so. ### Phase 4: Report Report per file, worst first: changed lines, diff coverage, branch coverage on changed lines, and the uncovered groups with their class and a one-line judgement. Point at exact line ranges - `src/pricing.ts:142-149` - so the fix needs no hunting. Finish with the two sentences that make the report honest: 1. What this measured: executed lines on the diff, from ``, produced by ``. 2. What it did not measure: whether the executing tests assert anything meaningful. ## Wiring It Into CI `./resources/ci-wiring.md` has working GitHub Actions and GitLab CI jobs, the base-ref fetch depth that trips everyone up on the first attempt, artifact merging across suites, and the sticky-PR-comment pattern. Three rules for the gate itself: - Gate on **changed lines**, never on a repo-wide floor. - Fail loudly or not at all. `|| true` on a coverage step is worse than no gate, because it reports green. - Ship the gate as a warning for one iteration, then turn it blocking. A gate introduced blocking on a legacy repo gets removed; one introduced as a warning gets fixed. ## Common Failure Modes - **Quoting repo coverage in a change conversation.** The single most common error, and the reason untested code merges into well-covered repos. - **Counting non-executable lines.** Imports, types, and interfaces inflate the denominator and hide real gaps. - **One suite's artifact, many suites' tests.** Unit-only LCOV makes integration-covered code look uncovered. Merge, or say which suites the number represents. - **Monorepo path mismatch.** Relative paths in the report that do not match the paths in the diff produce 0% coverage and a fake emergency. - **Chasing the number.** A gate met by asserting nothing. This is the predictable failure of any coverage gate; read `unslop-tests` before celebrating. - **Silent `/* istanbul ignore */`.** An exclusion with no reason and no owner is a waiver that skipped the register. - **Treating uncovered as automatically bad.** Uncovered logging lines are fine. Reporting them as findings trains people to ignore the report. ## Resource Map - `./resources/coverage-artifacts.md` - LCOV and Cobertura formats, generating them per runner, merging across suites, what counts as an executable line, monorepo path pitfalls - `./resources/exceptions-and-thresholds.md` - thresholds by profile, legitimate exception classes, how to record an exclusion with an owner and an expiry - `./resources/ci-wiring.md` - GitHub Actions and GitLab CI jobs, base-ref fetching, artifact merge, sticky PR comment, warn-then-block rollout ## Related Skills - `scoping-change-relevance` - supplies the risk tags that turn a below-threshold result into BLOCK rather than WARN - `unslop-tests` - the necessary counterweight: coverage proves execution, this proves the tests assert something - `writing-unit-tests` - when the finding is untested logic and tests have to be written - `deriving-a-quality-contract` - where the diff-coverage threshold for this project is set - `governing-quality-waivers` - when an uncovered area needs a recorded, dated exception - `analyzing-quality-metrics` - for coverage's caveats as a metric and what to report alongside it ## Definition of Done This skill is complete when: - the changed-line set and the coverage artifact are both identified, with the suites the artifact represents - diff coverage is computed on executable changed lines only, per file and overall - branch coverage on changed lines is reported separately from line coverage - every uncovered group is classified and judged, not listed raw - the verdict accounts for the risk tags of the files involved, not only the percentage - exceptions are recorded with a reason and an owner, never silently excluded - the report states what was measured and, explicitly, that coverage does not prove correctness