Problem
submitter_stats.submissions does not count submissions — it counts webhook passes — and it has no
per-PR idempotency key, so any third party can inflate an arbitrary contributor's counter.
recordSubmissionOutcome (src/review/submitter-reputation.ts:265-279) blindly increments:
submissions = submissions + 1, <col> = <col> + 1
There is no pull_number in the key. It is invoked from the public-surface block of
handlePullRequestWebhookEvent on every pass, and reputationOutcomeFromTerminalState returns
"manual" for a still-open PR whenever gate.conclusion === "failure" || "action_required".
That block runs for opened|reopened|synchronize|ready_for_review|edited|closed and for
pull_request_review, pull_request_review_comment, and pull_request_review_thread
(src/queue/processors.ts:7353-7370 — handlePullRequestWebhookEvent is entered for any payload
carrying pull_request).
The rule that consumes it is stats.submissions >= 8 && stats.merged < 1
(src/review/reputation-wire.ts:47-48, :61).
Two triggers
Self-inflicted. A contributor's first PR is held. Each body edit or push re-gates it and adds a
submissions + manual. After 8 passes they are burst-flagged on that repo — permanently, since
submitter_stats is all-time with no decay and the only documented exit is a merge they can no longer
easily get.
Adversarial. Any account can post review comments on a rival's held PR. Each comment re-gates the
PR and increments the rival's counter. Eight comments is a denial-of-review weapon against an arbitrary
contributor, keyed on a counter a stranger drives.
Live evidence
#9015's own live dump records dale053 with 12 real PRs on JSONbored/loopover but 21 recorded
submissions. The drift is already present in production data, and #9015 treated the counter as
authoritative when reasoning about the burst rule.
Impact
Post-#9041 a burst-flagged author's PRs get an ai_review_inconclusive hold rather than a skip — which
is the correct fail-closed direction, and it converts this into a targeted stall weapon. The contributor
cannot clear it, and no surface explains why.
Dedup
Distinct from #9015 (closed). That issue diagnosed the burst rule's fail-open direction and cited
this counter as ground truth ("21 subs"). It never asked whether submissions counts submissions.
#9015's fix does not touch recordSubmissionOutcome.
Requirements
- Key the outcome record on
(project, submitter, pull_number, outcome) so it is idempotent per PR.
- Stop recording
"manual" for non-terminal PRs, or record it in a separate non-cumulative column — a
still-open PR is not a submission outcome.
- Do not re-gate reputation from
pull_request_review* events at all; a third party's comment is not a
submission signal.
- Window and decay
submitter_stats, so a burst state is recoverable without a merge.
- Recompute the existing rows from the live
pull_requests ledger and record how many contributors were
wrongly flagged.
Test Coverage Requirements
99%+ patch coverage, branch-counted. Regression tests: N re-gates of one PR yield exactly one submission;
a review comment by a third party yields zero.
Links & Resources
maintainer-only — anti-abuse and contributor-fairness authority.
Problem
submitter_stats.submissionsdoes not count submissions — it counts webhook passes — and it has noper-PR idempotency key, so any third party can inflate an arbitrary contributor's counter.
recordSubmissionOutcome(src/review/submitter-reputation.ts:265-279) blindly increments:There is no
pull_numberin the key. It is invoked from the public-surface block ofhandlePullRequestWebhookEventon every pass, andreputationOutcomeFromTerminalStatereturns"manual"for a still-open PR whenevergate.conclusion === "failure" || "action_required".That block runs for
opened|reopened|synchronize|ready_for_review|edited|closedand forpull_request_review,pull_request_review_comment, andpull_request_review_thread(
src/queue/processors.ts:7353-7370—handlePullRequestWebhookEventis entered for any payloadcarrying
pull_request).The rule that consumes it is
stats.submissions >= 8 && stats.merged < 1(
src/review/reputation-wire.ts:47-48,:61).Two triggers
Self-inflicted. A contributor's first PR is held. Each body edit or push re-gates it and adds a
submissions+manual. After 8 passes they are burst-flagged on that repo — permanently, sincesubmitter_statsis all-time with no decay and the only documented exit is a merge they can no longereasily get.
Adversarial. Any account can post review comments on a rival's held PR. Each comment re-gates the
PR and increments the rival's counter. Eight comments is a denial-of-review weapon against an arbitrary
contributor, keyed on a counter a stranger drives.
Live evidence
#9015's own live dump records
dale053with 12 real PRs onJSONbored/loopoverbut 21 recordedsubmissions. The drift is already present in production data, and #9015 treated the counter as
authoritative when reasoning about the burst rule.
Impact
Post-#9041 a burst-flagged author's PRs get an
ai_review_inconclusivehold rather than a skip — whichis the correct fail-closed direction, and it converts this into a targeted stall weapon. The contributor
cannot clear it, and no surface explains why.
Dedup
Distinct from #9015 (closed). That issue diagnosed the burst rule's fail-open direction and cited
this counter as ground truth ("21 subs"). It never asked whether
submissionscounts submissions.#9015's fix does not touch
recordSubmissionOutcome.Requirements
(project, submitter, pull_number, outcome)so it is idempotent per PR."manual"for non-terminal PRs, or record it in a separate non-cumulative column — astill-open PR is not a submission outcome.
pull_request_review*events at all; a third party's comment is not asubmission signal.
submitter_stats, so a burst state is recoverable without a merge.pull_requestsledger and record how many contributors werewrongly flagged.
Test Coverage Requirements
99%+ patch coverage, branch-counted. Regression tests: N re-gates of one PR yield exactly one submission;
a review comment by a third party yields zero.
Links & Resources
src/review/submitter-reputation.ts~265-279;src/queue/processors.ts~7353-7370, ~8133-8145, ~6862-6876;src/review/reputation-wire.ts~47-48, ~59-66maintainer-only — anti-abuse and contributor-fairness authority.