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/uv-stretch-review"; const manifestFile = process.argv[2] ?? `${stage}/final-surface-policy-stage-manifest.json`; const manifest = JSON.parse(await readFile(manifestFile, "utf8")); const digest = bytes => createHash("sha256").update(bytes).digest("hex"); const json = async file => JSON.parse(await readFile(file, "utf8")); assert.deepEqual(manifest.dungeons.map(row => row.dungeonId).sort(), ["barren-caves", "hoto"]); const copies = [], edits = [], reviews = []; for (const row of manifest.dungeons) { const id = row.dungeonId; const canonical = await readFile(row.canonical.file); const optimized = await readFile(row.optimized.file); const shipping = await readFile(row.shipping.file); assert.equal(digest(canonical), row.canonical.oldSha256); assert.equal(digest(optimized), row.optimized.oldSha256); assert.equal(digest(shipping), row.shipping.oldSha256); // Historical converted copies can predate later shipping compression. Guard // each baseline independently; promotion gives both roles one reviewed output. for (const asset of [row.canonical, row.optimized]) { const bytes = await readFile(asset.candidate); assert.equal(digest(bytes), asset.candidateSha256); assert.equal(bytes.length, asset.candidateBytes); } const layout = await json(row.sourceLayoutProof); assert.equal(digest(await readFile(layout.beforeFile)), layout.beforeSha256); assert.equal(layout.afterSha256, row.canonical.candidateSha256); assert.equal(layout.hierarchyAndTransformsPreserved, true); assert.equal(layout.orderedSourceTriangleCornersPreserved, true); assert.equal(layout.allSourceAttributesExceptAdaptedUv1Preserved, true); assert.equal(layout.originalSourceImagesPreserved, true); const policy = await json(row.policyReport); assert.equal(policy.sourceCanonicalSha256, row.canonical.oldSha256); assert.equal(policy.outputSha256, row.canonical.candidateSha256); assert.equal(digest(await readFile(policy.sourceRos)), policy.sourceResourceSha256); assert.equal(policy.policyReport.surfaceQuality.status, "applied"); const quality = await json(row.textureQualityReport); assert.deepEqual(quality.errors, []); assert.equal(quality.policy.preservesSourceDimensionsAndFullMipChains, true); const codec = (await json(row.codecProof)).results.find(result => result.id === id); assert.ok(codec && codec.textures.length >= (id === "hoto" ? 4 : 6)); assert.equal(codec.glbSha256, row.optimized.candidateSha256); assert.ok(codec.textures.every(texture => texture.maeAlpha === 0)); const visual = await json(row.visualProof); assert.equal(visual.status, "passed"); const views = visual.results.filter(result => result.dungeonId === id); assert.ok(views.filter(result => result.stage === "candidate").length >= 2); for (const view of views) { assert.deepEqual(view.errors, []); assert.equal(view.glbSha256, view.stage === "candidate" ? row.optimized.candidateSha256 : row.shipping.oldSha256); assert.ok((await readFile(view.screenshot)).length > 1000); if (view.stage === "candidate") assert.ok(view.candidateResponses.some(response => response.status === 200 && response.bodySha256 === row.optimized.candidateSha256)); } const review = { manifest: manifestFile, policyReport: row.policyReport, sourceLayoutProof: row.sourceLayoutProof, textureQualityReport: row.textureQualityReport, codecProof: row.codecProof, visualProof: row.visualProof, canonicalBefore: row.canonical.oldSha256, canonicalAfter: row.canonical.candidateSha256, shippingBefore: row.shipping.oldSha256, shippingAfter: row.optimized.candidateSha256 }; reviews.push({ dungeonId: id, ...review }); copies.push({ file: row.canonical.file, candidate: row.canonical.candidate }, { file: row.optimized.file, candidate: row.optimized.candidate }, { file: row.shipping.file, candidate: row.optimized.candidate }); for (const directory of [`src/assets/game/dungeons/${id}`, `runewaker-export-work/${id}/converted`]) { for (const name of ["import-metadata.json", "optimization-report.json", "conversion-report.json"]) { const file = `${directory}/${name}`; const data = await json(file).catch(error => { if (error.code === "ENOENT" && directory.startsWith("runewaker-export-work") && name === "import-metadata.json") return null; throw error; }); if (!data) continue; if (name === "import-metadata.json") { const asset = data.assets.find(asset => asset.role === "visual"); assert.equal(asset.sha256, row.shipping.oldSha256); asset.sha256 = row.optimized.candidateSha256; asset.size = row.optimized.candidateBytes; if (data.nativeMaterialSemantics?.assembledSourceSha256) data.nativeMaterialSemantics.assembledSourceSha256 = row.canonical.candidateSha256; data.continuationReview = { ...data.continuationReview, surfaceQuality: review }; } else if (name === "optimization-report.json") { if (directory.startsWith("src/")) assert.equal(data.optimized.checksum, row.shipping.oldSha256); data.source = { ...data.source, checksum: row.canonical.candidateSha256, size: row.canonical.candidateBytes }; data.optimized = { ...data.optimized, checksum: row.optimized.candidateSha256, size: row.optimized.candidateBytes }; data.compression = "meshopt+ktx2"; data.textureQuality = quality; data.supersededVisualUvAdaptations ??= data.visualUvAdaptations; data.visualUvAdaptations = policy.policyReport; data.commands = [ "applyRunewakerVisualUvAdaptationsToFile with the source-pinned visualSurfaceQualityPolicies recipe opt-in", "gltf-transform optimize --compress meshopt --meshopt-level high --instance true --instance-min 3 --simplify false --texture-compress false --join false --prune false --prune-attributes false", "node scripts/runewaker-pipeline/compress-environment-textures.mjs --concurrency 1 --jobs 2", ]; data.surfaceQuality = review; } else data.continuationIntegrityProof = { ...data.continuationIntegrityProof, surfaceQuality: review }; edits.push({ file, data }); } } } // Validate every current/candidate identity before the first mutation. Exclusive // backups preserve the complete previous assets and prevent accidental replays. const backup = `${stage}/pre-surface-promotion-backup`; for (const row of [...copies, ...edits]) { const target = path.join(backup, row.file); await mkdir(path.dirname(target), { recursive: true }); await copyFile(row.file, target, constants.COPYFILE_EXCL); } for (const row of copies) await copyFile(row.candidate, row.file); for (const row of edits) await writeFile(row.file, `${JSON.stringify(row.data, null, 2)}\n`); await writeFile(`${stage}/surface-promotion-proof.json`, `${JSON.stringify({ manifest: manifestFile, backup, reviews, copies }, null, 2)}\n`); console.log(JSON.stringify({ promoted: reviews.map(row => row.dungeonId), backup }));