fix(review): stop AI-review verdict-shopping — content-fingerprint stickiness + flip-count escalation - #9094
Conversation
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9094 +/- ##
==========================================
+ Coverage 93.88% 95.17% +1.28%
==========================================
Files 810 716 -94
Lines 80600 58151 -22449
Branches 24454 20584 -3870
==========================================
- Hits 75675 55345 -20330
+ Misses 3560 1617 -1943
+ Partials 1365 1189 -176
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Caution 🛑 LoopOver review result - fixes requiredReview updated: 2026-07-26 17:47:12 UTC
Review summary Nits — 6 non-blocking
CI checks failing
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
…ickiness + flip-count escalation The AI reviewer is non-deterministic, and the review cache keys purely on head SHA, so a contributor could force fresh re-rolls (a no-op recommit invalidating the cache key, or a same-head retry once the 30-minute non-cacheable cooldown lapses) until a lucky clean roll auto-merged a PR another roll had flagged as blocked. Two independent defenses: 1. getCachedAiReviewAcrossHeads (src/db/repositories.ts): a fallback used only when the exact-head lookup misses. The input fingerprint already hashes the actual per-file patch content, not just the head SHA, so an identical fingerprint under a different head means the reviewed content is genuinely unchanged -- reuse the prior verdict instead of spending an independently-random fresh roll on it. 2. A per-PR verdict-flip counter (migration 0183, src/review/ verdict-flip-guard.ts + verdict-flip-store.ts): every FRESH (non-cache-hit) verdict in block mode is compared against the PR's last fresh verdict; a flip is a change in whether the verdict had a blocking AI defect. Once flips clear a threshold, the gate holds for a human instead of trusting the newest roll, regardless of what it says. Fixed a pre-existing test (#ops-review-burst) whose fixture returned identical patch content across two different head SHAs -- exactly the no-op-recommit case fix #1 is designed to reuse, which made its own aiCalls assertion test the wrong thing; it now varies real content between heads and stays a valid regression for the ORIGINAL concern (a genuinely new commit is never suppressed).
586cbd8 to
dc768f2
Compare
…stale migration-number comment loopover_ai_review_verdict_flip_escalated_total was emitted (processors.ts) with no DEFAULT_METRIC_META entry, failing the completeness drift guard. Also corrects verdict-flip-store.ts's header comment, which still said "migration 0182" -- the ai_review_verdict_flips migration was bumped to 0183 after this branch rebased past #9090's 0181->0182 renumbering, but the comment was never updated.
Closes #9016.
Security advisory: https://github.com/JSONbored/loopover/security/advisories/GHSA-5w8q-r748-9j9c
The AI reviewer is non-deterministic even at temperature 0, and the review cache keys purely on head SHA — so a contributor could force fresh re-rolls (a no-op recommit invalidating the cache key, or a same-head retry once the 30-minute non-cacheable cooldown lapses) until a lucky CLEAN roll auto-merged a PR another roll had flagged as blocked.
Fix 1 — content-fingerprint stickiness across head SHAs (
getCachedAiReviewAcrossHeadsinsrc/db/repositories.ts): a fallback used only when the exact-head cache lookup misses.aiReviewCacheInputFingerprintalready hashes the actual per-file PATCH content (not just paths), so an identical fingerprint under a DIFFERENT head SHA means the reviewed content is genuinely unchanged — this reuses the prior verdict instead of spending a fresh, independently-random roll on content the reviewer already judged. Same cacheable/recency rules as the exact-head lookup (durable rows reuse unboundedly; dynamic-context rows only within the existing cooldown, never once published).Fix 2 — per-PR verdict-flip escalation (migration 0183,
src/review/verdict-flip-guard.tspure +verdict-flip-store.tsIO): every FRESH (non-cache-hit) verdict in block mode is compared against the PR's last fresh verdict — a flip is a change in whether the verdict carried a blocking AI-judgment defect (ai_consensus_defect/ai_review_split). Repeating the same verdict never adds to the count (ordinary re-review isn't abuse); once flips clear the threshold (3), the gate holds for a human viaai_review_inconclusive, regardless of what the newest roll says — closing the exact exploit ("retry until blockers come back empty").Regression fix along the way: the pre-existing
#ops-review-burst: a genuinely new commit...test had a fixture that returned IDENTICAL patch content across two different head SHAs — exactly the no-op-recommit case fix #1 is designed to reuse, which made its ownaiCallsassertion test the wrong thing once this PR landed. Fixed by varying real content between heads (viaupsertPullRequestFile, matching how a real webhook-driven detail-sync would refresh storage) — it now correctly stays a valid regression for its ORIGINAL concern.Tests: 15 pure/store/repository tests (every flip-state transition including the exploit's own defect→clean→defect→clean oscillation pattern, the fingerprint-fallback's cacheable/non-cacheable/scope/recency/malformed-date/future-date/no-options edge cases, D1 fail-open on both read and write), plus an end-to-end queue test proving a pre-seeded threshold PR's next clean roll is genuinely held (
ai_review_inconclusiveingate_outcomes, the audit event recorded, the metric incremented, the PR never merged) and an advisory-mode test proving the mechanism never engages outside block mode. Changed-line coverage: repositories.ts and the pure/store modules are fully covered; two defensive fail-open.catch()arms in the wiring (an infra-failure guard around the fingerprint fallback, and around the escalation's own audit-event write) remain uncovered — consistent with dozens of similarly-guarded, untested bare catches already throughout this exact file (e.g. lines 2003, 2392, 2658, 2777, 3054) — forcing them without destabilizing the surrounding webhook harness proved impractical within scope; flagging for visibility rather than leaving unaddressed.