Skip to content

feat: add python line and branch coverage governance to ci - #317

Open
forrestjgq wants to merge 2 commits into
mainfrom
feat/python_coverage_ratchet
Open

feat: add python line and branch coverage governance to ci#317
forrestjgq wants to merge 2 commits into
mainfrom
feat/python_coverage_ratchet

Conversation

@forrestjgq

Copy link
Copy Markdown
Contributor

Summary

Adds Python coverage governance to CI so existing coverage cannot silently
regress and new code stays well tested, without pretending the project is
already near 100%.

  • scripts/coverage_gate.py plus make coverage* targets compute line and
    branch coverage for the raven package and drive four gates:
    • summary of totals and the lowest-covered files;
    • ratchet of total line and branch coverage against the audited
      .github/coverage-baseline.json (0.05pp tolerance for rounding noise);
    • diff coverage requiring 90% on the executable lines a PR changes
      (comments, deletions, and non-executable lines are excluded);
    • baseline-check rejecting any proposed baseline that lowers line or
      branch coverage relative to the target branch.
  • The CI unit job runs the suite once with coverage and reuses the report for
    every gate, uploads xml/json/html artifacts on every run (if: always()),
    and keeps diff coverage as a PR-only gate.
  • pyproject.toml configures coverage.py (branch mode, raven source, three
    documented omissions); generated reports are gitignored.
  • Key decision: the ratchet uses a manually bumped, audited baseline rather
    than auto-raising, so the diff-coverage gate is what guards new code while the
    ratchet only blocks regression.

The production-file pathspec uses git glob magic (:(glob)raven/**/*.py) so
top-level modules such as raven/__init__.py are not silently dropped from diff
coverage; a regression test covers this.

Type

  • Feature

Verification

  • uv run --frozen --python 3.12 pytest tests/test_coverage_gate.py -q -> 13 passed

  • uv run --frozen --python 3.12 --extra dev ruff check scripts/coverage_gate.py tests/test_coverage_gate.py -> All checks passed

  • uv run --frozen --python 3.12 --extra dev ruff format --check scripts/coverage_gate.py tests/test_coverage_gate.py -> already formatted

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

No runtime raven code changes; this is CI and tooling only. The new gates can
fail PRs that lower coverage, which is the intended behavior. Rollback is a
plain revert of this commit (or disabling the coverage steps in the unit job).

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

Add scripts/coverage_gate.py and make targets that measure line and
branch coverage for the raven package and enforce three gates: a
monotonic ratchet against an audited baseline, 90% coverage on the
executable lines a pull request changes, and a baseline that cannot be
lowered relative to the target branch. The CI unit job runs the suite
once with coverage and reuses the report for every gate, uploading xml,
json, and html artifacts on every run.

Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Claude finished @forrestjgq's task in 1m 32s —— View job


Review: python coverage governance CI

No blockers; this can merge.

  • Read prior review comments on this PR
  • Diffed the two commits (af70757 feat, e6e4b31 fix) against origin/main
  • Verified the two previously-flagged findings
  • Swept the rest of the diff (Makefile, pyproject.toml, coverage_gate.py, tests) for new issues

Both findings from the earlier review round were fixed in the follow-up commit e6e4b31:

  • .github/workflows/ci.yml: the unit job's install step is back to uv sync --locked --all-extras --dev --python ${{ matrix.python }}, so lockfile-freshness enforcement is restored while still pinning the matrix interpreter.
  • scripts/coverage_gate.py:196: parse_changed_lines now resets path = None on any +++ line that isn't +++ b/... (e.g. +++ /dev/null), with a regression test (test_parse_changed_lines_ignores_deleted_file_hunks) covering it.

The coverage-baseline.json was also regenerated against the Ubuntu/3.12 CI run itself so the ratchet reproduces at delta 0.00, matching the commit's stated intent.

No new issues found in the delta: gate logic (ratchet, diff, baseline-check), Makefile targets, and pyproject.toml coverage config are consistent with the PR description, and new files carry module docstrings per the updated AGENTS.md §1.1 rule.

Comment thread .github/workflows/ci.yml Outdated

- name: Install dependencies
run: uv sync --locked --all-extras --dev
run: uv sync --frozen --all-extras --dev --python ${{ matrix.python }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[blocker] This silently removes the only CI check that verifies uv.lock is up to date with pyproject.toml. The previous step used uv sync --locked ..., which fails if the lockfile is stale; switching to --frozen (needed to add --python ${{ matrix.python }}) makes uv sync accept a stale lockfile without error. Grepping the repo, no other job or Makefile target uses --locked for this purpose (Makefile's install-deps already used --frozen before this PR, but this was the one CI step enforcing lockfile freshness). After this change, a PR that edits a dependency in pyproject.toml without running uv lock will no longer be caught by CI.

Suggest keeping the freshness check, e.g. uv sync --locked --all-extras --dev and setting the interpreter via uv python pin/UV_PYTHON env instead of the --python flag, or adding a separate uv lock --check/--locked step.

Fix this →

Comment thread scripts/coverage_gate.py Outdated
Comment on lines +188 to +193
def parse_changed_lines(diff: str) -> dict[str, set[int]]:
"""Return new-side hunk lines; deleted lines never enter the denominator."""
changed: dict[str, set[int]] = {}
path: str | None = None
for line in diff.splitlines():
if line.startswith("+++ b/"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] path is never reset/cleared between files, and only updated on +++ b/... lines. A deleted file's diff shows +++ /dev/null, which doesn't match, so its hunks would silently be attributed to whatever path was last set from an earlier file. Currently harmless because the only caller (git_diff_inputs) filters with --diff-filter=ACMR (no deletions ever reach this function), but it's a latent bug if parse_changed_lines is ever reused on a raw diff. Consider setting path = None when the new-file line isn't +++ b/....

The bootstrap baseline was generated on macOS; the Ubuntu 3.12 CI run
produces slightly lower line and branch coverage, so the ratchet failed
by more than its 0.05pp tolerance. Replace it with the candidate emitted
by that CI run so the ratchet reproduces at delta 0.00.

Also restore uv sync --locked on the unit job, since dropping it for
--frozen removed the only CI check that uv.lock matches pyproject.toml,
and reset the parser path on a +++ /dev/null target so a deleted file's
hunks cannot be misattributed if parse_changed_lines runs on an
unfiltered diff.

Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
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.

2 participants