Skip to content

pr-review has no false-negative metric and no production feedback loop — accepted defects found by advisory bots after approval are never counted or learned from #1596

Closed

Description

@don-petry

The gap

We measure pr-review's operational health and its false positives. We do not measure its false negatives, and nothing feeds them back into the improvement loop.

What exists today:

surface measures does not measure
scripts/pr_review_health.sh (daily-pr-review-health.yml) run outcomes, cancellation rate, sweep hit-rate review quality at all
scripts/reviewer_report.sh / #1220 scorecard third-party bots' own activity: review counts, thread resolution, reactions any comparison against pr-review's verdict on the same PR
evals/deep-review/ + #1088 / #1094 / #1107 held-out synthetic cases, llm-judge, and explicitly the false-positive rate defects pr-review approved past in production

So the one signal that is both free and perfectly labelled — a third-party bot found a real defect on a PR pr-review had already approved — evaporates. It is never counted, never reported, and never becomes an eval case.

Evidence from a single day (2026-08-30/31)

Every one of these is a real, accepted defect that pr-review did not raise:

PR found by defect pr-review's state at the time
.github#995 graphite-app (08-21) and CodeRabbit (08-30), independently, 9 days apart ADR asserted a "rolling" 5-hour window while also asserting utilization is monotonic and only resets_at resumes — a self-contradiction that would have shipped as the acceptance criterion for #641/#994 had reviewed; never flagged it
.github#1017 codeant-ai contract test compared jq's rendered text, so "50" passed as an integer and "true" as a boolean — in the very test whose stated job is enforcing types approved
.github#1017 CodeRabbit config's own _exempt_note claims the exempt set is "the SAME set as standards/pr-limits.json", but nothing enforced it — silent drift between two gate policies APPROVED at 22:32, defect raised at 22:40
#1556 codeant-ai missing/placeholder Copilot token returns status 1, indistinguishable from a rate limit → deterministic auth failure classified as retryable (now #1591) approved
#1558 codeant-ai orphan-rescue sets force_review=true, which bypasses all CI/advisory gates, not just the idempotency marker (now #1590) approved
#1559 codeant-ai metadata-digest re-review path takes no atomic claim; two triggers can both launch the cascade (now #1589) approved

Six real findings, five distinct PRs, three third-party bots — in one day. Three of them were substantive enough that they are now tracked issues.

Is pr-review approving defective PRs a bug?

Partly, and the honest answer matters for what to fix.

Not a bug: the architecture deliberately delegates depth to advisory bots (there is an advisory-review gate that waits for them) and required_review_thread_resolution: true means late findings still block the merge. On #1017 that backstop worked — the defect was raised after approval and the ruleset held the PR. So the system caught it even though the agent did not.

Genuinely a bug, in two specific places:

  1. The advisory gate proceeds on partial bot coverage. Observed verbatim on .github#995:

    [advisory-gate] Only 2/3 required bots submitted; head is 1966s old — timeout fallback, proceeding
    

    pr-review approved having seen 2 of 3 advisory opinions. The timeout is a liveness protection, but it silently converts "all reviewers agreed" into "the ones that answered agreed", with no record that the verdict was taken on partial evidence.

  2. Approval is never re-evaluated when a later finding lands. pr-review's approval on #1017 remained attached while CodeRabbit was raising a real defect. Only the ruleset stopped the merge. If thread-resolution enforcement were ever relaxed — or the finding arrived as a comment rather than a review thread — an approved-and-defective PR merges.

The deeper issue is that an approval from pr-review is currently read as evidence of defect-freedom, and it is not — it is an orchestration signal. Nothing quantifies how much weight it actually deserves.

Proposal

1. Measure the miss rate

Extend the existing deterministic pipeline (no LLM needed for the metric itself — reviewer_report.sh already reads reviews, threads and resolution state in one GraphQL round-trip per repo). For each PR, emit:

  • missed_findings — threads opened by a trusted advisory bot after pr-review's approving review on the same head SHA, that were subsequently resolved as accepted (i.e. a fix landed or the disposition accepted it) rather than refuted.
  • caught_findings — findings pr-review raised itself that were accepted.
  • partial_evidence_approvals — approvals issued via the advisory-gate timeout fallback, i.e. before all registered bots reported.

Report per week alongside the #1220 scorecard, and per reviewer, so we can see which bot is catching what pr-review misses. That is directly actionable: a class of defect one bot repeatedly finds first is a prompt gap.

