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
11 changes: 11 additions & 0 deletions src/db/retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ export const LATEST_ONLY_SIGNAL_SNAPSHOT_TYPES = [
"repo-doc-refresh-attempt",
"repo-focus-manifest",
"repo-public-focus-manifest",
// 2026-07-23 recurrence of #3810, new offenders: the contributor-intelligence writers (processors.ts's
// scoring pass) append one ~36KB row PER CONTRIBUTOR PER PASS for these three types — ~6GB in three
// weeks at current review volume, refilling D1's 10GB cap before the 90-day age window could ever
// engage. No reader consumes them as a series (the canonical latest lives in the dedicated
// contributor_evidence / contributor_scoring_profiles upsert tables; nothing calls
// listSignalSnapshots for contributor-* types), so latest-only is lossless for every actual consumer.
// contributor-decision-pack stays EXCLUDED: the retention doc above records it as a bounded
// trend/change series by design, and its volume is a fraction of these three.
"contributor-evidence-graph",
"contributor-outcome-history",
"contributor-strategy",
] as const;

/**
Expand Down
26 changes: 26 additions & 0 deletions test/unit/retention.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,32 @@ describe("dedupeSignalSnapshots", () => {
expect(remaining?.id).toBe("s-3");
});

it("dedupes the contributor-intelligence types to latest-per-contributor (2026-07 recurrence of #3810: ~6GB in three weeks)", async () => {
const env = createTestEnv();
for (const signalType of ["contributor-evidence-graph", "contributor-outcome-history", "contributor-strategy"] as const) {
await insertSignalSnapshot(env, `${signalType}-old`, signalType, "octocat", "2026-07-01T00:00:00.000Z");
await insertSignalSnapshot(env, `${signalType}-new`, signalType, "octocat", "2026-07-02T00:00:00.000Z");
await insertSignalSnapshot(env, `${signalType}-other`, signalType, "hubot", "2026-07-02T00:00:00.000Z");
}
// decision-pack is a bounded trend series by design — must remain untouched.
await insertSignalSnapshot(env, "pack-1", "contributor-decision-pack", "octocat", "2026-07-01T00:00:00.000Z");
await insertSignalSnapshot(env, "pack-2", "contributor-decision-pack", "octocat", "2026-07-02T00:00:00.000Z");

const results = await dedupeSignalSnapshots(env);
const byType = Object.fromEntries(results.map((r) => [r.signalType, r.deleted]));
expect(byType["contributor-evidence-graph"]).toBe(1);
expect(byType["contributor-outcome-history"]).toBe(1);
expect(byType["contributor-strategy"]).toBe(1);

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("contributor-strategy-new");
expect(ids).toContain("contributor-strategy-other");
expect(ids).not.toContain("contributor-strategy-old");
expect(ids).toContain("pack-1"); // series preserved
expect(ids).toContain("pack-2");
});

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");
Expand Down
5 changes: 4 additions & 1 deletion test/unit/selfhost-d1-size-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,10 @@ describe("fetchD1TableRowCount", () => {
calls.push({ url: requestUrl(input), method: init?.method ?? "GET" });
const body = JSON.parse(String(init?.body ?? "{}")) as { sql: string; params: string[] };
capturedParams = body.params;
expect(body.sql).toContain("signal_type IN (?1, ?2, ?3, ?4)");
// Derived from the list itself so growing LATEST_ONLY_SIGNAL_SNAPSHOT_TYPES (as the #3810
// recurrences keep doing) can never silently desync this pin from the probe's real SQL.
const expectedPlaceholders = LATEST_ONLY_SIGNAL_SNAPSHOT_TYPES.map((_, index) => `?${index + 1}`).join(", ");
expect(body.sql).toContain(`signal_type IN (${expectedPlaceholders})`);
return new Response(envelope([{ results: [{ total: 20225, dedup_total: 107, dedup_distinct_keys: 36 }], success: true, meta: {} }]));
};
const row = await fetchD1TableRowCount(config, "signal_snapshots", fetchImpl);
Expand Down