Repository navigation
chore(scripts): guard the file set each coverage run measures (#2709) - #2711
Conversation
The per-file coverage gate only judges files that reach coverage-summary.json, and nothing checked that the report holds the files it should. Under vitest 5 web's report collapsed from 418 files to 11 and the gate stayed green (#2557). verify:coverage-scope resolves each gated client's coverage include/exclude through Vitest, expands them against the file system with Node's own glob (never Vitest's matcher), and fails naming every declared file missing from the report. It runs as the last step of `npm run coverage`, so local:gate and CI's coverage job both enforce it. A coverage client it does not list fails deny-by-default. On v2/main every client's report matched its declaration except web's pendingReauth.ts, a type-only module v8 has no row for; it joins web's existing pure-type coverage excludes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
🟡 Changes recommended
The omission escape hatch accepts missing reasons and fails to detect exemptions that become stale.
1 open finding
What changed in this PR
Adds a durable guard ensuring coverage reports contain every file declared by each client’s coverage configuration.
Changes:
- Adds and tests
verify:coverage-scope. - Wires the guard into coverage execution and CI.
- Documents the guard and excludes a type-only web module.
| File | Description |
|---|---|
scripts/verify-coverage-scope.mjs |
Implements coverage-scope validation. |
scripts/verify-coverage-scope.test.mjs |
Tests expansion, comparison, roster, and failure cases. |
package.json |
Adds and wires the verification command. |
clients/web/vite.config.ts |
Excludes a type-only module. |
docs/quality-gate.md |
Documents the new gate. |
AGENTS.md |
Adds the coverage-scope rule. |
.github/workflows/main.yml |
Documents CI coverage enforcement. |
.claude/skills/pre-push-gate/SKILL.md |
Adds troubleshooting guidance. |
🧠 Review effort: Balanced
…ndant ones (#2709) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
I ran this PR's own Then I renamed one gated directory on disk and left
The fix fits the loop that already exists: expand each include pattern on its own and fail when one yields zero files, naming the client and the pattern. The first block above is that check run over all five clients on this branch, and no pattern is dead today, so " We keep a catalog of this shape; the entry is "expected to find this, found nothing is a failure, not a pass", and the check we run for it is |
|
Review summary:
|
✅ Verdict: MERGEABLEThe guard passed on unmodified real coverage reports, both locally (macOS) and in CI (Linux), with identical counts. It failed correctly in every negative case tried. CI is green, DCO passes, and the branch is 0 commits behind Smoke test of PR head
|
| Where | Result |
|---|---|
Local, the full npm run coverage chain |
✓ web 419 · cli 20 · mcpdo 31 · tui 41 · launcher 1, exit 0 |
CI coverage job (run 38341760321) |
✓ same five counts, success |
The counts match the ones given in the PR body and in #2709.
Negative: the guard must go red
All four cases were run against real reports and restored afterwards.
| # | Mutation | Result |
|---|---|---|
| 1 | Web summary cut down to the 11 clients/web/server/** rows, which is the vitest 5 / #2557 shape |
✗ web: 408 of 419 file(s) … missing (the report holds 11), with 244 clients/web/src/** files among those listed, exit 1 |
| 2 | New type-only file clients/web/src/utils/smokeTypeOnly.ts |
✗ web: 1 of 420 … missing, naming the file, exit 1 |
| 3 | Row for core/mcp/inspectorClient.ts dropped from web's summary |
✗ web: 1 of 419 … missing, naming core/mcp/inspectorClient.ts |
| 4 | clients/launcher/coverage/coverage-summary.json removed |
✗ launcher: no …coverage-summary.json — run its coverage first. A missing summary fails; it is not skipped. |
After restoring, the guard exits 0 again and git status is clean.
Other checks
node --test scripts/verify-coverage-scope.test.mjs: 16/16 pass. The PR body says 14; the follow-up commit added the two omission cases.prettier --checkandeslint --max-warnings 0on both new scripts pass.verify:skillspasses. The listing is at 3907/4000; the new entry is skill body text, so the listing cost is unchanged.- The
pre-push-gatediagnosis entry covers both failure causes and the missing-summary case.
Non-blocking notes
- The guard repeats a Vite warning four times. Resolving web's config through
createVitestre-emits Vite'sconfigLoader: 'native'deprecation warning (about__dirnameand extensionless imports invite.config.ts) four times. That adds about 28 lines between the guard's banner and its verdict. The warning comes from the existing config, not this PR, so this is only noise. SettingVITE_CONFIG_NATIVE_IGNORE_WARNING=truein the guard's environment, or fixing the config later, would quiet it. - New type-only files now need an exclude entry. Negative 2 shows the trade-off this guard makes. A new type-only module under a gated web directory now fails
coverageuntil someone adds it to the coverageexclude, because v8 reports no row for it. That is the intended behavior, and the skill entry points to the fix. It is called out here so the first person to hit it isn't surprised.
🤖 Generated with Claude Code

Closes #2709
Problem
The per-file ≥90 gate only judges files that reach
coverage-summary.json, and nothing checked that the report holds the files it should. During #2557, vitest 5 shrank web's report from 418 files to 11, andlocal:gatestayed green.What changed
scripts/verify-coverage-scope.mjs(new,npm run verify:coverage-scope). This is the issue's preferred shape: it compares each report against an independent expansion of the client's own declaration.include/excludethrough Vitest, so it checks the patterns the run actually used, including Vitest's hard-coded default excludes.fs.globSync/path.matchesGlob, never Vitest's matcher.test:coveragescript that the guard doesn't list fails (deny-by-default, likeverify:test-timeouts).KNOWN_OMISSIONS, which must state a reason. It is empty today.clients/web/server/**). Excludes match leniently, so they can only shrink what the guard expects.coveragescript, so it runs inlocal:gateand in CI'scoveragejob. It reads coverage output, so it is not part ofvalidate.clients/web/vite.config.ts:src/utils/pendingReauth.tsis added to web's existing pure-type coverage excludes. It contains only type declarations, so v8 reports no row for it, and the guard flagged it on unmodifiedv2/main. That was the only difference on the current branch.scripts/verify-coverage-scope.test.mjshas 14 cases intest:scripts. They cover the pattern-reading rules, the diff, the roster, andrun()over a fixture repo, including the vitest-5 collapse toclients/web/server/**.docs/quality-gate.md, a diagnosis entry in thepre-push-gateskill, one sentence inAGENTS.mdnext to the existing coverage-whitelist rule, and a comment on the CI step.Acceptance
v2/mainas-is passes.npm run local:gateis green. The guard reports web 419, cli 20, mcpdo 31, tui 41, launcher 1, matching the counts in the issue.vitestand@vitest/coverage-v8, web's@vitest/browser-playwrightand itsvitestoverride) to5.0.4, as chore(deps): monthly dependency refresh #2557 did, and ran web coverage. The report held 11 files, and the guard failed withweb: 408 of 419 file(s) … missing, listing each one.legacy-peer-deps, because@storybook/addon-vitest@10.5.5peersbrowser-playwright ^4.🤖 Generated with Claude Code