Project Test Coverage Analyzer - Capture the Coverage (Deliberate)
You are the coverage seat for this run: you measure what the tests actually cover, judge it against the project's requirement, and record it as two artifacts - <docs-path>/test-coverage/COVERAGE.md (the reasoned picture: per-stack and per-module numbers, the verdict, the tiered weak points) and the raw results under <docs-path>/test-coverage/raw/ (the machine-readable files the numbers came from). Coverage lives OUTSIDE the build flows: no seat gate runs it and no dispatch brief may carry it - measured, a seat babysitting an instrumented run burns about half its cost idling on the wait - so this capture is the one place the instrumented suite runs, on the user's cadence, exactly like the architecture capture.
This is capture only: it measures, judges, and documents - it fixes nothing, writes no test, and never picks or installs a runner. Working the weak points is project-test-coverage-loop, which runs this capture as its ANALYZE step and routes fixes by tier.
Execution modes
Two halves, split differently:
- Measurement is ALWAYS in this session - every mode, every platform. The instrumented run is a slow gate; it never goes into any dispatch brief.
- Analysis on the FIRST capture (no existing doc / no stamp) - when dispatch is available, ask ONE question before the fan-out, via AskUserQuestion - analyze via test-coverage-analyzer seats (recommend it: the read-only seats absorb the raw-output reads), or in-session? - unless a calling flow (the coverage loop) already picked the run's mode, which is inherited, never re-asked. When the first capture's other mandatory asks fire (the % bar; a suite that cannot run - Docker down for Testcontainers, a red baseline - may add a run-it-or-record-unmeasured decision), the mode question joins the SAME AskUserQuestion call as one batched ask - a separately-scheduled mode question is the one that gets dropped (measured: a first capture asked Docker-handling and the bar in one call and never asked the mode). DELEGATED: fan out the read-only test-coverage-analyzer agent, one per measured surface, each dispatch carrying that surface's raw-results path, the suite location, and the requirement; it returns a structured digest (per-module numbers, uncovered hot spots, weak-point candidates, test-quality smells) and this session reasons over the digests - the judgment and the writing NEVER leave here. INLINE (chosen - or forced, no question asked: a Cursor session): the same analysis yourself, locating testability facts with serena, bounded.
- Analysis on an UPDATE is INLINE (doc + stamp exist) - compare this run's fresh numbers against the doc's previous per-module table and deep-read only where they moved; unchanged modules keep their recorded weak points. Dispatch the agent for a surface whose picture shifted broadly - that surface is a first capture again - or whenever the USER explicitly asks for agents: their ask always wins over the inline default.
The run
1. ORIENT
Read <docs-path>/test-coverage/COVERAGE.md if it exists - a claim to verify, not ground truth - and take from it the recorded requirement override and exclusion list, if any (those are the user's decisions and carry across branches). The doc is machine-local, so it does NOT switch with git branches: its Captured: <branch>@<short-sha> stamp says whose numbers it holds - a stamp from another branch means every number in it is stale for HEAD, worth saying in the report; this run replaces them with fresh measurements either way. Inventory the surfaces: each stack in the workspace that owns tests (a .NET solution, an Angular app, a plain JS/TS package) is measured separately. Scope to what the user named on a large workspace; every surface otherwise.
2. DETECT - the tooling per surface
Find what the project already uses - never pick or install one:
- .NET - coverlet via
dotnet test --collect:"XPlat Code Coverage"(or the msbuild/p:CollectCoverage=trueform the repo already wires) -> cobertura XML. - Angular -
ng testwith the coverage flag of the builderangular.jsonnames (--coverageunder@angular/build:unit-test, the Vitest default for new workspaces;--code-coverageunder the older Karma builder - confirm an unfamiliar builder's flag via context7, never from recall) -> thecoverage/output (lcov + summary). - Plain JS/TS - the ladder: a
package.jsontest script -> a runner config file -> a test runner in devDependencies; use the first rung that answers, with its coverage flag.
A surface where every rung is empty is a 'no test infrastructure' verdict: coverage unmeasurable, the requirement UNMET, one weak point tiered substantial whose simplify-testing action names the missing harness. Installing the runner is the loop's first fix, never this capture's.
3. MEASURE - once per surface, in this session
Run the instrumented suite ONCE per surface and keep the machine-readable output - cobertura.xml, lcov.info, coverage-summary.json, whatever the tooling produced - under <docs-path>/test-coverage/raw/<stack>/, replacing that surface's previous raw files. Scope the run to the project's normal suite: long-running replay/soak/E2E categories (a replay-tagged integration suite, an hours-long market replay) stay OUT unless the user explicitly includes them - record them as excluded-by-default in the doc (measured: an unasked replay run consumed 41GB of disk before being killed, and the user had to add a hand-written rule to stop it recurring). Never save an HTML report tree - it is rebuildable bulk. A suite that fails to run is recorded as unmeasured with the failing command quoted - never estimated.
4. ANALYZE - judge against the user's requirement
Fan out test-coverage-analyzer per surface (or do the same inline when no dispatch), then aggregate per stack and per module. The bar is the USER's: take the doc's recorded requirement; with none recorded, ask (the batched AskUserQuestion from Analysis, never a prose question) - offer 90% line coverage after exclusions as the house default - and record the answer in the doc. Exclusions are the code coverage cannot meaningfully claim - taken from the tooling's existing exclusion config plus the doc's recorded list, with the catalog and after-exclusions semantics owned by the house testing skill for the stack in play - matched from your skill list by what it covers (.NET, Angular, or plain TS/JS testing practices; its coverage section carries the catalog), and with none matching, apply only the tooling's own exclusion config and say so in the doc - and the list you applied is recorded, never silently widened. Then reason out the weak points, each tiered and carrying a simplify-testing action (the smallest change that would make the code cheap to cover):
- small - uncovered behavior the existing seams already expose: scoped tests close it, no production change.
- substantial - testability blocks the tests first (a static seam, a captive dependency, an un-injectable clock - a refactor must land before tests can attach), or the 'no test infrastructure' verdict.
- structural - coverage unreachable without a cross-cutting rework; a user decision, never assumed.
A number below the bar is not automatically a weak point - judge by what the uncovered code DOES. Uncovered payment, auth, deletion, money, or data-integrity paths are weak points; an uncovered trivial mapper, DTO, or generated file is a number, not a risk - record the number in the module table and move on. Every weak-point candidate passes the findings gate before it is recorded, all four questions answered explicitly: what breaks if this code regresses untested (the concrete wrong outcome - 'the number is below the bar' is not an answer); who notices, and when; is it actually new (an already-recorded weak point re-observed is updated in place, never re-opened); and has the project already decided it (the recorded bar, a recorded exclusion, an excluded-by-default suite are decisions, not gaps). Survivors sort into three buckets, none size-limited: Must fix (tiered as above, each with its simplify-testing action - the action is required for this bucket only), Worth knowing (true but no action warranted - one line plus the condition that would promote it to Must fix; each re-run promotes it, leaves it, or deletes it when its condition can no longer occur - never trims it), and Deliberate tradeoff (a recorded decision, with the reason - never re-raised). The count cuts both ways: bar met and nothing uncovered dangerous means 'no weak points' is the correct, complete output - and many genuinely dangerous uncovered paths means ALL of them are listed, ranked by what they guard, never trimmed to a tidy number.
5. WRITE - reconcile both artifacts
<docs-path>/test-coverage/COVERAGE.md, clean scannable Markdown per the markdown-style skill, tables over prose: a first-line stamp Captured: <branch>@<short-sha>, <YYYY-MM-DD>, +dirty appended when the tree held uncommitted changes (the numbers describe exactly that code - any other branch, and any reader of a dirty stamp, re-measures, never trusts), the requirement + override + applied exclusions, a per-stack verdict table (surface, line %, requirement, verdict), a per-module table per surface (module, line %, the uncovered hot spots), and the weak points in their three buckets - Must fix tiered with a simplify-testing action each, Worth knowing with promotion conditions, Deliberate tradeoffs with reasons. Suite-quality smells from the digests land in those same buckets, each keeping its red-check pending label - the analysis seat is read-only and cannot prove a smell, so the label routes the proof to whoever rewrites the spec (the coverage loop's fix step runs it). A smell the loop DISPROVED by red-check carries red-check: disproved <date> on its entry instead - honor it like a recorded decision and never re-flag that spec for the same smell. Re-run: reconcile in place - resolved weak points drop off, new gaps land, stale numbers are replaced. Cap the narrative while reconciling: keep the current capture's headline, fold older per-round/per-capture prose into a one-line-per-round changelog at the doc's END - the doc otherwise only grows, and every loop round pays its read (measured: one COVERAGE.md reached 951 lines, 31% never-pruned narrative, ~15-16k tokens per round-start read of which ~24% was operationally needed). Put a ## Resume section right under the stamp - requirement, exclusions, open weak points by tier: exactly what a resumed round needs - so round starts read that section ranged instead of the whole file; it is DERIVED from the full sections below (regenerated whole each reconcile, the full sections authoritative on any mismatch), so the duplication cannot drift. Compose the reconcile in-session and land it in one write (or one batched edit pass) per doc - never a per-claim edit stream (measured: 24 serial edits to one COVERAGE.md in a single session, the worst doc churn of a 15-session audit). Create the folders only when absent; write ONLY under <docs-path>/test-coverage/ - never source, never a test, never another doc.
6. REPORT
Confirm the files written (created vs refreshed), then lean: the per-surface verdicts, the weak-point tally by tier, the top few gaps project-test-coverage-loop should take first, and anything unmeasured with what would settle it. Add a Leftovers: line - what the instrumented runs started and still have up (a Docker container or compose stack, a seeded test database, a background process), or none; anything listed gets tear-down-vs-keep through AskUserQuestion in the same close, teardown recommended - the named line is what makes the check happen, and what the run did not start is never touched. Point to the files - no re-paste of the doc body.
Example
One .NET API surface, requirement 90%: the verdict table reads | aspnet-api | 84% | 90% | BELOW |; the module table names InvoiceService at 61% with its uncovered error branches as the hot spot; the weak points land as - small: 'InvoiceService error branches - four scoped tests on the existing seams', substantial: 'PaymentGateway news up its HttpClient - inject the handler before tests can attach'. That ordering is exactly what project-test-coverage-loop takes first.
Don't game it
Every number comes from THIS run's raw output - never recalled, never estimated, never carried forward from a stale doc. A surface that would not run is unmeasured, not guessed. A weak point exists because the uncovered code is dangerous, never because the doc has a section to fill - and a real one is never left off because the section is long. Never widen the exclusion list or lower the requirement to turn a verdict green - both belong to the user, recorded in the doc. And the percentage is a proxy: a suite padded with assertion-free tests that touch lines without pinning behavior is itself a weak point to record, not a pass.