Problem
predicted_gate_calls writes one un-deduped row per call, and every one of those rows pairs against the
same eventual real decision — so the predicted-vs-live agreement metric can be driven to ~100% by the
contributor it measures.
computePredictedGateVerdict (src/mcp/server.ts:4546) — backing both loopover_predict_gate and
loopover_explain_gate_disposition — ends with recordPredictedGateCall(...), which states in-code
(src/review/predicted-gate-calls.ts:64-71) "Every call gets its own row (no dedup key)" and inserts
with a random id.
computePredictedGateAgreement (src/review/predicted-gate-agreement.ts:70-76) then pairs them, and
documents the many-to-one relationship as intentional:
Multiple predicted calls before one real PR (a contributor iterating on title/scope) all pair against
that same eventual decision … so none are dropped as duplicates.
Neither tool calls enforceToolRateLimit. That per-tool 20-per-300s cap (src/mcp/server.ts:4363) is
applied to only nine tools — all of them cheaper than this one and none of them writing a row. The only
ceiling here is the shared normal route class for /mcp: 120/min (src/auth/rate-limit.ts:25).
Trigger
A contributor calls loopover_predict_gate for their own login on an accessible repo 500 times in ~5
minutes (comfortably within 120/min), then opens one PR the real gate merges. All 500 predicted merge
rows pair against that single real merge:
pairedSamples: 500, bothMerge: 500, agreementRate: 1.0, unsafeDisagreements: 0
MIN_PAIRED_SAMPLE = 30 (predicted-gate-agreement.ts:56) is crossed by one contributor in under a
second, flipping hasSignal to true.
Impact
The maintainer-facing measure of "can the predictor be trusted" is controllable by the single party with
an incentive to inflate it, and the safety counter (unsafeDisagreements) is diluted to noise by the
same mechanism. Secondary: unbounded contributor-driven growth of predicted_gate_calls, which #9083
already flags as a table class with no usable retention index.
Requirements
- Dedupe before pairing: keep only the latest prediction per
(login, project) window, or dedupe on
(login, project, predicted_action, date). One contributor iterating should contribute one paired
sample, not N.
- Add
enforceToolRateLimit("loopover_predict_gate") inside computePredictedGateVerdict so both tools
share the sibling tools' cap.
- Re-derive the currently published agreement figure after dedup and state whether the prior number was
computed over an inflated sample.
- Give
predicted_gate_calls a retention path.
Test Coverage Requirements
99%+ patch coverage, branch-counted. Regression test: N predictions before one real decision yield
exactly one paired sample.
Links & Resources
maintainer-only — metric integrity.
Problem
predicted_gate_callswrites one un-deduped row per call, and every one of those rows pairs against thesame eventual real decision — so the predicted-vs-live agreement metric can be driven to ~100% by the
contributor it measures.
computePredictedGateVerdict(src/mcp/server.ts:4546) — backing bothloopover_predict_gateandloopover_explain_gate_disposition— ends withrecordPredictedGateCall(...), which states in-code(
src/review/predicted-gate-calls.ts:64-71) "Every call gets its own row (no dedup key)" and insertswith a random id.
computePredictedGateAgreement(src/review/predicted-gate-agreement.ts:70-76) then pairs them, anddocuments the many-to-one relationship as intentional:
Neither tool calls
enforceToolRateLimit. That per-tool 20-per-300s cap (src/mcp/server.ts:4363) isapplied to only nine tools — all of them cheaper than this one and none of them writing a row. The only
ceiling here is the shared
normalroute class for/mcp: 120/min (src/auth/rate-limit.ts:25).Trigger
A contributor calls
loopover_predict_gatefor their own login on an accessible repo 500 times in ~5minutes (comfortably within 120/min), then opens one PR the real gate merges. All 500 predicted
mergerows pair against that single real
merge:MIN_PAIRED_SAMPLE = 30(predicted-gate-agreement.ts:56) is crossed by one contributor in under asecond, flipping
hasSignaltotrue.Impact
The maintainer-facing measure of "can the predictor be trusted" is controllable by the single party with
an incentive to inflate it, and the safety counter (
unsafeDisagreements) is diluted to noise by thesame mechanism. Secondary: unbounded contributor-driven growth of
predicted_gate_calls, which #9083already flags as a table class with no usable retention index.
Requirements
(login, project)window, or dedupe on(login, project, predicted_action, date). One contributor iterating should contribute one pairedsample, not N.
enforceToolRateLimit("loopover_predict_gate")insidecomputePredictedGateVerdictso both toolsshare the sibling tools' cap.
computed over an inflated sample.
predicted_gate_callsa retention path.Test Coverage Requirements
99%+ patch coverage, branch-counted. Regression test: N predictions before one real decision yield
exactly one paired sample.
Links & Resources
src/mcp/server.ts~4363, ~4546;src/review/predicted-gate-calls.ts~64-71;src/review/predicted-gate-agreement.ts~56, ~70-76, ~134-150;src/auth/rate-limit.ts~25maintainer-only — metric integrity.