From a09d42484a7d570d9a143eab2b78b1f540adc86b Mon Sep 17 00:00:00 2001 From: Jesse Merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:14:12 +1000 Subject: [PATCH] fix: require six times P99 for sustained signals (#3204) --- convex/lib/publisherAbuseScoring.test.ts | 29 +++++++++++++---------- convex/lib/publisherAbuseScoring.ts | 5 ++-- convex/publisherAbuseTemporalScan.test.ts | 6 ++--- specs/security-moderation.md | 8 +++---- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/convex/lib/publisherAbuseScoring.test.ts b/convex/lib/publisherAbuseScoring.test.ts index 4a5df9c0..a947e63f 100644 --- a/convex/lib/publisherAbuseScoring.test.ts +++ b/convex/lib/publisherAbuseScoring.test.ts @@ -449,24 +449,27 @@ describe("publisher abuse scoring", () => { expect(score.reasonCodes).toContain("temporal_download_spike_flat_installs"); }); - it("flags sustained high downloads with flat installs", () => { + it("flags sustained flat-install traffic one download above six times platform P99", () => { const todayDay = 100; const score = computeCurrentSkillTemporalAbuseScore({ todayDay, benchmark: temporalBenchmark({ - downloads30dP95: 3_000, - downloads30dP99: 3_000, + downloads30dP95: 284, + downloads30dP99: 600, spikeMultiplier7dP95: 20, spikeMultiplier7dP99: 50, }), - dailyStats: dailyRange(71, 30, { downloads: 120, installs: 0 }), + dailyStats: [ + ...dailyRange(71, 29, { downloads: 120, installs: 0 }), + { day: 100, downloads: 121, installs: 0 }, + ], }); expect(score.spike).toBe(false); expect(score.sustained).toBe(true); - expect(score.recent30Downloads).toBe(3_600); + expect(score.recent30Downloads).toBe(3_601); expect(score.recent30Installs).toBe(0); - expect(score.downloadInstallRatio30).toBe(3_600); + expect(score.downloadInstallRatio30).toBe(3_601); expect(score.downloads30dCohortBand).toBe("p99"); expect(score.reasonCodes).toContain("temporal_sustained_downloads_flat_installs"); }); @@ -486,17 +489,17 @@ describe("publisher abuse scoring", () => { expect(score.downloads30dCohortBand).toBeUndefined(); }); - it("keeps sub-3000 P99 download traffic below the absolute review floor", () => { + it("keeps downloads at exactly six times platform P99 below the sustained threshold", () => { const score = computeCurrentSkillTemporalAbuseScore({ todayDay: 100, benchmark: temporalBenchmark({ - downloads30dP95: 1_000, - downloads30dP99: 2_000, + downloads30dP95: 284, + downloads30dP99: 600, }), - dailyStats: dailyRange(71, 30, { downloads: 90, installs: 0 }), + dailyStats: dailyRange(71, 30, { downloads: 120, installs: 0 }), }); - expect(score.recent30Downloads).toBe(2_700); + expect(score.recent30Downloads).toBe(3_600); expect(score.sustained).toBe(false); expect(score.downloads30dCohortBand).toBeUndefined(); }); @@ -611,8 +614,8 @@ describe("publisher abuse scoring", () => { it("finds historical spike and sustained windows for backfill scans", () => { const score = computeHistoricalSkillTemporalAbuseScore({ benchmark: temporalBenchmark({ - downloads30dP95: 3_000, - downloads30dP99: 10_000, + downloads30dP95: 284, + downloads30dP99: 1_000, spikeMultiplier7dP95: 5, spikeMultiplier7dP99: 25, }), diff --git a/convex/lib/publisherAbuseScoring.ts b/convex/lib/publisherAbuseScoring.ts index 3acf837d..7f4ebaf6 100644 --- a/convex/lib/publisherAbuseScoring.ts +++ b/convex/lib/publisherAbuseScoring.ts @@ -128,7 +128,7 @@ const TEMPORAL_SUSTAINED_DAYS = 30; const TEMPORAL_MAX_SPIKE_INSTALLS = 2; const TEMPORAL_MAX_SUSTAINED_INSTALLS = 5; const TEMPORAL_MIN_SPIKE_7_DOWNLOADS = 2_000; -const TEMPORAL_MIN_SUSTAINED_30_DOWNLOADS = 3_000; +const TEMPORAL_SUSTAINED_DOWNLOADS_P99_MULTIPLIER = 6; const TEMPORAL_MIN_BASELINE_7_DOWNLOADS = 100; const TEMPORAL_MIN_NEAR_CONVERSION_7_DOWNLOADS = 500; const TEMPORAL_MIN_NEAR_CONVERSION_30_DOWNLOADS = 1_000; @@ -437,7 +437,8 @@ export function classifySkillTemporalAbuseScore( score.spikeMultiplier / Math.max(1, benchmark.spikeMultiplier7dP95); const downloads30dCohortBand = score.recent30Installs <= TEMPORAL_MAX_SUSTAINED_INSTALLS && - score.recent30Downloads >= TEMPORAL_MIN_SUSTAINED_30_DOWNLOADS + score.recent30Downloads > + benchmark.downloads30dP99 * TEMPORAL_SUSTAINED_DOWNLOADS_P99_MULTIPLIER ? p99Band({ value: score.recent30Downloads, p99: benchmark.downloads30dP99 }) : undefined; const spikeMultiplierCohortBand = diff --git a/convex/publisherAbuseTemporalScan.test.ts b/convex/publisherAbuseTemporalScan.test.ts index 5b55a326..dbc2ef0e 100644 --- a/convex/publisherAbuseTemporalScan.test.ts +++ b/convex/publisherAbuseTemporalScan.test.ts @@ -655,8 +655,8 @@ describe("scheduled temporal publisher abuse scan", () => { sampleSize: 1_000, downloads30dAverage: 180, downloads30dMedian: 45, - downloads30dP95: 900, - downloads30dP99: 3_000, + downloads30dP95: 284, + downloads30dP99: 600, spikeMultiplier7dP95: 4, spikeMultiplier7dP99: 12, }; @@ -667,7 +667,7 @@ describe("scheduled temporal publisher abuse scan", () => { }); const candidate = temporalCandidate( "skills:anysearch" as Id<"skills">, - temporalScore({ recent30Downloads: 3_370, recent30Installs: 4 }), + temporalScore({ recent30Downloads: 3_700, recent30Installs: 4 }), ); const runQuery = vi .fn() diff --git a/specs/security-moderation.md b/specs/security-moderation.md index b46ab84d..a85a99d0 100644 --- a/specs/security-moderation.md +++ b/specs/security-moderation.md @@ -94,10 +94,10 @@ See also: [acceptable-usage.md](./acceptable-usage.md) for the marketplace polic Partial scans must not archive signals or present their top-download slice as a platform percentile. - Flat-install temporal review signals are deliberately high-confidence: - sustained volume must exceed the platform P99, reach at least 3,000 downloads - in 30 days, and have at most 5 installs; a spike must exceed the platform P99, - reach at least 2,000 downloads in 7 days, and have at most 2 installs. These - signals indicate anomalous traffic for manual review, not publisher attribution. + sustained volume must exceed six times the platform 30-day download P99 and + have at most 5 installs; a spike must exceed the platform P99, reach at least + 2,000 downloads in 7 days, and have at most 2 installs. These signals indicate + anomalous traffic for manual review, not publisher attribution. - Publisher abuse scoring must skip staff-linked and official publishers before nominations are created. Publisher abuse autoban must process pending `potential_ban_candidate` pressure nominations without waiting for the score