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 }));