Distinguishing accepted-from-refuted is the crux. Roughly 3 of 4 codeant/gemini findings dispositioned this same day were false positives correctly refuted (yq claimed missing when the dedicated test workflow installs it; a registry assertion claimed unguarded when the adjacent test covers it). A naive "bot opened a thread → pr-review missed it" counter would be dominated by noise and would push the agent toward more false positives. Use the accepted/refuted disposition already recorded in the resolving reply, and treat anything ambiguous as not a miss.

2. Close the loop into the evals — dev split only

Harvest accepted misses into evals/deep-review/dev/cases.jsonl as regression cases (de-identified per evals/README.md).

Hard invariant, do not violate: harvested cases go to dev/ only. holdout/cases.jsonl is CODEOWNER-gated and holdout-guard.yml hard-fails proposer-authored PRs touching evals/; the whole point of the split is that the proposer must never see the cases it is scored against. Auto-harvesting into holdout/ would silently destroy the one control that makes #1088's success metric meaningful. Harvesting into dev/ is exactly the "merged-PR few-shot" gap #1088 already names — this issue supplies the source of truth for those cases instead of hand-authored synthetic ones.

3. Fix the two concrete holes

  • Record on the PR when an approval was issued under the advisory-gate timeout, and count it (partial_evidence_approvals). Consider requiring a re-check rather than a bare approval when coverage was partial.
  • When a trusted advisory bot opens an accepted finding after an approving review at the same head, dismiss or re-run that approval rather than leaving it standing on required_review_thread_resolution alone.

Acceptance criteria

  • missed_findings, caught_findings, partial_evidence_approvals computed deterministically from GitHub review data — no LLM in the metric path, matching reviewer_report.sh's stated design.
  • Accepted-vs-refuted classification is explicit, and ambiguous dispositions count as not a miss; the rule is documented and unit-tested with both a real miss and a refuted false positive.
  • Weekly report surfaces miss rate overall and per third-party reviewer.
  • Accepted misses are harvested into evals/deep-review/dev/cases.jsonl, de-identified, never into holdout/; a test asserts the harvester cannot write a holdout/ path.
  • An approval issued under the advisory-gate timeout is recorded as such on the PR and counted.
  • A post-approval accepted finding from a trusted bot invalidates the standing approval rather than relying solely on thread-resolution enforcement.
  • Baseline the miss rate over the trailing 30 days before any prompt change, so #1088's improvements can be measured against it.

Relationship to existing work — extends, does not duplicate

  • #1088 (world-class bug hunter) targets bug-finding depth and names "merged-PR few-shot — no learning from this repo's past review→merge histories" as gap 4, with a success metric of held-out eval score plus no false-positive regression. It has no false-negative metric and no production feedback path. This issue supplies both, and gives gap 4 a real case source. Treat as a companion/sub-issue.
  • #1220 measures third-party reviewers' activity, never pr-review's verdict against theirs.
  • #1094/#1107 gate on the held-out synthetic split; this is about production signal.
  • #1552 (merged) surfaced pr-review's decision/reason — the plumbing this metric can build on.

⚠️ CI steering for PR #1604 (added 2026-08-31 after two failed auto-fix passes)

Two distinct failures on #1604. They need different fixes; the first is a design decision, not a test to relax.

1. not ok 29 Advisory gate: script is minimal — [ "$lines" -lt 500 ] (tests/dev-lead/unit/test_advisory_review_gate.bats:172)

scripts/lib/advisory-review-gate.sh has a deliberate size guard: "non-blocking means fewer lines". Adding partial-evidence recording pushed it past 500.

Do NOT raise the threshold to make this pass. The guard encodes a real property — the advisory gate runs on every review pass and is deliberately kept small and non-blocking. Growing it is the thing the test is there to catch.

Instead, extract the partial-evidence recording into its own library — e.g. scripts/lib/review-miss-evidence.sh — and have the gate call it. That matches the pattern #1567 established with scripts/lib/review-change-evidence.sh (pure, unit-tested helpers in their own lib; the caller keeps only the plumbing). It also fixes failure 2 below for free, because the new tests can source the new lib directly.

2. not ok 702-705 Partial-evidence: … — source "$SCRIPT_DIR/lib/advisory-review-gate.sh" failed (tests/dev-lead/unit/test_partial_evidence_marker.bats:16)

