codecharter req coverage and the MCP
req_coverage tool report one
coverage model from both directions of traceability, computed by a single
shared aggregator so the two surfaces can never disagree on a number:
- Requirement → code: does each requirement's evidence currently hold?
- Code → requirement: which methods carry no
[Satisfies]justification at all, and why?
Both halves reuse the evaluators and indexes Requirements and Requirements over MCP already describe — no criterion, verdict, or justification walk is re-implemented for this report.
What the number is for, and who reads it
Coverage answers two questions that, in day-to-day project work, are asked by different people. Project leads and the customer ask about the state of the commitments: does every requirement still hold, and where does it not. The development team asks the other direction: which part of the code is bound to a commitment at all, and which part sits there unaccounted for.
Answering both from one run is the actual point. A traceability matrix that only knows the first direction always looks good, as long as you only look at the anchored places. Only the reverse direction shows how much code never appears at all.
The command is a report first. It does not fail your build until you turn that on explicitly, see The tolerated-legacy gate below.
Reading an example header
req coverage — scope: solution
requirements: 12, criteria: 27, anchors: 9 (anchored requirements: 8)
code: 340 methods — justified 41.18% (140), accounted 46.47% (158 incl. 18 infrastructure), unjustified 182.
Of twelve requirements, eight carry an anchor at all, so four are not yet bound anywhere in the code. Of 340 methods, 140 are reachable from a requirement through a justification walk, another 18 are deliberately classified as infrastructure, and 182 stand there without a justification.
What these numbers do not say is that those 140 methods do the right thing. They say someone entered a binding for them that a reviewer saw in a diff. A low number at the start is normal and is not a defect of the code, it is the honest starting point.
Requirement → code: rollups
Every requirement's criteria are evaluated once, through the same pin
evaluator req check --with-code and req_verify use, then rolled up to a
single worst-of verdict by a fixed severity order:
| Rank | Verdict | Why it ranks there |
|---|---|---|
| 5 (worst) | violated |
Proven broken against the current code. |
| 4 | stale |
A decayed positive claim: it once held, and the pinned evidence has since changed — actively misleading in an old traceability matrix, so it outranks an honest non-answer. |
| 3 | unknown |
Evaluation could not decide. An honest non-answer, not evidence of health or decay. |
| 2 | open |
A declared gap — the least alarming non-green outcome, since it is deliberate rather than discovered. |
| 1 (best) | holds |
Every criterion holds. |
Never vacuously green. A requirement with zero criteria does not roll up
to holds — it rolls up to open with an unbound reason of
no-criteria. The same applies to zero anchors (no-anchors) and anchors
that all failed to resolve (anchors-unresolved). This is the same
"nothing ever reports Holds without having checked" invariant the criterion
evaluators already apply, extended to the requirement level.
The frontmatter status key is never an input to the rollup — it is
reported beside it. A requirement whose declared status disagrees with its
rollup (status: implemented while the rollup is not holds, or
status: open while the rollup is holds) is surfaced as a status
divergence: reported for visibility, never used to flip the rollup.
Code → requirement: classified methods
Every method in the analyzed code model falls into exactly one of three disjoint buckets, reported as two headline percentages that are never merged into one ambiguous number:
justified— reached by at least one non-infrastructure requirement's[Satisfies]justification walk.infrastructure-declared— see Infrastructure exemption.- unjustified, itself split into:
dead-candidate— no caller is visible to the justification walk, and the method is not a dispatch target (no interface implementation, override, or implementing method reaches it either). This is never proven unused — only not reachable through this walk's visible edges (no reflection, no delegate invocation, no dynamic dispatch is followed). The report always says "candidate", never "dead" on its own.undocumented-requirement-candidate— has visible callers, but no anchor's justification walk reaches it. Carries anearestAnchoredNeighbour: the closest justified method found by a bounded forward search over the candidate's own outgoing calls (never callers, capped at four hops), together with the requirement ids that justify it —nullwhen the cap is hit, never a guess. This hint is exactly whatreq_draftseeds a new requirement from.
justified_percent = justified / total_methods;
accounted_percent = (justified + infrastructure_declared) / total_methods.
If the corpus has no [Satisfies] anchors at all, both percentages report
0 with an explicit notice, and no classified list is produced — a repo
that has not adopted anchoring yet must not receive ten thousand "dead
candidates" on its first run.
Infrastructure exemption
A requirement with kind: infrastructure exempts exactly the methods its
[Satisfies] anchors name directly — a method anchor exempts that
method, a type anchor exempts that type's methods. This exemption is
declared, never guessed: there is no heuristic that infers "this looks
like glue code" on its own, so mislabeling ordinary business logic as
infrastructure is always a visible attribute change in a diff, never a
silent classifier decision.
It is also non-transitive by design. Anchoring Program or a
dependency-injection registration class does not exempt everything those
methods call — only the anchored declarations themselves. A transitive
exemption would let one attribute on your composition root exempt half the
solution, which is exactly the kind of mislabeling this report exists to
keep visible. When a non-infrastructure requirement's justification walk
reaches a method also directly exempted as infrastructure, the report emits
an info-level req-infrastructure-overreach notice — a signal the
infrastructure label may be misapplied to code that business logic actually
depends on.
Diff scope
--git-ref/git_ref (or raw diff text over MCP) narrows the classified
method list and the rollup list to what the diff touches. Every total and
every percentage — including justified_percent and accounted_percent —
stays corpus-wide; only the two lists shrink, with a scope: diff notice
naming what happened. This mirrors graph_hubs's own scope field: a
diff-scoped view never silently narrows what a summary number claims to
cover.
The tolerated-legacy gate
Coverage reports a number from the moment you adopt it, even on a large,
partially-anchored codebase — but that number should not have to be 100%
before the report is useful, and it should not silently regress either.
req coverage therefore ships a one-way ratchet, the same pattern the
findings baseline uses:
codecharter req coverage --write-baselinerecords every currently unjustified method's identity (Roslyn declaration id, or a graph-key fallback when the compiler produced none — a rename invalidates a fallback entry by design) to.codecharter/req/coverage-baseline.json.- From then on, a baseline-listed method renders
tolerated; a new unjustified method — one not in the baseline — rendersnew. - The gate itself (non-zero exit on any
newmethod) is opt-in, behindreq.coverage.gate.enabled: truein.codecharter/config.yml(defaultfalse). The mechanics and the failing-gate exit code exist and are tested regardless of whether any repository has opted in.
Infrastructure-declared methods are exempt by design and never count as
baseline debt — only dead-candidate and undocumented-requirement-candidate
methods are tracked. The baseline never widens itself: only
--write-baseline writes it, and repeated writes of the same set are
byte-identical (sorted, de-duplicated, no timestamps).
Report first, gate later — a Goodhart note
Attributes are diff-visible and review-carried; the metric is not a target until the gate exists.
[Satisfies] anchors land in the pull request diff and get reviewed there
like any other code change. Coverage turns that into a number that is
visible and, once you switch the gate on, can no longer regress silently.
Optimizing straight for the percentage — for example, anchoring requirements
to code indiscriminately just to raise justified_percent — defeats the
point: the number is only as trustworthy as the anchors a human actually
reviewed.