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"; const stage = "artifacts/playtest/rom-deep-review/water-screen-effects"; const proof = JSON.parse(await readFile(`${stage}/proof.json`, "utf8")); const browser = JSON.parse(await readFile(`${stage}/browser-proof.json`, "utf8")); assert.ok(browser.some(row => row.staged && !row.errors.length && row.water.some(layer => layer.screenEffectsReady === 1))); const sha = bytes => createHash("sha256").update(bytes).digest("hex"); const writeJson = (file, data) => writeFile(file, `${JSON.stringify(data, null, 2)}\n`); const canonical = proof.files.find(row => row.kind === "canonical"); const shipping = proof.files.find(row => row.kind === "shipping"); const backup = `${stage}/pre-promotion-backup`; const directories = ["src/assets/game/dungeons/the-origin", "runewaker-export-work/the-origin/converted"]; const edits = []; for (const directory of directories) { for (const name of ["import-metadata.json", "optimization-report.json", "conversion-report.json"]) { const file = `${directory}/${name}`; const bytes = await readFile(file); const data = JSON.parse(bytes); if (name === "import-metadata.json") { const visual = data.assets.find(row => row.role === "visual"); assert.equal(visual.sha256, shipping.previousSha256); visual.sha256 = shipping.sha256; visual.size = shipping.bytes; data.nativeMaterialSemantics.assembledSourceSha256 = canonical.sha256; data.continuationReview.waterScreenEffects = proof; } else if (name === "optimization-report.json") { assert.equal(data.source.checksum, canonical.previousSha256); assert.equal(data.optimized.checksum, shipping.previousSha256); data.source.checksum = canonical.sha256; data.source.size = canonical.bytes; data.optimized.checksum = shipping.sha256; data.optimized.size = shipping.bytes; data.waterScreenEffects = proof; } else { data.continuationIntegrityProof.waterScreenEffects = proof; } edits.push({ file, bytes, data }); } } const nativeFile = "src/assets/game/dungeons/the-origin/native-water-report.json"; const nativeBytes = await readFile(nativeFile); const native = JSON.parse(nativeBytes); native.waterScreenEffects = proof; native.nativeMaterialSemantics.viewportMaterial = "Source-enabled browser reflection/refraction with native split planes, .025 bump perturbation and Fresnel .02037/.97963 coefficients. Three.js lighting replaces native SH; reflect-class filtering is unsupported."; native.limitations = native.limitations.filter(row => !row.includes("projected screen reflection/refraction paths")); native.limitations.push("Browser screen effects use up to four 512px plane pairs per asset and at most ten two-pass captures per second. Additional planes and below-plane views retain the lit animated fallback. Native SH and reflect-class filtering remain unsupported."); edits.push({ file: nativeFile, bytes: nativeBytes, data: native }); // Verify every expected old/new identity before any canonical mutation. for (const row of proof.files) { assert.equal(sha(await readFile(row.file)), row.previousSha256); assert.equal(sha(await readFile(row.staged)), row.sha256); } for (const row of [...proof.files, ...edits]) { const destination = path.join(backup, row.file); await mkdir(path.dirname(destination), { recursive: true }); await copyFile(row.file, destination, constants.COPYFILE_EXCL); } for (const row of proof.files) await copyFile(row.staged, row.file); for (const row of edits) await writeJson(row.file, row.data); console.log(JSON.stringify({ promoted: proof.files.map(row => ({ file: row.file, sha256: row.sha256 })), backup }));