The setup() sources a path that does not exist, so all four tests error before asserting anything. Point it at the real location of whichever lib ends up owning these helpers (see 1 — after the extraction it should source scripts/lib/review-miss-evidence.sh). Check SCRIPT_DIR resolution against a sibling unit test that already sources a scripts/lib/*.sh correctly rather than assuming the prefix.

Reminder on scope

Everything else on #1604 is green (49 checks). Keep the change to these two items; do not touch the metric semantics already agreed in this issue — in particular the accepted-vs-refuted classification and the dev/ split only, never holdout/ invariant both still stand.

Activity

  1. github-actions commented on Aug 31, 2026

    @github-actions
    Contributor

    Thanks for this thorough and well-evidenced writeup — the day-of examples and the careful distinction between system-level catches and agent-level misses make the gap very concrete. Before diving into implementation, the most important question is around the accepted/refuted classification rule: do you have a specific heuristic in mind for determining when a resolved thread counts as "accepted" (e.g. a follow-up commit landed on the same files, or the resolver explicitly acknowledged the finding), versus "refuted" — and is there an existing example in the current reviewer_report.sh output or GraphQL data that already exposes that signal, or would it need a new field?

  2. don-petry commented on Aug 31, 2026

    @don-petry
    CollaboratorAuthor

    Dev-Lead Implementation Plan

    Issue: #1596 — pr-review has no false-negative metric and no production feedback loop

    Scope

    Add a deterministic (no-LLM) false-negative pipeline: measure missed_findings / caught_findings / partial_evidence_approvals from the review data reviewer_report.sh already collects, surface the miss rate weekly (overall + per third-party reviewer), harvest accepted misses into evals/deep-review/dev/ only, record advisory-gate timeout approvals on the PR, and provide a mechanism to invalidate a standing approval when a trusted bot lands an accepted post-approval finding.

    Implementation Checklist

    • scripts/lib/pr-review-miss-rate.sh (new, pure-logic): accepted/refuted/ambiguous disposition classifier; per-PR miss metrics jq program (findings a trusted bot opened after pr-review's approving review, resolved-as-accepted); org-level aggregation (overall + per reviewer); markdown renderer; standing-approval-invalidation detector; partial-evidence + approval marker regexes.
    • scripts/lib/miss-harvest.sh (new, pure-logic): dev-path assertion that refuses any holdout/ path; de-identification (strip URLs/@mentions/token-like strings); accepted-miss → evals/deep-review/dev/cases.jsonl case builder + append.
    • scripts/lib/advisory-review-gate.sh: add advisory_partial_evidence_marker() + maybe_post_partial_evidence_marker(); set ADVISORY_PROCEED_REASON and stamp the marker on the PR when approval proceeds under the head-age/quiescence timeout fallback.
    • scripts/reviewer_report.sh: source the miss-rate lib; add an additive miss_pr collection pass (mirrors the [Phase 4] Capture the pre-rollout baseline for the three success metrics, including a new no-action-comment noise metric #1411 agent_comment pass); render a "Review miss rate" section in render_reviewer_report.
    • scripts/invalidate-standing-approval.sh (new): DRY_RUN-default wrapper over the pure detector (mirrors the repo's staged-write convention).
    • scripts/lib/README.md: document the metric, the accepted/refuted/ambiguous rule, the dev-only harvest invariant, the partial-evidence marker, approval invalidation, and the 30-day baseline runbook.
    • .github/workflows/lint.yml: add the new bats files to the bats job list.

    Tests to Write

    • tests/test_pr_review_miss_rate.bats: a real accepted miss counts; a refuted false positive does NOT; an ambiguous disposition does NOT; caught_findings and partial_evidence_approvals counting; per-reviewer attribution; invalidation detector.
    • tests/test_miss_harvest.bats: harvester cannot write a holdout/ path (asserts refusal); de-identification strips a URL/mention; appended line is valid JSON with an id.
    • tests/test_advisory_review_gate.bats: extend with partial-evidence marker string + reason.

    Test Command

    scripts/run-bats.sh tests/test_pr_review_miss_rate.bats tests/test_miss_harvest.bats tests/test_advisory_review_gate.bats tests/reviewer_report.bats and bash .dev-lead/scripts/dev-lead-lint.sh

  3. don-petry commented on Aug 31, 2026

    @don-petry
    CollaboratorAuthor

    Dev-Lead: Implementation Complete — PR #1604

    The implementation for issue #1596 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.

    Review happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.

  4. added 3 commits that reference this issue on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    dev-leadFor dev-lead agent pickupenhancementFeature requests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions