Skip to content

Measure PR Review Return on Cost in the Backlog Report - #3886

Merged
ptr727 merged 13 commits into
developfrom
feature/review-metrics
Nov 5, 2026
Merged

ptr727 merged 13 commits into
developfrom
feature/review-metrics

Conversation

@ptr727

@ptr727 ptr727 commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Why

The PR bots are moving from automatic to policy-driven review, per the review-cutover track (#3877). That move needs before and after figures for what each review returns. This adds them to scripts/backlog_metrics.py, so a baseline can be captured before any repository changes its .github/pr-review.json.

What

Two new tables in the weekly report, read from the window's merged pull requests through one paged GraphQL search per repository, newest created first, capped at 500 with a reached cap named.

  • Per bot: the policy trigger and the branch it was read at, review requests split into review-requested events by a bot actor and everything else, and rounds per reviewed pull request. CodeRabbit's reviewed heads are classed as automatic, explicit, or unclassified from the commit time, the ask and request times, and the first real review. Qodo's rounds are counted as the gate counts them, from its asks and its one standing comment, with no per-head classes. Threaded findings, findings a clean local attestation missed, and findings on all-green heads follow.
  • Per repository: commits per merged pull request, local attestation findings, failing heads, and failing heads after a clean local attestation. Promotion pull requests are left out and counted apart.

The report now runs as a uv script, uv run --no-project --script scripts/backlog_metrics.py, with the same inline metadata as pr_review.py. That lets it import the review gate's own readers rather than copies: attestation per read commit, page_flag for cut connections, reviewer_nodes with refusal_of for reviewed heads, side_asks and side_command for asks, side_rounds for Qodo, and VOCABULARIES for the logins. The server timer's command has to change to that form, with uv on the host, before the next weekly run.

The policy is read at develop where it exists, since feature pull requests are gated there, and otherwise at the default branch. It goes through pr_review_policy.fetch, so a file the gate refuses prints error and a file it takes as absent prints the hub default's triggers marked (default). The gate's absent test, a 404 or a 422, moves from pr_review.py into gh_graphql.py as said_absent, so both share it. A change to the file inside the window is noted. A failed default-branch, develop, policy, or history read still writes the report, names that read on stderr, and exits 1. A failed review read still writes the backlog and billing tables, says the review figures were unavailable, and exits 1.

Verification

  • uv run --no-project --with-requirements tests/requirements.txt python tests/test_backlog_metrics.py: 79 tests pass. Each fix was reverted on its own and its test failed.
  • The full suite under uv run --no-project --with-requirements tests/requirements.txt python -m unittest discover -s tests: 3532 tests pass.
  • A live run, uv run --no-project --script scripts/backlog_metrics.py --repo ProjectTemplate --days 7, exits 0 in 41 seconds. Its per-repository table is identical before and after the switch to the gate's attestation.
  • ruff check and format are clean. mypy reports nothing in the changed files beyond what develop already reports.

Local-review findings, disposed

  • introduced, design, left by the maintainer's decision: the reviewed-heads set in review_pr restates pr_review_rounds.reviewed_heads's filter rather than calling it, since the report needs the set and the gate returns a count. Exposing the set there would take pr_review_rounds.py past its 700-line ceiling, so Share the Attestation Marker and Reviewer Logins Between the Gate and the Metrics Report #3880 exposes it when it splits that file.
  • introduced, fixed in be564a6: classify_rounds compared a committedDate keeping the committer's offset with UTC times as strings. pr_review_checks.utc_instant now converts each to UTC first, and the gate's age builds on it in 787b3b3, so one parser reads every GitHub stamp.
  • introduced, fixed in be564a6: graphql duplicated the GraphQLError conversion walked already makes.
  • introduced, fixed in 787b3b3: the thread loop restated the gate's thread_author, and a truncation note named the PR cap whichever cap was reached.
  • introduced, fixed in 9c3240b: the README's commit-date caveat named the wrong count, its markdown-it-py list left out backlog_metrics.py, and two docstrings credited the UTC conversion rather than the parse with ordering stamps.
  • introduced, fixed in e4a655b: no test built a review-request event naming CodeRabbit, so dropping events from its round triggers passed every case.

Pre-existing findings, filed

Dropped findings

  • spec/audit.py and .github/actions/repo-gate/repo_gate.py keep their own HTTP-status parse beside said_absent, a consolidation on text this change did not touch.

Follow-ups

🤖 Generated with Claude Code

ptr727 and others added 6 commits November 5, 2026 07:05
Moving the PR bots from automatic to requested review needs a before and
after figure for what each review returns. The report now reads the
window's merged pull requests and tallies rounds to land, requests by
automation and by user, bot findings, local attestation findings,
findings a clean local review missed, and failing or all-green heads.

The policy trigger is read once per repository from the default branch,
where a 404 means absent and any other failure is printed and exits 1.
Every connection is capped and a cap that was reached is named in the
report so a truncated figure is never presented as whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efault

The policy file nests each bot under `bots`, per spec/pr-review.schema.json, so the
trigger column now reads it there. A repository carrying no file runs under the hub's
default policy, so the column says `default` rather than `none`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An adversarial review found the first review metrics unreliable. A 50
pull request cap hid most of a busy window, queued check suites made
every head read as pending, and any comment could forge a local
attestation. Promotion pull requests also dominated the commit and
failing-head counts.

The read now pages through the shared GraphQL reader up to 500 pull
requests and names a hit cap. Heads are classified from the rolled-up
commit status. Attestations count only from trusted, visible comments
on the matching base. Trigger comments match on any line, automatic
rounds are derived for the two bots that leave no request event, and
a change to the policy file inside the window is noted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A second adversarial pass found defects in the paging and the policy
reads. Sorting by update time let a pull request move between pages,
and the automatic round count subtracted comments from heads, which
are different units. A failed history read also blanked the trigger.

The search now sorts by creation, skips a repeated pull request and
notes it. Each head a bot reviewed is sorted as explicit, automatic or
unclassified from the times of its commit, the triggers and the first
review. The history read has its own error and note, asks for one more
commit than it reports, and the policy and history are read at develop
where it exists, since feature pull requests are gated against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…licit Rounds

The report now reads the review policy through pr_review_policy, so a file
the gate refuses prints as an error and an absent file prints the hub
default's triggers marked as such. The per-bot table gains the explicit
rounds column it was already counting. A failed develop probe no longer
names the default branch, and stderr says which read failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Read

The report now reads the policy file through pr_review_policy's fetch,
with the gate's own absent test, so a 422 reads as the hub default here
as it does in the gate. That test moves from pr_review.py into
pr_review_policy.py, so the two share one definition. A failed read of
the default branch or of develop is named as that read rather than as
the policy file, and the empty-trigger guard GitHub's schema rules out
is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings November 5, 2026 15:37
@ptr727

ptr727 commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 82e8d1514cbd62e3bdef20e74ef84725593c08e3, the content this pull request carries at that commit against develop, and it recorded 7 findings.

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Copilot AI 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.

🟡 Changes recommended

Multiple active findings show that the report can diverge from the gate and mishandle review, policy-history, and GraphQL failures, while one test is nondeterministic.

11 open issues
What changed in this PR

This pull request adds review return-on-cost metrics to the weekly backlog report, including policy state, review requests, rounds, findings, attestations, failing heads, and repository-level results. It also centralizes absent-response detection and expands documentation and test coverage.

Changes:

  • Adds paged GraphQL collection and aggregation for per-bot and per-repository review metrics.
  • Reads review policies from the appropriate repository branch and reports policy errors or history changes.
  • Adds Markdown and HTML report rendering, documentation, and extensive tests.
  • Unresolved findings remain around metric accuracy, parser and gate divergence, error handling, failure isolation, and test determinism.
File Description
tests/​test_backlog_metrics.py Adds fixtures and coverage for review metrics, policy handling, pagination, failures, and rendering.
scripts/​README.md Documents the new review return-on-cost tables, measurement rules, and failure behavior.
scripts/​pr_review.py Routes absent-response detection through the shared policy helper.
scripts/​pr_review_policy.py Adds the shared GitHub absent-response predicate.
scripts/​backlog_metrics.py Implements GraphQL review collection, policy reads, metric aggregation, error handling, and report rendering.

Review effort: Lite

Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread scripts/backlog_metrics.py Resolved
Comment thread tests/test_backlog_metrics.py Resolved
@codecov

codecov Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 84.11%. Comparing base (b38e084) to head (e4a655b).
⚠️ Report is 102 commits behind head on develop.

Files with missing lines Patch % Lines
scripts/backlog_metrics.py 99.64% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3886      +/-   ##
===========================================
+ Coverage    81.60%   84.11%   +2.50%     
===========================================
  Files           26       32       +6     
  Lines         9070    10572    +1502     
===========================================
+ Hits          7402     8893    +1491     
- Misses        1668     1679      +11     
Flag Coverage Δ
python-3.13 84.11% <99.66%> (+2.50%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ptr727 and others added 3 commits November 5, 2026 07:58
The report kept its own copies of the gate's login table, attestation
reader, ask matcher and reviewed-head rule. Each copy could drift from the
gate and then report figures the gate would not accept. The script is now a
uv script with the gate's inline metadata, so it imports those readers.

Attestations, reviewed heads, refusals and asks now follow the gate, and
the first attestation per head wins as it does there. Qodo edits one
standing comment that names no head, so its rounds come from the gate's
side_rounds and its round classes read n/a.

The policy probes follow the gate's absent-file rule. A history error is an
error, and a walk error reaches main as a named line rather than a trace.
A failed review read no longer discards the backlog and billing report.
The null-node test now starts no process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local pass over the gate-reader rework found the round timing still
read a refusal as a head's first review, so an ask between a refusal
and the real round classed it as automatic. Every ask the gate sends
posts under the session's own account, so asks now count as a user's.

The absent test moves to gh_graphql, the shared gh reader, since a
branch probe is not a policy read. A contents read with no content is
named as such rather than decoded as the text None.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report still restated the gate's attestation rule and its page flag
beside the readers it already imports. It now calls the gate's own
attestation once per read commit and its page_flag, so a change to
either reaches both. A live run against the hub gave the same figures.
The policy read's KeyError and TypeError arms answered no input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 26c4c01f4b8ecf1be6d3a90eac853f73a799970f, the content this pull request carries at that commit against develop, and it recorded 2 findings.

ptr727 and others added 2 commits November 5, 2026 08:43
A commit date keeps the committer's own offset while review and ask
times are UTC, so comparing them as text misordered a head committed
away from UTC. gh_graphql now parses a GitHub timestamp to UTC, and the
round classes compare those instants. The gate's own comparison is
#3887. The graphql wrapper no longer converts an error that walked
already names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UTC parse moves beside the gate's age in pr_review_checks, and age
now builds on it, so the gate and the report read a GitHub stamp one
way. A stamp naming no zone reads as no instant in both. The report's
thread loop calls the gate's thread_author, and a truncation note now
names the cap each cut connection reached rather than the PR cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 787b3b32112529f666f32b890b481316fef10f9f, the content this pull request carries at that commit against develop, and it recorded 3 findings.

ptr727 and others added 2 commits November 5, 2026 09:08
The UTC docstrings credited the conversion with fixing the order, while
parsing is what fixes it, since aware datetimes compare by instant. The
README caveat had the commit-date error inflating the automatic count,
where a commit made before its push widens the window and inflates the
explicit one. The markdown-it-py list now names backlog_metrics.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No case built a request event naming CodeRabbit, so dropping events
from the round triggers left every test passing. This one fails then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head e4a655bb80a7a16aa03fc9c9f6b6109105fa971c, the content this pull request carries at that commit against develop, and it recorded 0 findings.

@ptr727
ptr727 merged commit ba118f1 into develop Nov 5, 2026
13 checks passed
@ptr727
ptr727 deleted the feature/review-metrics branch November 5, 2026 17:28
ptr727 added a commit that referenced this pull request Nov 6, 2026
…t Policy (#3892)

## Summary

Promotes #3881, #3884, #3886, #3888, #3889, and #3891 to `main`.

Closes #3200.
Closes #3885.
Closes #3887.

- **Per-repository review policy (#3881).** `scripts/pr_review.py` reads
which bots review a repository, on which bases, when, and how often,
from the repository's own policy, with the hub default reproducing
today's behavior.
- **Review return on cost (#3886).** `scripts/backlog_metrics.py`
measures reviewer cost per useful finding. It is now a uv script, so the
metrics server's weekly timer must run it as `uv run --no-project
--script scripts/backlog_metrics.py` from this release on.
- **After-checks side reviewer (#3888).** An explicit after-checks
CodeRabbit or Qodo is asked once CI settles, and ask times compare in
UTC.
- **Commit in place for an operational repository (#3889).** The policy
file became `.github/agent-policy.json`, with its schema under `review`
and a `guard` section that lets `gh-write-guard` allow an in-place
commit on a listed branch.
- **Agent policy rendered from the registry (#3891).**
`repo-config/configure.sh` renders `.github/agent-policy.json` from
`registry/repos.json` and applies the bot settings it implies: the
Copilot ruleset rule, CodeRabbit's auto-review keys, and Qodo's
auto-feedback key. Once this is on `main`, `configure.sh check` reports
agent-policy drift on every repository until `apply --checkout` has been
run there.
- **Dependabot (#3884).** The actions-deps group bump.

Each feature pull request carried recorded local strict review passes,
with every finding fixed, declined with evidence, or filed (#3890).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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