Skip to content

feat(coverage): devkit-owned coverage gate — fail-hard on absent data, config-driven, opt-in - #138

Merged
norvalbv merged 2 commits into
mainfrom
norvalbv/coverage-gate
Jul 19, 2026
Merged

feat(coverage): devkit-owned coverage gate — fail-hard on absent data, config-driven, opt-in#138
norvalbv merged 2 commits into
mainfrom
norvalbv/coverage-gate

Conversation

@norvalbv

@norvalbv norvalbv commented Jul 19, 2026

Copy link
Copy Markdown
Owner

What this PR does

Lifts the coverage check out of a consumer's hand-rolled .husky/pre-commit and makes it a first-class devkit-owned, opt-in deterministic guard that is fail-closed on absent data.

  • gate-engine/coverage/run.mts — the gate. Reads coverage/coverage-final.json (istanbul/V8) and aggregates statements/functions/branches/lines.
  • gate-engine/config.mtsCoverageConfig type + coverage field/default/resolver.
  • gate-engine/deterministic/run.mts — registers coverage as an opt-in guard; adds DEFAULT_IDS so opt-in guards are excluded from the missing-config fallback.
  • cli/lib/ship/prepare-gate-worktree.sh — links coverage/ into the gate worktree so the gate verifies real coverage.
  • cli/lib/components.mts, package.json, guard.config.example.json — registry entry, guard-coverage bin, config docs.
  • docs/decisions/coverage-gate.md — the decision record.

Behaviour (guard.config.json coverage)

