feat(scanner): complete localized artifact quality checkpoint
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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<NativeScannerResultTombstone[]> {
|
||||
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<NativeScannerResultTombstone>;
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user