diff --git a/src/review/submitter-reputation.ts b/src/review/submitter-reputation.ts index f773be09a4..369169f5d1 100644 --- a/src/review/submitter-reputation.ts +++ b/src/review/submitter-reputation.ts @@ -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. ── diff --git a/test/unit/submitter-reputation.test.ts b/test/unit/submitter-reputation.test.ts index fe7985f4de..15a7c66acd 100644 --- a/test/unit/submitter-reputation.test.ts +++ b/test/unit/submitter-reputation.test.ts @@ -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");