Skip to content

Spec coverage: a change maps to the specs that cover it

Every spec under docs/reference/specs/ already says what code it covers: the paths in its - **Code**: and - **Tests**: header lines, which specs:check requires to exist. specs:coverage turns those headers into a map from a change to the specs that must still be true after it — the list the review agent reads for its spec contradiction check (agent-review.md item 14) — and names the changed source paths no spec claims, so a behavior can be shipped without a contract only in the open. There is no separate covers: field to rot: the headers a reader already follows are the coverage.

Behavior

  1. A spec's coverage is its headers. The paths a spec covers are the backtick spans on its - **Code**: and - **Tests**: lines — bare or inside a markdown link — that carry a directory and no wildcard (src/core/x.ts, deploy/cloudflare-bot/; not Actor, conversations.info, /runs, src/core/trace/*.test.ts). The rule is the one specs:check applies to the same lines, kept as its own copy under src/ because nothing there may import from scripts/ (the bot image copies src/ alone). No other line contributes: a proof reference in the criteria table names a test, not coverage.
  2. A header path covers itself and everything beneath it, trailing slash or not. src/core/authz/ and src/core/authz both cover src/core/authz/policy.ts; a file path covers exactly itself, since a file has no descendants. Coverage is by path segment: src/core/authz never covers src/core/authzExtra.ts. The rule needs no look at the disk, so the same paths give the same answer in CI, in a resident and in a sandbox.
  3. Touched specs, with the reason. For a set of changed paths, the result lists every spec at least one path falls under — in the specs directory's order, each once — with the header paths that matched, so a reader can see why a spec is on the list.
  4. Uncovered means a source path no spec claims. A changed path is a source path when it is a code file (.ts, .mts, .cts, .js, .mjs, .cjs, .vue) under src/, web/src/ or deploy/ and is not a test (.test./.spec.), a snapshot, a fixture under a testing/ directory, a declaration (.d.ts), or config, docs or anything outside those roots. Only such paths appear in uncovered: a new test file, a doc or a lockfile without a spec is not a gap.
  5. One command, three inputs, two shapes, one gate. npm run specs:coverage takes the changed paths from --changed <a>...<b> (git diff --name-only over that range — the review agent's form, origin/<base>...HEAD), from --paths p1 p2 …, or from stdin one per line. It prints the touched specs one per line (<spec> ← <matching header paths>) and then the uncovered list, or the same as { touched, uncovered } with --json. It exits 0 whatever it finds; --require exits 1 when uncovered is non-empty. A malformed invocation — --changed with no range or with a flag where the range should be, an unknown flag, or nothing on stdin at a terminal (it never blocks waiting for input) — prints the usage line and exits 1. The gate is warn-only until every source path has a covering spec, then --require joins check:consistency — warn first, then error, the way the hygiene ratchet landed.

Validation criteria

CriterionProof
Header paths are read from the Code and Tests lines only, bare or linked, with line numbers; identifiers, dotted symbols, routes and globs are not paths; table rows and other headers contribute nothing (item 1)[unit] src/docs/specCoverage.test.ts::parseHeaderPaths::*
A source path is a code file under src/, web/src/ or deploy/; tests, snapshots, testing/ fixtures, declarations, config, docs and paths outside the roots are not (item 4)[unit] src/docs/specCoverage.test.ts::isSourcePath::*
Equality touches the spec and names the matching header; a header path covers its subtree with or without a trailing slash and never a prefix-sharing sibling (items 2–3)[unit] src/docs/specCoverage.test.ts::coveringSpecs::a changed path equal to a header path touches that spec, and says which header path matched, ::a header path covers everything beneath it, with or without its trailing slash, ::a header path never covers a sibling that merely shares its prefix
One path touches several specs; each spec is listed once, in order, with only the headers that matched; nothing in → nothing out (item 3)[unit] ::one changed path can touch several specs; a spec is listed once, in spec order, with only the header paths that matched, ::no changed paths → nothing touched, nothing uncovered
uncovered holds only source paths with no covering spec (item 4)[unit] ::uncovered lists only source paths with no covering spec — a test, a doc or a config file without a spec is not a finding
The command over a real range lists the touched specs and exits 0 with an uncovered path; --require exits 1 on the same input; --json is the machine shape (item 5)[agent] npm run --silent specs:coverage -- --paths src/core/costs.ts src/nothing/here.ts → one costs.md ← src/core/costs.ts line, then 1 changed source path(s) with no covering spec naming src/nothing/here.ts, exit 0; add --require → exit 1; add --json{ "touched": [...], "uncovered": ["src/nothing/here.ts"] }. On a branch: npm run --silent specs:coverage -- --changed origin/main...HEAD lists the specs whose headers the diff falls under.