feat(scanner): complete localized artifact quality checkpoint

This commit is contained in:
AzuTear
2026-07-11 15:59:19 +02:00
parent 639b0b7f59
commit 8b9f948c6b
215 changed files with 35440 additions and 7273 deletions
+81
View File
@@ -0,0 +1,81 @@
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { createHash } from "node:crypto";
import { loadNativeScannerDeletedResultIds } from "../electron/services/nativeScannerResultTombstones.js";
import { replayNativeScanResults } from "../src/eval/nativeScanReplay.js";
import type { StoredScanResultEntry } from "../src/types/storage.js";
const options = parseArgs(process.argv.slice(2));
const scanRoot = path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "genshin-artifact-assistant", "native-scans");
const runDir = options.runDir ? path.resolve(options.runDir) : await latestRunDir(scanRoot);
const sourcePath = path.join(runDir, "scan-results.json");
const source = await fs.readFile(sourcePath, "utf8");
const parsed = JSON.parse(source);
if (!Array.isArray(parsed) || parsed.length === 0) throw new Error(`No scan results found in ${sourcePath}`);
const rawEntries = parsed.filter(isStoredScanResultEntry);
if (rawEntries.length !== parsed.length) throw new Error(`Invalid scan-result entries: ${parsed.length - rawEntries.length}`);
const deletedResultIds = await loadNativeScannerDeletedResultIds(runDir);
const entries = rawEntries.filter((entry) => !deletedResultIds.has(entry.id));
if (entries.length === 0) throw new Error(`No visible scan results found in ${sourcePath}`);
const report = replayNativeScanResults({
entries,
repeats: options.repeats,
runId: path.basename(runDir),
sourcePath,
sourceSha256: createHash("sha256").update(source).digest("hex"),
});
const outputDir = path.resolve("outputs", "native-replay", report.runId);
await fs.mkdir(outputDir, { recursive: true });
const reportPath = path.join(outputDir, "native-replay-report.json");
await fs.writeFile(reportPath, JSON.stringify(report, null, 2), "utf8");
console.log(`Native offline replay: ${report.runId}`);
if (deletedResultIds.size > 0) console.log(`Locally removed results excluded: ${deletedResultIds.size}`);
console.log(
`Results: ${report.total}; evaluated=${report.values.evaluated}; excluded=${report.values.excluded}; `
+ `review=${report.values.review}; unknown=${report.values.unknown}`,
);
console.log(`Projection: available=${report.projections.available}; complete=${report.projections.complete}; unavailable=${report.projections.unavailable}`);
console.log(`Score: min=${report.score.min}; avg=${report.score.average}; max=${report.score.max}`);
console.log(`Deterministic: ${report.deterministic} across ${report.repeats} repeats`);
console.log(`Report: ${reportPath}`);
if (!report.deterministic) throw new Error("Offline replay produced different evaluation hashes.");
if (report.cleanUnevaluated.length > 0) {
console.error(`Clean but unevaluated results: ${report.cleanUnevaluated.map((entry) => `#${entry.sequence} ${entry.summary}`).join(" | ")}`);
process.exitCode = 1;
}
function parseArgs(args: string[]) {
const runDir = args.find((argument) => argument.startsWith("--run-dir="))?.slice("--run-dir=".length) ?? "";
const repeatValue = Number(args.find((argument) => argument.startsWith("--repeats="))?.slice("--repeats=".length) ?? 3);
return { runDir, repeats: Number.isFinite(repeatValue) ? repeatValue : 3 };
}
async function latestRunDir(root: string) {
const entries = await fs.readdir(root, { withFileTypes: true });
const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().reverse();
for (const candidate of candidates) {
const runDir = path.join(root, candidate);
try {
await fs.access(path.join(runDir, "scan-results.json"));
return runDir;
} catch {
// Continue to the next saved run; no live capture is started.
}
}
throw new Error(`No saved native scan-results.json found below ${root}`);
}
function isStoredScanResultEntry(value: unknown): value is StoredScanResultEntry {
if (!value || typeof value !== "object") return false;
const entry = value as Partial<StoredScanResultEntry>;
return typeof entry.id === "string"
&& typeof entry.runId === "string"
&& Number.isFinite(entry.sequence)
&& typeof entry.extractionStatus === "string"
&& typeof entry.valueStatus === "string"
&& Array.isArray(entry.notes);
}