From d5c5716e30cfc4f7d49a7b93f3beca5d82993d64 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:24:11 -0700 Subject: [PATCH 1/2] fix(db): reset expanded linked issue claims --- src/db/repositories.ts | 8 -------- test/unit/db-parsers.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 7bcf60859b..99c8fd9217 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -378,17 +378,9 @@ function resolveLinkedIssueClaimedAt( if (linkedIssues.length === 0) return null; if (!existing) return observedLinkedIssueClaimedAt; if (existing.linkedIssuesJson === linkedIssuesJson) return existing.linkedIssueClaimedAt ?? observedLinkedIssueClaimedAt; - if (existing.linkedIssueClaimedAt && linkedIssuesOverlap(parseJson(existing.linkedIssuesJson, []), linkedIssues)) { - return existing.linkedIssueClaimedAt; - } return observedLinkedIssueClaimedAt; } -function linkedIssuesOverlap(left: number[], right: number[]): boolean { - const rightIssues = new Set(right); - return left.some((issue) => rightIssues.has(issue)); -} - export async function upsertIssueFromGitHub(env: Env, repoFullName: string, issue: GitHubIssuePayload, options: { seenOpenAt?: string } = {}): Promise { const record = toIssueRecord(repoFullName, issue); const db = getDb(env.DB); diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 1faf0a52df..4b48375ad8 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -92,7 +92,7 @@ describe("database row parser hardening", () => { ); }); - it("REGRESSION: adding another linked issue preserves the original shared-issue claim time", async () => { + it("REGRESSION: adding another linked issue resets the PR-level claim time", async () => { const env = createTestEnv(); vi.useFakeTimers(); @@ -136,7 +136,7 @@ describe("database row parser hardening", () => { expect(expanded).toMatchObject({ title: "Expanded claim", linkedIssues: [1, 2], - linkedIssueClaimedAt: first?.linkedIssueClaimedAt, + linkedIssueClaimedAt: "2026-06-29T10:05:00.000Z", }); vi.setSystemTime(new Date("2026-06-29T10:10:00.000Z")); From 013e03e3e36b97facd5fa190df78fb06eeb293e6 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 30 Jun 2026 02:30:17 -0700 Subject: [PATCH 2/2] fix(db): preserve reordered linked issue claims --- src/db/repositories.ts | 19 +++++++++++++- test/unit/db-parsers.test.ts | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 99c8fd9217..9ddb3d6529 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -377,10 +377,27 @@ function resolveLinkedIssueClaimedAt( ): string | null { if (linkedIssues.length === 0) return null; if (!existing) return observedLinkedIssueClaimedAt; - if (existing.linkedIssuesJson === linkedIssuesJson) return existing.linkedIssueClaimedAt ?? observedLinkedIssueClaimedAt; + if ( + existing.linkedIssuesJson === linkedIssuesJson || + sameLinkedIssueSet(parseLinkedIssuesJson(existing.linkedIssuesJson), linkedIssues) + ) + return existing.linkedIssueClaimedAt ?? observedLinkedIssueClaimedAt; return observedLinkedIssueClaimedAt; } +function parseLinkedIssuesJson(value: string): number[] { + const parsed = parseJson(value, []); + return Array.isArray(parsed) ? (parsed as number[]) : []; +} + +function sameLinkedIssueSet(left: number[], right: number[]): boolean { + return normalizedLinkedIssueSet(left) === normalizedLinkedIssueSet(right); +} + +function normalizedLinkedIssueSet(numbers: number[]): string { + return jsonString([...new Set(numbers)].sort((left, right) => left - right)); +} + export async function upsertIssueFromGitHub(env: Env, repoFullName: string, issue: GitHubIssuePayload, options: { seenOpenAt?: string } = {}): Promise { const record = toIssueRecord(repoFullName, issue); const db = getDb(env.DB); diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 4b48375ad8..b1bd5d7192 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -139,6 +139,22 @@ describe("database row parser hardening", () => { linkedIssueClaimedAt: "2026-06-29T10:05:00.000Z", }); + vi.setSystemTime(new Date("2026-06-29T10:07:00.000Z")); + await upsertPullRequestFromGitHub(env, "owner/repo", { + number: 11, + title: "Reordered expanded claim", + state: "open", + user: { login: "alice" }, + labels: [], + body: "Fixes #2\nFixes #1", + }); + const reordered = (await listPullRequests(env, "owner/repo")).find((p) => p.number === 11); + expect(reordered).toMatchObject({ + title: "Reordered expanded claim", + linkedIssues: [2, 1], + linkedIssueClaimedAt: expanded?.linkedIssueClaimedAt, + }); + vi.setSystemTime(new Date("2026-06-29T10:10:00.000Z")); await upsertPullRequestFromGitHub(env, "owner/repo", { number: 11, @@ -203,6 +219,41 @@ describe("database row parser hardening", () => { }); }); + it("repairs sparse non-array linked issue cache rows without throwing", async () => { + const env = createTestEnv(); + + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-29T11:10:00.000Z")); + await upsertPullRequestFromGitHub(env, "owner/repo", { + number: 13, + title: "Sparse cached claim", + state: "open", + user: { login: "alice" }, + labels: [], + body: "Fixes #8", + }); + await env.DB.prepare("UPDATE pull_requests SET linked_issues_json = ? WHERE repo_full_name = ? AND number = ?") + .bind("{}", "owner/repo", 13) + .run(); + + vi.setSystemTime(new Date("2026-06-29T11:12:00.000Z")); + await upsertPullRequestFromGitHub(env, "owner/repo", { + number: 13, + title: "Sparse cached claim repaired", + state: "open", + user: { login: "alice" }, + labels: [], + body: "Fixes #8", + }); + + const repaired = (await listPullRequests(env, "owner/repo")).find((p) => p.number === 13); + expect(repaired).toMatchObject({ + title: "Sparse cached claim repaired", + linkedIssues: [8], + linkedIssueClaimedAt: "2026-06-29T11:12:00.000Z", + }); + }); + it("markPullRequestRegated stamps the internal last_regated_at marker (sweep convergence #audit-sweep-converge)", async () => { const env = createTestEnv(); await upsertPullRequestFromGitHub(env, "owner/repo", { number: 5, title: "Stale PR", state: "open", user: { login: "alice" }, labels: [] });