import fs from "node:fs/promises"; import path from "node:path"; /** * Native runs remain replayable evidence. Instead of rewriting their raw result * array, locally removed rows are recorded here and filtered at load time. */ export const NATIVE_SCANNER_RESULT_TOMBSTONES_FILE = "deleted-results.jsonl"; export interface NativeScannerResultTombstone { version: 1; resultId: string; deletedAt: string; imagePath?: string; removeLinkedStoreRecord: boolean; storeRecordId?: string; } export function nativeScannerResultTombstonePath(runDir: string) { return path.join(runDir, NATIVE_SCANNER_RESULT_TOMBSTONES_FILE); } export async function loadNativeScannerResultTombstones(runDir: string): Promise { try { const raw = await fs.readFile(nativeScannerResultTombstonePath(runDir), "utf8"); return raw .split(/\r?\n/) .filter(Boolean) .map(parseTombstone) .filter((entry): entry is NativeScannerResultTombstone => Boolean(entry)); } catch (error) { if (isMissingFileError(error)) return []; throw error; } } export async function loadNativeScannerDeletedResultIds(runDir: string) { const tombstones = await loadNativeScannerResultTombstones(runDir); return new Set(tombstones.map((entry) => entry.resultId)); } export async function appendNativeScannerResultTombstone( runDir: string, tombstone: NativeScannerResultTombstone, ) { const filePath = nativeScannerResultTombstonePath(runDir); await fs.appendFile(filePath, `${JSON.stringify(tombstone)}\n`, "utf8"); return filePath; } function parseTombstone(value: string): NativeScannerResultTombstone | null { try { const parsed = JSON.parse(value) as Partial; const resultId = typeof parsed.resultId === "string" ? parsed.resultId.trim() : ""; const deletedAt = typeof parsed.deletedAt === "string" ? parsed.deletedAt : ""; if (!resultId || !deletedAt) return null; return { version: 1, resultId, deletedAt, ...(typeof parsed.imagePath === "string" ? { imagePath: parsed.imagePath } : {}), removeLinkedStoreRecord: Boolean(parsed.removeLinkedStoreRecord), ...(typeof parsed.storeRecordId === "string" ? { storeRecordId: parsed.storeRecordId } : {}), }; } catch { return null; } } function isMissingFileError(error: unknown) { return typeof error === "object" && error !== null && "code" in error && (error as { code?: string }).code === "ENOENT"; }