From 229a4c60a23f2e7bbbfc49183cf5610f620ba355 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 29 Apr 2026 13:54:24 -0500 Subject: [PATCH] Limit legacy disposition preview lookback Default the legacy execution-disposition preview to recently created issues, keep an explicit --all escape hatch, and cover the lookback behavior with a targeted test. Co-Authored-By: Paperclip --- package.json | 1 + ...acy-invalid-execution-dispositions.test.ts | 29 ++++++++++++ ...w-legacy-invalid-execution-dispositions.ts | 45 ++++++++++++------- 3 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 scripts/preview-legacy-invalid-execution-dispositions.test.ts diff --git a/package.json b/package.json index e08162bc802..b0153c0a141 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "db:migrate": "pnpm --filter @paperclipai/db migrate", "issue-references:backfill": "pnpm run preflight:workspace-links && tsx scripts/backfill-issue-reference-mentions.ts", "execution-dispositions:preview": "tsx scripts/preview-legacy-invalid-execution-dispositions.ts", + "execution-dispositions:preview:test": "tsx --test scripts/preview-legacy-invalid-execution-dispositions.test.ts", "secrets:migrate-inline-env": "tsx scripts/migrate-inline-env-secrets.ts", "db:backup": "./scripts/backup-db.sh", "paperclipai": "node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts", diff --git a/scripts/preview-legacy-invalid-execution-dispositions.test.ts b/scripts/preview-legacy-invalid-execution-dispositions.test.ts new file mode 100644 index 00000000000..c5f7e75ad87 --- /dev/null +++ b/scripts/preview-legacy-invalid-execution-dispositions.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { filterIssuesByLookback, issueLookbackTimestamp } from "./preview-legacy-invalid-execution-dispositions.ts"; + +describe("legacy execution-disposition preview lookback", () => { + it("scopes default repair candidates by issue creation time, not recent updates", () => { + const cutoff = new Date("2026-04-27T12:00:00.000Z"); + const oldButRecentlyUpdated = { + id: "old", + title: "old blocked issue", + status: "blocked" as const, + createdAt: "2026-04-20T12:00:00.000Z", + updatedAt: "2026-04-29T12:00:00.000Z", + }; + const newlyCreated = { + id: "new", + title: "new blocked issue", + status: "blocked" as const, + createdAt: "2026-04-28T12:00:00.000Z", + updatedAt: "2026-04-28T12:00:00.000Z", + }; + + assert.equal(issueLookbackTimestamp(oldButRecentlyUpdated)?.toISOString(), "2026-04-20T12:00:00.000Z"); + assert.deepEqual(filterIssuesByLookback([oldButRecentlyUpdated, newlyCreated], cutoff).map((issue) => issue.id), [ + "new", + ]); + }); +}); diff --git a/scripts/preview-legacy-invalid-execution-dispositions.ts b/scripts/preview-legacy-invalid-execution-dispositions.ts index 419ef23bc98..449a38611a5 100644 --- a/scripts/preview-legacy-invalid-execution-dispositions.ts +++ b/scripts/preview-legacy-invalid-execution-dispositions.ts @@ -1,3 +1,5 @@ +import { pathToFileURL } from "node:url"; + type IssueStatus = "backlog" | "todo" | "in_progress" | "in_review" | "done" | "blocked" | "cancelled"; type InvalidDisposition = { @@ -80,14 +82,14 @@ function dateValue(value: string | Date | null | undefined): Date | null { return Number.isNaN(parsed.getTime()) ? null : parsed; } -function issueScopeTimestamp(issue: IssueSummary): Date | null { - return dateValue(issue.updatedAt) ?? dateValue(issue.createdAt); +export function issueLookbackTimestamp(issue: IssueSummary): Date | null { + return dateValue(issue.createdAt); } -function filterIssuesByLookback(issues: IssueSummary[], cutoff: Date | null): IssueSummary[] { +export function filterIssuesByLookback(issues: IssueSummary[], cutoff: Date | null): IssueSummary[] { if (!cutoff) return issues; return issues.filter((issue) => { - const timestamp = issueScopeTimestamp(issue); + const timestamp = issueLookbackTimestamp(issue); return timestamp ? timestamp >= cutoff : false; }); } @@ -183,6 +185,19 @@ function proposedAction(reason: string, issue: IssueSummary): string { } } +let apiUrl: string; +let apiKey: string; +let runId: string | undefined; + +function configureApiFromEnvironment() { + apiUrl = requireConfig( + "PAPERCLIP_API_URL or --api-url", + readFlag("--api-url") ?? process.env.PAPERCLIP_API_URL, + ); + apiKey = requireConfig("PAPERCLIP_API_KEY", process.env.PAPERCLIP_API_KEY); + runId = process.env.PAPERCLIP_RUN_ID?.trim() || undefined; +} + async function requestJson(method: string, path: string, body?: unknown): Promise { const url = new URL(path, apiUrl); const response = await fetch(url, { @@ -284,7 +299,7 @@ function printPreview( console.log(`# Legacy execution-disposition reconciliation preview`); console.log(""); console.log(`Company: ${companyId}`); - console.log(`Lookback: ${cutoff ? `updated since ${cutoff.toISOString()}` : "all non-terminal issues"}`); + console.log(`Lookback: ${cutoff ? `created since ${cutoff.toISOString()}` : "all non-terminal issues"}`); console.log(`Non-terminal issues fetched: ${totalFetched}`); console.log(`Non-terminal issues scanned: ${totalScanned}`); console.log(`Invalid issues found: ${invalidCount}`); @@ -340,14 +355,8 @@ async function applySafeCorrections(groups: PreviewGroup[]): Promise { return applied; } -const apiUrl = requireConfig( - "PAPERCLIP_API_URL or --api-url", - readFlag("--api-url") ?? process.env.PAPERCLIP_API_URL, -); -const apiKey = requireConfig("PAPERCLIP_API_KEY", process.env.PAPERCLIP_API_KEY); -const runId = process.env.PAPERCLIP_RUN_ID?.trim(); - async function main() { + configureApiFromEnvironment(); const companyId = requireConfig( "PAPERCLIP_COMPANY_ID or --company", readFlag("--company") ?? process.env.PAPERCLIP_COMPANY_ID, @@ -372,8 +381,10 @@ async function main() { printPreview(companyId, fetchedIssues.length, issues.length, cutoff, groups, appliedCount); } -void main().catch((error) => { - const message = error instanceof Error ? error.message : String(error); - console.error(`Legacy execution-disposition reconciliation preview failed: ${message}`); - process.exitCode = 1; -}); +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + void main().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`Legacy execution-disposition reconciliation preview failed: ${message}`); + process.exitCode = 1; + }); +}