Files
healer-man/scripts/runewaker-pipeline/compile-pose-audit-review.mjs
T
2026-08-14 15:56:39 -04:00

232 lines
10 KiB
JavaScript

#!/usr/bin/env node
/** Compile deterministic human-review JSON/Markdown from pose-audit reports. */
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
function slash(value) {
return value.replaceAll("\\", "/");
}
function countBy(values, selector) {
const counts = new Map();
for (const value of values) {
const key = selector(value);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return Object.fromEntries([...counts].sort(([left], [right]) => left.localeCompare(right)));
}
function markdownCell(value) {
return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
}
async function main() {
const [batchRootValue, reviewInputValue, outputJsonValue, outputMarkdownValue] = process.argv.slice(2);
if (!outputMarkdownValue) {
throw new Error(
"Usage: compile-pose-audit-review.mjs <batch-root> <visual-review-input.json> " +
"<output.json> <output.md>",
);
}
const batchRoot = path.resolve(batchRootValue);
const reviewInput = JSON.parse(await readFile(path.resolve(reviewInputValue), "utf8"));
const summary = JSON.parse(await readFile(path.join(batchRoot, "summary.json"), "utf8"));
const expected = summary.actors.map((actor) => actor.input).sort();
const reviewed = [...new Set(reviewInput.reviewedVariants)].sort();
if (JSON.stringify(expected) !== JSON.stringify(reviewed)) {
const missing = expected.filter((variant) => !reviewed.includes(variant));
const extra = reviewed.filter((variant) => !expected.includes(variant));
throw new Error(`Visual review inventory mismatch. Missing=${missing.join(",")} Extra=${extra.join(",")}`);
}
const actors = [];
const warningSamples = [];
const assetWarnings = [];
for (const summaryActor of [...summary.actors].sort((left, right) => left.input.localeCompare(right.input))) {
const reportFile = path.join(batchRoot, summaryActor.report);
const report = JSON.parse(await readFile(reportFile, "utf8"));
const override = reviewInput.notes[summaryActor.input];
const visualVerdict =
override?.verdict ??
(summaryActor.status === "pass"
? "visually-coherent"
: "visually-coherent-with-numeric-warning");
const visualNote =
override?.note ??
"Bind and every rendered semantic-family pose preserve a coherent silhouette, joint layout, " +
"and attached equipment; no visible quaternion-basis deformation.";
const actionWarnings = [];
for (const sample of report.samples) {
if (!sample.issues.length) continue;
const row = {
variantId: summaryActor.input,
action: sample.action,
semanticFamily: sample.semanticFamily,
frameLabel: sample.frameLabel,
frame: sample.frame,
issues: sample.issues,
};
actionWarnings.push(row);
warningSamples.push(row);
}
for (const issue of report.assetIssues ?? []) {
assetWarnings.push({ variantId: summaryActor.input, ...issue });
}
const reviewDirectory = summaryActor.report.replace(/\.pose-audit\.json$/i, ".review");
actors.push({
dungeonId: summaryActor.input.split("/")[0],
variantId: summaryActor.input,
numericStatus: summaryActor.status,
actions: summaryActor.actions,
evaluatedPoses: summaryActor.poses,
flaggedSamples: summaryActor.flaggedSamples,
warningCodes: countBy(
actionWarnings.flatMap((sample) => sample.issues),
(issue) => issue.code,
),
warningActions: countBy(actionWarnings, (sample) => sample.action),
assetIssues: report.assetIssues ?? [],
visualVerdict,
visualNote,
report: slash(path.relative(batchRoot, reportFile)),
contactSheet: slash(path.join(reviewDirectory, "contact-sheet.png")),
});
}
const dungeonIds = [
...new Set([...actors.map((actor) => actor.dungeonId), ...reviewInput.explicitEmptyDungeons]),
].sort();
const dungeons = dungeonIds.map((dungeonId) => {
const rows = actors.filter((actor) => actor.dungeonId === dungeonId);
return {
dungeonId,
configuredVariants: rows.length,
numericPass: rows.filter((actor) => actor.numericStatus === "pass").length,
numericWarning: rows.filter((actor) => actor.numericStatus === "warning").length,
numericError: rows.filter((actor) => actor.numericStatus === "error").length,
failed: rows.filter((actor) => actor.numericStatus === "failed").length,
actions: rows.reduce((sum, actor) => sum + actor.actions, 0),
evaluatedPoses: rows.reduce((sum, actor) => sum + actor.evaluatedPoses, 0),
flaggedSamples: rows.reduce((sum, actor) => sum + actor.flaggedSamples, 0),
visualAccepted: rows.filter((actor) => !actor.visualVerdict.startsWith("defect")).length,
confirmedDefects: rows.filter((actor) => actor.visualVerdict.startsWith("defect")).length,
emptyReason:
rows.length === 0 ? reviewInput.emptyReasons?.[dungeonId] ?? "No configured runtime variants." : null,
};
});
const result = {
schemaVersion: 1,
batch: reviewInput.batch,
reviewedOn: reviewInput.reviewedOn,
authority: {
runtimeCoverage: reviewInput.runtimeCoverage,
numericSummary: "summary.json",
method: reviewInput.method,
command: reviewInput.command,
},
totals: {
configuredVariants: actors.length,
contactSheetsReviewed: reviewed.length,
numericPassActors: actors.filter((actor) => actor.numericStatus === "pass").length,
numericWarningActors: actors.filter((actor) => actor.numericStatus === "warning").length,
numericErrorActors: actors.filter((actor) => actor.numericStatus === "error").length,
failedActors: actors.filter((actor) => actor.numericStatus === "failed").length,
sampledActions: actors.reduce((sum, actor) => sum + actor.actions, 0),
evaluatedPoses: actors.reduce((sum, actor) => sum + actor.evaluatedPoses, 0),
flaggedSamples: actors.reduce((sum, actor) => sum + actor.flaggedSamples, 0),
sampleWarnings: warningSamples.length,
assetWarnings: assetWarnings.length,
visualAccepted: actors.filter((actor) => !actor.visualVerdict.startsWith("defect")).length,
confirmedDefects: actors.filter((actor) => actor.visualVerdict.startsWith("defect")).length,
},
disposition: reviewInput.disposition,
dungeons,
warningActors: actors.filter(
(actor) => actor.numericStatus !== "pass" || actor.assetIssues.length > 0,
),
warningSamples,
assetWarnings,
errors: actors.filter((actor) => ["error", "failed"].includes(actor.numericStatus)),
actors,
limitations: reviewInput.limitations,
};
await writeFile(path.resolve(outputJsonValue), `${JSON.stringify(result, null, 2)}\n`, "utf8");
const lines = [
`# RuneWaker animation pose audit - ${reviewInput.batch}`,
"",
reviewInput.disposition,
"",
`- Configured shipping variants: ${result.totals.configuredVariants}`,
`- Contact sheets reviewed: ${result.totals.contactSheetsReviewed}`,
`- Evaluated poses: ${result.totals.evaluatedPoses} across ${result.totals.sampledActions} actions`,
`- Numeric actor results: ${result.totals.numericPassActors} pass, ${result.totals.numericWarningActors} warning, ${result.totals.numericErrorActors} error, ${result.totals.failedActors} failed`,
`- Numeric issue events: ${result.totals.sampleWarnings} sampled-pose warnings and ${result.totals.assetWarnings} asset warning`,
`- Visual results: ${result.totals.visualAccepted} accepted, ${result.totals.confirmedDefects} confirmed defects`,
"",
"## Dungeon totals",
"",
"| Dungeon | Variants | Pass | Warning | Error/failed | Actions | Poses | Flagged | Visual accepted |",
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
...dungeons.map(
(dungeon) =>
`| ${dungeon.dungeonId} | ${dungeon.configuredVariants} | ${dungeon.numericPass} | ${dungeon.numericWarning} | ${dungeon.numericError + dungeon.failed} | ${dungeon.actions} | ${dungeon.evaluatedPoses} | ${dungeon.flaggedSamples} | ${dungeon.visualAccepted} |`,
),
"",
"## Warning disposition",
"",
"Every numeric warning is retained in `batch-a-review.json` down to action, frame label, frame value, and issue payload.",
"",
"| Variant | Numeric warnings | Affected actions | Visual verdict | Disposition |",
"| --- | ---: | --- | --- | --- |",
...result.warningActors.map((actor) => {
const warningCount = Object.values(actor.warningCodes).reduce((sum, count) => sum + count, 0);
const actions = Object.entries(actor.warningActions)
.map(([name, count]) => `${name} (${count})`)
.join(", ");
return `| ${actor.variantId} | ${warningCount + actor.assetIssues.length} | ${markdownCell(actions || actor.assetIssues.map((issue) => issue.code).join(", "))} | ${actor.visualVerdict} | ${markdownCell(actor.visualNote)} |`;
}),
"",
"## Per-actor visual verdicts",
];
for (const dungeon of dungeons) {
lines.push("", `### ${dungeon.dungeonId}`, "");
if (dungeon.configuredVariants === 0) {
lines.push(dungeon.emptyReason, "");
continue;
}
lines.push(
"| Variant | Numeric | Actions/poses | Visual verdict | Review note | Evidence |",
"| --- | --- | ---: | --- | --- | --- |",
);
for (const actor of actors.filter((row) => row.dungeonId === dungeon.dungeonId)) {
lines.push(
`| ${actor.variantId.split("/").at(-1)} | ${actor.numericStatus} | ${actor.actions}/${actor.evaluatedPoses} | ${actor.visualVerdict} | ${markdownCell(actor.visualNote)} | [sheet](${actor.contactSheet}) / [metrics](${actor.report}) |`,
);
}
}
lines.push(
"",
"## Command",
"",
"```powershell",
reviewInput.command,
"```",
"",
"## Limits",
"",
...reviewInput.limitations.map((limitation) => `- ${limitation}`),
"",
);
await writeFile(path.resolve(outputMarkdownValue), lines.join("\n"), "utf8");
}
main().catch((error) => {
console.error(error.stack ?? error.message);
process.exitCode = 1;
});