Context: #2597
Problem
Merge evidence is gathered with one gh pr list --state merged REST query per repository, matched locally by branch name. That approach has two costs the current design then has to work around:
- It truncates on high-traffic repositories, producing
merged-pr-window-truncated and rendering every absent merged finding in that repository unproven.
- Because the batch cannot answer for a branch it missed, an exact per-branch
--head fallback is needed — and because that transmits a local branch name to github.com, it is privacy-gated, producing merge-evidence-privacy-gated and leaving branches with no verdict at all.
A single aliased GraphQL query removes both. Neither finding kind needs to exist.
Evidence
Measured live against the GitHub GraphQL API:
query {
rateLimit { cost nodeCount }
b0: repository(owner:"…", name:"…") {
pullRequests(headRefName:"branch-a", first:1, states:[MERGED]) {
nodes { number headRefName headRefOid mergedAt state } } }
b1: repository(owner:"…", name:"…") { pullRequests(headRefName:"branch-b", …) { … } }
}
- 1 branch →
cost: 1
- 40 branches →
cost: 1, nodeCount: 40, all 40 aliases returned
Against a documented ceiling of 500,000 nodes per call, first/last ∈ 1–100, and a 5,000-point/hour primary limit. headRefName is an exact match.
By contrast, on the fleet measured: the REST approach capped claude-code-plugins at its 1000-PR window while 1403 merged PRs exist, and gated 102 branches in one repository and 27 in another. (Verification separately confirmed the 403 PRs outside the window contained no match for any unflagged branch, so the realized impact on that run was zero — but the mechanism is real and the report correctly said so.)
The privacy gate's stated remedy — a per-branch gh pr list --head for each gated branch — is 102 manual network round-trips in the worst case observed. That is not a remedy anyone will execute.
Also worth recording, since it is the obvious alternative and is wrong: the search API's head: qualifier matches "pull requests opened from branch names beginning with the word" — prefix semantics that would treat feature/auth-v2 as merged because feature/auth merged. Only GraphQL's headRefName argument is exact.
Proposed change
- Replace the per-repository REST merged-PR crawl with one aliased GraphQL query per repository, batching branches (100 per alias page, well within limits).
- Retire
merged-pr-window-truncated — a query that answers per-branch does not have a window to exhaust.
- Retire
merge-evidence-privacy-gated — the query transmits no local branch name that is not already a branch the tool is asking about on the operator's own repository, and the whole-repository form transmits none at all. If a privacy posture is still wanted, make it a configuration choice rather than a silent gate that leaves branches unverdicted.
- Never use the search API's
head: qualifier for branch matching.
Acceptance criteria
- A repository with more merged PRs than the old window returns complete merge evidence.
- No finding kind reports that merge evidence was withheld for branches the operator asked about.
- Branch matching is exact;
feature/auth and feature/auth-v2 never conflate.
- The rate cost per repository is documented and bounded.
Context: #2597
Problem
Merge evidence is gathered with one
gh pr list --state mergedREST query per repository, matched locally by branch name. That approach has two costs the current design then has to work around:merged-pr-window-truncatedand rendering every absent merged finding in that repository unproven.--headfallback is needed — and because that transmits a local branch name to github.com, it is privacy-gated, producingmerge-evidence-privacy-gatedand leaving branches with no verdict at all.A single aliased GraphQL query removes both. Neither finding kind needs to exist.
Evidence
Measured live against the GitHub GraphQL API:
cost: 1cost: 1,nodeCount: 40, all 40 aliases returnedAgainst a documented ceiling of 500,000 nodes per call,
first/last∈ 1–100, and a 5,000-point/hour primary limit.headRefNameis an exact match.By contrast, on the fleet measured: the REST approach capped
claude-code-pluginsat its 1000-PR window while 1403 merged PRs exist, and gated 102 branches in one repository and 27 in another. (Verification separately confirmed the 403 PRs outside the window contained no match for any unflagged branch, so the realized impact on that run was zero — but the mechanism is real and the report correctly said so.)The privacy gate's stated remedy — a per-branch
gh pr list --headfor each gated branch — is 102 manual network round-trips in the worst case observed. That is not a remedy anyone will execute.Also worth recording, since it is the obvious alternative and is wrong: the search API's
head:qualifier matches "pull requests opened from branch names beginning with the word" — prefix semantics that would treatfeature/auth-v2as merged becausefeature/authmerged. Only GraphQL'sheadRefNameargument is exact.Proposed change
merged-pr-window-truncated— a query that answers per-branch does not have a window to exhaust.merge-evidence-privacy-gated— the query transmits no local branch name that is not already a branch the tool is asking about on the operator's own repository, and the whole-repository form transmits none at all. If a privacy posture is still wanted, make it a configuration choice rather than a silent gate that leaves branches unverdicted.head:qualifier for branch matching.Acceptance criteria
feature/authandfeature/auth-v2never conflate.