135 lines
5.3 KiB
JavaScript
135 lines
5.3 KiB
JavaScript
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const EVAL_FIELDS = new Set(["name", "slot", "level", "mainStat", "mainValue", "setName", "equipped", "substats"]);
|
|
|
|
function argValue(name, fallback = "") {
|
|
const prefix = `--${name}=`;
|
|
const match = process.argv.find((entry) => entry.startsWith(prefix));
|
|
return match ? match.slice(prefix.length) : fallback;
|
|
}
|
|
|
|
function parseExpectedFields() {
|
|
const inlineJson = argValue("expect-json");
|
|
const expectFile = argValue("expect-file");
|
|
if (!inlineJson && !expectFile) {
|
|
throw new Error("Missing expected labels. Pass --expect-json=... or --expect-file=...");
|
|
}
|
|
const raw = (inlineJson || fs.readFileSync(path.resolve(expectFile), "utf8")).replace(/^\uFEFF/, "");
|
|
const parsed = JSON.parse(raw);
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
throw new Error("Expected labels must be a JSON object.");
|
|
}
|
|
const expect = {};
|
|
for (const [field, value] of Object.entries(parsed)) {
|
|
if (!EVAL_FIELDS.has(field)) throw new Error(`Unknown expected field: ${field}`);
|
|
if (field === "level") {
|
|
if (!Number.isInteger(value) || value < 0) throw new Error("Expected level must be a non-negative integer.");
|
|
expect[field] = value;
|
|
continue;
|
|
}
|
|
if (field === "substats") {
|
|
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string" && entry.trim())) {
|
|
throw new Error("Expected substats must be a non-empty string array.");
|
|
}
|
|
expect[field] = value;
|
|
continue;
|
|
}
|
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Expected ${field} must be a non-empty string.`);
|
|
expect[field] = value;
|
|
}
|
|
if (Object.keys(expect).length === 0) throw new Error("Expected labels must include at least one field.");
|
|
return expect;
|
|
}
|
|
|
|
function loadCandidate(inputPath, candidateId) {
|
|
const payload = JSON.parse(fs.readFileSync(path.resolve(inputPath), "utf8"));
|
|
const candidates = Array.isArray(payload?.candidates) ? payload.candidates : [];
|
|
const candidate = candidates.find((entry) => entry.id === candidateId);
|
|
if (!candidate) throw new Error(`Candidate not found: ${candidateId}`);
|
|
if (!candidate.ocr || typeof candidate.ocr !== "object" || Object.keys(candidate.ocr).length === 0) {
|
|
throw new Error(`Candidate has no OCR payload: ${candidateId}`);
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function stableId(value) {
|
|
return String(value || "")
|
|
.replace(/[^0-9A-Za-z]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 80) || "review-case";
|
|
}
|
|
|
|
function safeProvenanceValue(value, fallback = "unknown") {
|
|
const text = typeof value === "string" ? value.trim() : "";
|
|
if (!text || text.length > 200 || /[\r\n]/.test(text)) return fallback;
|
|
if (path.isAbsolute(text) || /[A-Za-z]:[\\/]/.test(text) || text.startsWith("\\\\")) return fallback;
|
|
return text;
|
|
}
|
|
|
|
function visualEvidenceNote(candidate) {
|
|
const status = candidate?.visualEvidence?.status === "available" ? "available" : "unavailable";
|
|
const parts = [`visualEvidence=${status}`];
|
|
const captureId = safeProvenanceValue(candidate?.captureId, "");
|
|
const capturedAt = safeProvenanceValue(candidate?.capturedAt, "");
|
|
if (captureId) parts.push(`captureId=${captureId}`);
|
|
if (capturedAt) parts.push(`capturedAt=${capturedAt}`);
|
|
return parts.join(" | ");
|
|
}
|
|
|
|
function objectLiteral(value, indent = 2) {
|
|
return JSON.stringify(value, null, indent).replace(/"([A-Za-z_$][0-9A-Za-z_$]*)":/g, "$1:");
|
|
}
|
|
|
|
function snippetFor(candidate, expect) {
|
|
const id = stableId(argValue("id", `confirmed-${candidate.id}`));
|
|
const reason = safeProvenanceValue(candidate.reason, "review-sample");
|
|
const savedAt = safeProvenanceValue(candidate.savedAt, "unknown");
|
|
const resolution = safeProvenanceValue(candidate.resolution, "");
|
|
const sourceCandidate = stableId(candidate.id);
|
|
const entry = {
|
|
id,
|
|
confirmed: true,
|
|
ocr: candidate.ocr,
|
|
expect,
|
|
meta: {
|
|
source: "review-sample",
|
|
resolution: resolution || undefined,
|
|
note: `${reason} | savedAt=${savedAt} | sourceCandidate=${sourceCandidate} | ${visualEvidenceNote(candidate)}`,
|
|
},
|
|
};
|
|
return `${objectLiteral(entry, 2)},\n`;
|
|
}
|
|
|
|
function defaultInputPath() {
|
|
return path.join(process.cwd(), "outputs", "review-eval-candidates", "review-eval-candidates.json");
|
|
}
|
|
|
|
function main() {
|
|
const inputPath = argValue("input", defaultInputPath());
|
|
const candidateId = argValue("candidate");
|
|
if (!candidateId) throw new Error("Missing candidate id. Pass --candidate=<id>.");
|
|
const outputPath = path.resolve(argValue("out", path.join(process.cwd(), "outputs", "review-eval-candidates", `${stableId(candidateId)}.confirmed.ts`)));
|
|
const candidate = loadCandidate(inputPath, candidateId);
|
|
if (candidate?.visualEvidence?.status !== "available") {
|
|
throw new Error("Candidate has no retrievable visual evidence. Re-export after retaining a local PNG before preparing a confirmed corpus case.");
|
|
}
|
|
const expect = parseExpectedFields();
|
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
const snippet = snippetFor(candidate, expect);
|
|
fs.writeFileSync(outputPath, snippet, "utf8");
|
|
console.log(JSON.stringify({ ok: true, candidateId, outputPath, labeledFields: Object.keys(expect) }, null, 2));
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = {
|
|
loadCandidate,
|
|
parseExpectedFields,
|
|
snippetFor,
|
|
stableId,
|
|
visualEvidenceNote,
|
|
};
|