Files
healer-man/scripts/runewaker-pipeline/promote-origin-tree-alpha.mjs
T
2026-10-06 22:25:34 -04:00

143 lines
8.9 KiB
JavaScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { constants } from "node:fs";
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
// Promotion consumes reviewed, immutable candidates rather than regenerating
// them. Hash guards and exclusive backups protect unrelated continuation work.
const vine = process.argv.includes("--vine");
const stage = vine ? "artifacts/playtest/rom-deep-review/origin-root-card-attribution"
: "artifacts/playtest/rom-deep-review/origin-tree-alpha";
const field = vine ? "originVineAlpha" : "originTreeAlpha";
const cutoff = (vine ? 170 : 192) / 255;
const imageName = vine ? "72d48adc_elves_vine" : "da39b4ca_elves_gaint_tree";
const proof = JSON.parse(await readFile(`${stage}/staged-patch-proof.json`, "utf8"));
const visual = JSON.parse(await readFile(`${stage}/visual-ab-proof.json`, "utf8"));
assert.equal(visual.accepted, true, "Same-camera, served-identity tree review must be accepted first.");
const digest = bytes => createHash("sha256").update(bytes).digest("hex");
function glbParts(bytes) {
assert.equal(bytes.toString("ascii", 0, 4), "glTF");
assert.equal(bytes.readUInt32LE(8), bytes.length);
const jsonLength = bytes.readUInt32LE(12);
const json = JSON.parse(bytes.toString("utf8", 20, 20 + jsonLength));
const binStart = 20 + jsonLength + 8;
return { json, bin: bytes.subarray(binStart) };
}
function verifyVinePayloadPreservation(beforeBytes, afterBytes, patchedMaterials, role) {
const before = glbParts(beforeBytes), after = glbParts(afterBytes);
for (const key of ["accessors", "meshes", "nodes", "scenes", "scene", "textures", "samplers", "extensions"]) {
assert.deepEqual(after.json[key], before.json[key], `Origin ${key} changed outside the vine material/image patch.`);
}
assert.equal(after.json.materials.length, before.json.materials.length);
for (const [index, material] of before.json.materials.entries()) {
const candidate = after.json.materials[index];
if (!patchedMaterials.includes(index)) assert.deepEqual(candidate, material);
else {
const strip = value => { const { alphaMode, alphaCutoff, extras, ...rest } = value; return rest; };
assert.deepEqual(strip(candidate), strip(material));
assert.equal(candidate.alphaMode, "MASK");
assert.equal(candidate.alphaCutoff, cutoff);
for (const [key, value] of Object.entries(material.extras ?? {})) {
if (key !== "semanticRole") assert.deepEqual(candidate.extras?.[key], value);
}
}
}
const target = before.json.images.map((image, index) => image.name === imageName ? index : -1).filter(index => index >= 0);
assert.equal(target.length, role === "canonical-source" ? 2 : 1);
const targetViews = new Set(target.map(index => before.json.images[index].bufferView));
if (role === "canonical-source") assert.ok(after.bin.equals(before.bin), "Canonical vine patch changed source BIN.");
assert.equal(after.json.images.length, before.json.images.length);
for (const [index, image] of before.json.images.entries()) if (!target.includes(index)) assert.deepEqual(after.json.images[index], image);
for (const [index, view] of before.json.bufferViews.entries()) {
if (targetViews.has(index)) continue;
assert.deepEqual(after.json.bufferViews[index], view);
const offset = view.byteOffset ?? 0;
assert.ok(after.bin.subarray(offset, offset + view.byteLength).equals(before.bin.subarray(offset, offset + view.byteLength)),
`Non-target Origin bufferView ${index} changed.`);
}
}
const canonical = proof.stagedCopies.find(row => row.role === "canonical-source");
const shipping = proof.stagedCopies.find(row => row.role === "shipping-copy");
assert.ok(canonical && shipping && proof.stagedCopies.length === 3);
if (vine) {
assert.deepEqual(proof.replacementKtx2.dimensions, [256, 256]);
assert.equal(proof.replacementKtx2.mipLevels, 9);
assert.equal(proof.replacementKtx2.sourcePixelsPassingThreshold, 10407);
assert.equal(proof.replacementKtx2.decodedPixelsPassingThreshold, 10407);
assert.deepEqual(proof.replacementKtx2.thresholdMaskMismatches, { transparentBecamePassing: 0, opaqueBecameFailing: 0 });
assert.equal(digest(await readFile(proof.replacementKtx2.file)), proof.replacementKtx2.sha256);
}
assert.ok(visual.before.servedResponses.some(row => row.status === 200 && row.bodySha256 === shipping.inputSha256));
assert.ok(visual.after.servedResponses.some(row => row.status === 200 && row.bodySha256 === shipping.outputSha256));
assert.deepEqual(visual.before.errors, []);
assert.deepEqual(visual.after.errors, []);
assert.equal(visual.before.targetMaterialFirstHit, true);
assert.equal(visual.after.targetMaterialFirstHit, true);
assert.equal(visual.after.firstHit.texture, imageName);
assert.equal(visual.after.firstHit.alphaTest, cutoff);
assert.deepEqual(visual.before.camera.eye, visual.after.camera.eye);
assert.deepEqual(visual.before.camera.target, visual.after.camera.target);
assert.deepEqual(visual.before.lighting, visual.after.lighting);
for (const phase of [visual.before, visual.after]) {
assert.equal(digest(await readFile(phase.screenshot)), phase.screenshotSha256);
}
assert.equal(proof.gltfPolicy.alphaMode, "MASK");
assert.equal(proof.gltfPolicy.alphaCutoff, cutoff);
assert.equal(proof.gltfPolicy.lighting, "standard and unchanged");
const family = JSON.parse(await readFile(proof.sourceFamilyProof, "utf8"));
assert.equal(family.sourceRows.length, vine ? 2 : 7);
assert.equal(family.sharedDDSIdentity.sha256, proof.sourceTextureDdsSha256);
for (const row of family.sourceRows) {
assert.equal(digest(await readFile(row.nativeExporterReport)), row.nativeExporterReportSha256);
assert.equal(digest(await readFile(row.sourceDDS)), row.sourceDDSSha256);
assert.equal(digest(await readFile(row.preparedPNG)), row.preparedPNGSha256);
}
for (const row of proof.stagedCopies) {
if (!vine || row.role === "canonical-source") assert.equal(row.nonJsonChunksByteIdentical, true);
else assert.equal(row.nonTargetBufferViewsByteIdentical, true);
assert.equal(row.geometryWaterLightingAndAllNonTargetJsonPreserved, true);
const beforeBytes = await readFile(row.input), afterBytes = await readFile(row.output);
assert.equal(digest(beforeBytes), row.inputSha256, `${row.input} changed since staging.`);
assert.equal(digest(afterBytes), row.outputSha256, `${row.output} changed since review.`);
if (vine) verifyVinePayloadPreservation(beforeBytes, afterBytes, row.patchedMaterialIndexes, row.role);
}
const review = { stagedProof: `${stage}/staged-patch-proof.json`, visualProof: `${stage}/visual-ab-proof.json`, sourceFamilyProof: proof.sourceFamilyProof, sourceTextureDdsSha256: proof.sourceTextureDdsSha256, canonicalSha256: canonical.outputSha256, shippingSha256: shipping.outputSha256, alphaMode: "MASK", alphaCutoff: cutoff, geometryWaterLightingAndAllNonTargetJsonPreserved: true };
const edits = [];
for (const directory of ["src/assets/game/dungeons/the-origin", "runewaker-export-work/the-origin/converted"]) {
for (const name of ["import-metadata.json", "optimization-report.json", "conversion-report.json"]) {
const file = `${directory}/${name}`;
const data = JSON.parse(await readFile(file, "utf8"));
if (name === "import-metadata.json") {
const asset = data.assets.find(row => row.role === "visual");
assert.equal(asset.sha256, shipping.inputSha256);
asset.sha256 = shipping.outputSha256;
asset.size = shipping.outputBytes;
assert.equal(data.nativeMaterialSemantics.assembledSourceSha256, canonical.inputSha256);
data.nativeMaterialSemantics.assembledSourceSha256 = canonical.outputSha256;
data.continuationReview = { ...data.continuationReview, [field]: review };
} else if (name === "optimization-report.json") {
assert.equal(data.source.checksum, canonical.inputSha256);
assert.equal(data.optimized.checksum, shipping.inputSha256);
data.source.checksum = canonical.outputSha256;
data.source.size = canonical.outputBytes;
data.optimized.checksum = shipping.outputSha256;
data.optimized.size = shipping.outputBytes;
data[field] = review;
} else {
data.continuationIntegrityProof = { ...data.continuationIntegrityProof, [field]: review };
}
edits.push({ file, data });
}
}
const backup = `${stage}/pre-promotion-backup`;
for (const file of [...proof.stagedCopies.map(row => row.input), ...edits.map(row => row.file)]) {
const destination = path.join(backup, file);
await mkdir(path.dirname(destination), { recursive: true });
await copyFile(file, destination, constants.COPYFILE_EXCL);
}
for (const row of proof.stagedCopies) await copyFile(row.output, row.input);
for (const row of edits) await writeFile(row.file, `${JSON.stringify(row.data, null, 2)}\n`);
await writeFile(`${stage}/promotion-proof.json`, `${JSON.stringify({ promoted: proof.stagedCopies.map(row => ({ file: row.input, sha256: row.outputSha256 })), backup, review }, null, 2)}\n`);
console.log(JSON.stringify({ promoted: proof.stagedCopies.map(row => row.input), backup }));