value coverage data result
absent (default {}) missing exit 1 — run test:run:coverage first (was a silent warn-and-pass)
absent present pass (no % floor)
false bypass (the only opt-out)
{ statements, functions, lines?, branches? } any set key below exit 1, names the shortfall
present but malformed exit 1 (corrupt data isn't verification)

Exit contract matches the deterministic trichotomy (0 pass/bypass, 1 fail); coverage deliberately has no fail-open (2) arm — that was the defect being removed.

Why

The old hand-rolled block warned-and-passed when coverage/coverage-final.json was absent, so substantive commits could ship with coverage silently unverified. It was fail-open on purpose because devkit ship's ephemeral worktree never carried the gitignored coverage/ artifact. This fixes that at the root: prepare_gate_worktree now links coverage/ into the worktree (existence-guarded — absent → not linked → the gate fails hard), so the gate verifies real coverage instead of being warned away.

Opt-in / no surprise

coverage is in GUARD_IDS but not RECOMMENDED_GUARD_IDS (it needs a test:run:coverage provider). It runs only when a repo selects it; DEFAULT_IDS keeps it out of the missing-config fallback so an unadopted/CI repo without .devkit/config.json is never wedged. devkit upgrade surfaces it as an opt-in via the existing newBundledGates path (like review/sentry).

Tests

New gate-engine/coverage/__tests__/run.test.mts (bypass / absent→1 / malformed→1 / thresholds / per-key enforcement / istanbul aggregation) + deterministic opt-in wiring + config resolution + registry assertions. tsc, biome, and the full suite pass.

Follow-up (not in this PR)

Migrate frink onto this guard once released: bump devkitRef → devkit upgrade (accept coverage) → delete frink's hand-rolled block → set coverage: { statements: 62, functions: 59 } in frink's guard.config.json.

Summary by CodeRabbit

  • New Features

    • Added an opt-in coverage guard to verify coverage thresholds.
    • Added support for statement, function, branch, and line coverage metrics.
    • Added the guard-coverage command for running coverage checks.
    • Added coverage configuration with customizable thresholds or an explicit opt-out.
  • Documentation

    • Added guidance for configuring and using the coverage guard.
  • Tests

    • Added coverage validation for missing, invalid, and insufficient coverage data.

…, config-driven, opt-in

Lift the hand-rolled coverage check out of consumer .husky/pre-commit into a
first-class opt-in deterministic guard. Fail-CLOSED: coverage:false bypasses,
an absent/malformed coverage-final.json exits 1 (was a silent warn-and-pass),
a present artifact enforces the guard.config.json `coverage` thresholds
(statements/functions/lines/branches — only keys set are enforced). Default
{} = active-strict (no % floor, but absent data still fails hard).

- gate-engine/coverage/run.mts: the gate + istanbul/V8 aggregation
- gate-engine/config.mts: CoverageConfig type/field/default/resolver
- deterministic/run.mts: registered opt-in; DEFAULT_IDS excludes opt-in guards
  from the missing-config fallback so an unadopted/CI repo is never wedged
- ship: prepare_gate_worktree links coverage/ into the worktree so the gate
  verifies real coverage instead of being warned away (the ship-worktree bug)
- components/package.json/guard.config.example.json: registry, bin, docs
- docs/decisions/coverage-gate.md
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@norvalbv, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3cbed93b-8b4b-4146-840a-302217045c8d

📥 Commits

Reviewing files that changed from the base of the PR and between 78ed8d0 and 6de09f5.

📒 Files selected for processing (3)
  • gate-engine/coverage/__tests__/run.test.mts
  • gate-engine/coverage/run.mts
  • guard.config.json
📝 Walkthrough

Walkthrough

Changes

The PR adds an opt-in coverage guard with fail-closed artifact validation, configurable thresholds, deterministic execution, CLI selection, coverage artifact linking in ship worktrees, documentation, and a public guard-coverage command.

Coverage guard

Layer / File(s) Summary
Coverage configuration and execution
gate-engine/config.mts, gate-engine/coverage/*, guard.config.example.json, package.json, docs/decisions/coverage-gate.md
Coverage configuration supports strict defaults and explicit false, while the runner aggregates metrics, validates coverage-final.json, enforces configured thresholds, and exposes a CLI binary with tests and documentation.
Deterministic opt-in orchestration
gate-engine/deterministic/run.mts, gate-engine/deterministic/__tests__/run.test.mts
The deterministic registry marks coverage as opt-in, excludes it from missing-config defaults, and runs it only when selected.
CLI selection and worktree artifact wiring
cli/lib/components.mts, cli/__tests__/components-new-gates.test.mts, cli/lib/ship/prepare-gate-worktree.sh
The CLI exposes coverage as selectable but not recommended, updates bundled-gate expectations, and links the coverage directory into prepared gate worktrees.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant runDeterministic
  participant runCoverage
  participant CoverageArtifact
  Developer->>runDeterministic: select coverage guard
  runDeterministic->>runCoverage: execute coverage gate
  runCoverage->>CoverageArtifact: read coverage/coverage-final.json
  CoverageArtifact-->>runCoverage: coverage data or error
  runCoverage-->>Developer: pass or fail exit status
Loading

Possibly related PRs

  • norvalbv/devkit#11: Refactors the deterministic hook and orchestration path used by the new coverage guard.
  • norvalbv/devkit#40: Extends the shared CLI guard registry and wizard option wiring.
  • norvalbv/devkit#95: Introduces the worktree symlink setup extended here for the coverage artifact.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the new devkit-owned, opt-in coverage gate and its fail-closed behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch norvalbv/coverage-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gate-engine/coverage/run.mts`:
- Around line 1-21: The new gate-engine/coverage directory is rejected because
coverage is not an allowed root domain. Register coverage in devkit’s own
gate-engine structure configuration, preserving the existing folder location and
ensuring the folder-structure validation allowlist includes it.
- Around line 39-76: Validate the parsed coverage value and each file’s nested
coverage fields before or within computePercentages, rejecting null, arrays,
non-object entries, and non-array branch counters with an error. Ensure this
validation is handled by the existing catch around JSON parsing/processing so
malformed-but-parseable input produces the documented “not valid JSON”-style
message and exits through the established fail-closed path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4557f521-61b5-4bbf-8fb0-9f5ff7b3fb8d

📥 Commits

Reviewing files that changed from the base of the PR and between 85ee11f and 78ed8d0.

📒 Files selected for processing (11)
  • cli/__tests__/components-new-gates.test.mts
  • cli/lib/components.mts
  • cli/lib/ship/prepare-gate-worktree.sh
  • docs/decisions/coverage-gate.md
  • gate-engine/config.mts
  • gate-engine/coverage/__tests__/run.test.mts
  • gate-engine/coverage/run.mts
  • gate-engine/deterministic/__tests__/run.test.mts
  • gate-engine/deterministic/run.mts
  • guard.config.example.json
  • package.json

Comment thread gate-engine/coverage/run.mts
Comment thread gate-engine/coverage/run.mts Outdated
…r the coverage structure domain

- computePercentages now validates shape and THROWS on parseable-but-malformed
  input (non-object root, null/array/non-object entry, non-array branch counter);
  runCoverage routes it through the existing catch, so malformed data fails CLOSED
  with a clean message + exit 1 instead of crashing on a TypeError or reading a
  garbage entry as 100% coverage.
- register `coverage` in guard.config.json structure libDomains["@root"] — the only
  gate-engine domain that was missing, so the walk-based structure gate accepts it.
@norvalbv
norvalbv merged commit c8a009c into main Jul 19, 2026
1 of 2 checks passed
norvalbv pushed a commit that referenced this pull request Jul 19, 2026
Replays PR #108 cleanly on current main after #104 and #138 merged. Configures review-mode gate routing and initialization support. Includes a minimal correctness hardening so an unset review guard allowlist behaves like an empty allowlist, with regression coverage.
norvalbv added a commit that referenced this pull request Jul 19, 2026
…seline

Repair scope violation flagged in human review: this PR (review profile and review-mode gate routing) had picked up an unrelated size-debt "fix" that deleted research-rationale comments.

- gate-engine/decisions/detect.mts: restores the research-rationale doc comments (arXiv:2402.14848, arXiv:2302.00093, arXiv:2407.16833 citations, the --gate/scan exit-code contract, the W-3 portability-invariant note) that were deleted to dodge an unrelated size-debt gate. No logic changed either direction - comment-only.
- eslint/baselines/size-lines.json: re-freezes detect.mts at its true 550 lines (main itself was already 25 lines over its own recorded 525 ceiling for this file before this PR touched it - a pre-existing stale baseline, not new debt from this change).

docs/benchmarks/catalog.json / README.md are kept as-is (not reverted): removing that guard-coverage catalog entry re-exposes a genuine pre-existing benchmarks:check failure inherited from an earlier, unrelated PR (#138) that added the guard-coverage bin without a catalog entry. That one-line backfill is a real, separate bug fix bundled here only because it currently blocks every commit, not scope creep from this PR's stated purpose.
norvalbv added a commit that referenced this pull request Jul 22, 2026
Replays PR #108 cleanly on current main after #104 and #138 merged. Configures review-mode gate routing and initialization support. Includes a minimal correctness hardening so an unset review guard allowlist behaves like an empty allowlist, with regression coverage.
norvalbv added a commit that referenced this pull request Jul 22, 2026
…seline

Repair scope violation flagged in human review: this PR (review profile and review-mode gate routing) had picked up an unrelated size-debt "fix" that deleted research-rationale comments.

- gate-engine/decisions/detect.mts: restores the research-rationale doc comments (arXiv:2402.14848, arXiv:2302.00093, arXiv:2407.16833 citations, the --gate/scan exit-code contract, the W-3 portability-invariant note) that were deleted to dodge an unrelated size-debt gate. No logic changed either direction - comment-only.
- eslint/baselines/size-lines.json: re-freezes detect.mts at its true 550 lines (main itself was already 25 lines over its own recorded 525 ceiling for this file before this PR touched it - a pre-existing stale baseline, not new debt from this change).

docs/benchmarks/catalog.json / README.md are kept as-is (not reverted): removing that guard-coverage catalog entry re-exposes a genuine pre-existing benchmarks:check failure inherited from an earlier, unrelated PR (#138) that added the guard-coverage bin without a catalog entry. That one-line backfill is a real, separate bug fix bundled here only because it currently blocks every commit, not scope creep from this PR's stated purpose.
norvalbv added a commit that referenced this pull request Jul 24, 2026
…, config-driven, opt-in (#138)

* feat(coverage): devkit-owned coverage gate — fail-hard on absent data, config-driven, opt-in

Lift the hand-rolled coverage check out of consumer .husky/pre-commit into a
first-class opt-in deterministic guard. Fail-CLOSED: coverage:false bypasses,
an absent/malformed coverage-final.json exits 1 (was a silent warn-and-pass),
a present artifact enforces the guard.config.json `coverage` thresholds
(statements/functions/lines/branches — only keys set are enforced). Default
{} = active-strict (no % floor, but absent data still fails hard).

- gate-engine/coverage/run.mts: the gate + istanbul/V8 aggregation
- gate-engine/config.mts: CoverageConfig type/field/default/resolver
- deterministic/run.mts: registered opt-in; DEFAULT_IDS excludes opt-in guards
  from the missing-config fallback so an unadopted/CI repo is never wedged
- ship: prepare_gate_worktree links coverage/ into the worktree so the gate
  verifies real coverage instead of being warned away (the ship-worktree bug)
- components/package.json/guard.config.example.json: registry, bin, docs
- docs/decisions/coverage-gate.md

* fix(coverage): fail closed on malformed coverage-final.json + register the coverage structure domain

- computePercentages now validates shape and THROWS on parseable-but-malformed
  input (non-object root, null/array/non-object entry, non-array branch counter);
  runCoverage routes it through the existing catch, so malformed data fails CLOSED
  with a clean message + exit 1 instead of crashing on a TypeError or reading a
  garbage entry as 100% coverage.
- register `coverage` in guard.config.json structure libDomains["@root"] — the only
  gate-engine domain that was missing, so the walk-based structure gate accepts it.
norvalbv added a commit that referenced this pull request Jul 24, 2026
Replays PR #108 cleanly on current main after #104 and #138 merged. Configures review-mode gate routing and initialization support. Includes a minimal correctness hardening so an unset review guard allowlist behaves like an empty allowlist, with regression coverage.
norvalbv added a commit that referenced this pull request Jul 24, 2026
…seline

Repair scope violation flagged in human review: this PR (review profile and review-mode gate routing) had picked up an unrelated size-debt "fix" that deleted research-rationale comments.

- gate-engine/decisions/detect.mts: restores the research-rationale doc comments (arXiv:2402.14848, arXiv:2302.00093, arXiv:2407.16833 citations, the --gate/scan exit-code contract, the W-3 portability-invariant note) that were deleted to dodge an unrelated size-debt gate. No logic changed either direction - comment-only.
- eslint/baselines/size-lines.json: re-freezes detect.mts at its true 550 lines (main itself was already 25 lines over its own recorded 525 ceiling for this file before this PR touched it - a pre-existing stale baseline, not new debt from this change).

docs/benchmarks/catalog.json / README.md are kept as-is (not reverted): removing that guard-coverage catalog entry re-exposes a genuine pre-existing benchmarks:check failure inherited from an earlier, unrelated PR (#138) that added the guard-coverage bin without a catalog entry. That one-line backfill is a real, separate bug fix bundled here only because it currently blocks every commit, not scope creep from this PR's stated purpose.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant