diff --git a/src/db/retention.ts b/src/db/retention.ts index 78033f38d5..1b69018f8b 100644 --- a/src/db/retention.ts +++ b/src/db/retention.ts @@ -118,6 +118,18 @@ export const LATEST_ONLY_SIGNAL_SNAPSHOT_TYPES = [ "contributor-evidence-graph", "contributor-outcome-history", "contributor-strategy", + // #8900: these eight writers also INSERT a fresh row every run (persistSignalSnapshot is not an + // upsert) while every consumer reads only index [0] / the latest row — same latest-only contract as + // the repo-* and contributor-intelligence types above. queue-health stays EXCLUDED (feeds + // buildQueueTrendReport as a real series). + "config-quality", + "label-audit", + "maintainer-lane", + "maintainer-cut-readiness", + "contributor-intake-health", + "issue-quality", + "repo-outcome-patterns", + "pr-reviewability", ] as const; /** diff --git a/test/unit/retention.test.ts b/test/unit/retention.test.ts index 7797b9a7d9..2d40c0f300 100644 --- a/test/unit/retention.test.ts +++ b/test/unit/retention.test.ts @@ -201,6 +201,39 @@ describe("dedupeSignalSnapshots", () => { expect(ids).toContain("pack-2"); }); + it("dedupes the eight latest-only signal types added in #8900 to one row per repo", async () => { + const env = createTestEnv(); + const types = [ + "config-quality", + "label-audit", + "maintainer-lane", + "maintainer-cut-readiness", + "contributor-intake-health", + "issue-quality", + "repo-outcome-patterns", + "pr-reviewability", + ] as const; + for (const signalType of types) { + await insertSignalSnapshot(env, `${signalType}-old`, signalType, "JSONbored/loopover", "2026-07-01T00:00:00.000Z"); + await insertSignalSnapshot(env, `${signalType}-mid`, signalType, "JSONbored/loopover", "2026-07-02T00:00:00.000Z"); + await insertSignalSnapshot(env, `${signalType}-new`, signalType, "JSONbored/loopover", "2026-07-03T00:00:00.000Z"); + await insertSignalSnapshot(env, `${signalType}-other`, signalType, "other/repo", "2026-07-03T00:00:00.000Z"); + } + + const results = await dedupeSignalSnapshots(env); + const byType = Object.fromEntries(results.map((r) => [r.signalType, r.deleted])); + for (const signalType of types) { + expect(byType[signalType]).toBe(2); // old + mid deleted; new kept; other/repo untouched + expect(await countSignalSnapshots(env, signalType)).toBe(2); // latest for loopover + other/repo + } + const remaining = await env.DB.prepare("SELECT id FROM signal_snapshots ORDER BY id").all<{ id: string }>(); + const ids = (remaining.results ?? []).map((row) => row.id); + expect(ids).toContain("pr-reviewability-new"); + expect(ids).toContain("pr-reviewability-other"); + expect(ids).not.toContain("pr-reviewability-old"); + expect(ids).not.toContain("pr-reviewability-mid"); + }); + it("dedupes private and public focus-manifest cache snapshots (regression for storage exhaustion)", async () => { const env = createTestEnv(); await insertSignalSnapshot(env, "private-old", REPO_FOCUS_MANIFEST_SIGNAL, "JSONbored/loopover", "2026-06-01T00:00:00.000Z");