Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/review/submitter-reputation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,14 @@ export function signalFromCounts(c: ReputationCounts, cfg: ReputationConfig = DE
const weightedFails = c.qualityFail + c.qualityFailLight * cfg.lightFailWeight;
// The quality-relevant sample (excludes conflicts/out-of-band/manual — those never reach here).
const sample = c.success + c.qualityFail + c.qualityFailLight + c.promptInjection;
if (sample < cfg.minSample) return "neutral";

// ── 'low' — genuine malice: ANY prompt-injection (the single hard-abuse signal). ──
// ── 'low' — genuine malice: ANY prompt-injection (the single hard-abuse signal). This is an
// unconditional hard override, so it precedes the minSample guard below: a brand-new, low-history
// account attempting a single prompt injection is precisely the worst case it exists to catch, and
// it must not be masked by the small-sample "neutral" shortcut. ──
if (c.promptInjection > 0) return "low";

if (sample < cfg.minSample) return "neutral";
// ── 'low' — serial quality-failure: a high genuine-fail rate AND very few successes. A high-volume
// contributor with a healthy number of recent merges fails this (success guard) and stays 'neutral'. The
// soft signals (duplicates/unfetchable) only count at half weight here, so they can't brand alone. ──
Expand Down
6 changes: 6 additions & 0 deletions test/unit/submitter-reputation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ describe("signalFromCounts — generous, quality-weighted, recency-aware (#reput
it("any single prompt-injection (over a sufficient sample) → low", () => {
expect(signalOf(["closed", "source_prompt_injection", 1], ["closed", "dual_review_declined", 4])).toBe("low");
});
it("a single prompt-injection from a LOW-sample account → low (hard override precedes minSample, #5940)", () => {
// sample = 1 < minSample (5): the prompt-injection hard override must still fire. A brand-new,
// low-history account attempting a single injection is the worst case — previously the small-sample
// "neutral" shortcut masked it and returned "neutral".
expect(signalOf(["closed", "source_prompt_injection", 1])).toBe("low");
});
it("a serial quality-failure history with very few successes → low", () => {
// 1 success, 7 genuine declines: failRate 7/8 = 0.875 >= 0.7 AND success < 2 → low.
expect(signalOf(["merged", "dual_review_approved", 1], ["closed", "dual_review_declined", 7])).toBe("low");
Expand Down