Repository navigation
Measure PR Review Return on Cost in the Backlog Report - #3886
Conversation
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>
|
A recorded local strict-review pass covers head |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
There was a problem hiding this comment.
🟡 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
Match trigger comments to the gate's request rules · New Share the gate's attestation parser · New Count Qodo standing-comment rounds · New Exclude refusal reviews from Copilot round counts · New Reuse the shared absent-response predicate · New Report policy-history read failures · New Convert cursor-walk errors to the report failure path · New Preserve backlog output when review collection fails · New Share reviewer-login lookup with the gate · New Reuse the per-merged formatter · New Inject a deterministic runner in the null-node test · New
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
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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>
|
A recorded local strict-review pass covers head |
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>
|
A recorded local strict-review pass covers head |
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>
|
A recorded local strict-review pass covers head |
…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)


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.
The report now runs as a uv script,
uv run --no-project --script scripts/backlog_metrics.py, with the same inline metadata aspr_review.py. That lets it import the review gate's own readers rather than copies:attestationper read commit,page_flagfor cut connections,reviewer_nodeswithrefusal_offor reviewed heads,side_asksandside_commandfor asks,side_roundsfor Qodo, andVOCABULARIESfor 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
developwhere it exists, since feature pull requests are gated there, and otherwise at the default branch. It goes throughpr_review_policy.fetch, so a file the gate refuses printserrorand 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 frompr_review.pyintogh_graphql.pyassaid_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 exits1. A failed review read still writes the backlog and billing tables, says the review figures were unavailable, and exits1.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.uv run --no-project --with-requirements tests/requirements.txt python -m unittest discover -s tests: 3532 tests pass.uv run --no-project --script scripts/backlog_metrics.py --repo ProjectTemplate --days 7, exits0in 41 seconds. Its per-repository table is identical before and after the switch to the gate'sattestation.developalready reports.Local-review findings, disposed
introduced, design, left by the maintainer's decision: the reviewed-heads set inreview_prrestatespr_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 takepr_review_rounds.pypast 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_roundscompared acommittedDatekeeping the committer's offset with UTC times as strings.pr_review_checks.utc_instantnow converts each to UTC first, and the gate'sagebuilds on it in 787b3b3, so one parser reads every GitHub stamp.introduced, fixed in be564a6:graphqlduplicated theGraphQLErrorconversionwalkedalready makes.introduced, fixed in 787b3b3: the thread loop restated the gate'sthread_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 outbacklog_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
scripts/pr_review_side.py:117, correctness:side_asked_on_headmakes the same mixed-offset string comparison. Filed as Compare Side-Reviewer Ask Times With the Head's Commit Time in UTC #3887.Dropped findings
spec/audit.pyand.github/actions/repo-gate/repo_gate.pykeep their own HTTP-status parse besidesaid_absent, a consolidation on text this change did not touch.Follow-ups
--days 28, is captured once this merges and the timer is moved.🤖 Generated with Claude Code