From ab729bbb0320e9005581d73f9250a8001e6cb30e Mon Sep 17 00:00:00 2001 From: Jeff <158072326+jeffrey701@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:19:00 +0200 Subject: [PATCH] fix(engine): dedup provider-track-record aggregation per provider+target --- .../src/calibration/provider-track-record.ts | 10 +++++++++- test/unit/provider-track-record-engine.test.ts | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/loopover-engine/src/calibration/provider-track-record.ts b/packages/loopover-engine/src/calibration/provider-track-record.ts index 57e9e1a212..f8faf9b9f6 100644 --- a/packages/loopover-engine/src/calibration/provider-track-record.ts +++ b/packages/loopover-engine/src/calibration/provider-track-record.ts @@ -100,9 +100,17 @@ export function computeProviderTrackRecords( stances.set(signal.provider, signal.vote === "fail"); } + // #8876: collapse repeated (provider, targetKey) signals to the LATEST one before aggregating. A provider + // re-reviewing the same PR (common after a new push) emits another audit row, and loadLiveProviderTrackRecords + // reads raw rows with no dedup — so iterating `signals` directly counted signals/decided/shared/consensus once + // per row rather than once per distinct (provider, targetKey) pair, inflating that provider's precision and + // agreement stats. Keep the last row per pair, matching stancesByTarget's own latest-vote `.set` dedup above. + const latestByProviderTarget = new Map(); + for (const signal of signals) latestByProviderTarget.set(`${signal.provider} ${signal.targetKey}`, signal); + const perRepo = new Map>(); // provider → repo → stats const overall = new Map(); - for (const signal of signals) { + for (const signal of latestByProviderTarget.values()) { let repos = perRepo.get(signal.provider); if (repos === undefined) { repos = new Map(); diff --git a/test/unit/provider-track-record-engine.test.ts b/test/unit/provider-track-record-engine.test.ts index 87b30498bf..6fbd48ed54 100644 --- a/test/unit/provider-track-record-engine.test.ts +++ b/test/unit/provider-track-record-engine.test.ts @@ -63,6 +63,22 @@ describe("computeProviderTrackRecords (#8228)", () => { expect(aRepo).toMatchObject({ signals: aOverall.signals, decided: aOverall.decided, precision: aOverall.precision }); }); + // #8876: a provider re-reviewing the same PR (a second audit row after a push) must count ONCE per + // (provider, targetKey), scored on the LATEST vote — not inflate its signals/precision/agreement per raw row. + it("collapses a provider's repeated same-target signals to the latest, not double-counting (#8876)", () => { + const records = computeProviderTrackRecords( + [ + signal("solo", "acme/widgets#1", "pass"), // first review + signal("solo", "acme/widgets#1", "fail"), // re-review after a new push — the latest vote wins + ], + [labeled("acme/widgets#1", "confirmed")], + ); + const overall = records.find((r) => r.provider === "solo" && r.repoFullName === null)!; + // Before the fix this was signals: 2 / agreementRate: 0.5 (both raw rows counted); now one distinct pair, + // scored on the latest fail vote against the confirmed firing. + expect(overall).toMatchObject({ signals: 1, decided: 1, confirmed: 1, precision: 1, agreementRate: 1 }); + }); + it("keeps a one-provider corpus's consensus/split rates null — no shared targets, no consensus to measure", () => { const records = computeProviderTrackRecords( [signal("solo", "acme/widgets#1", "fail"), signal("solo", "acme/widgets#2", "pass")],