Files
aetherbound-guild/docs/runtime/p9_1/visual_slice/verify.mjs
T

74 lines
3.8 KiB
JavaScript

import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { chromium } from "playwright";
import { createServer } from "node:http";
const root = fileURLToPath(new URL(".", import.meta.url));
const manifest = JSON.parse(await readFile(join(root, "frame-manifest.json"), "utf8"));
const assets = JSON.parse(await readFile(join(root, "asset-manifest.json"), "utf8"));
const errors = [];
const hashes = {};
for (const asset of assets.assets) {
const local = await readFile(join(root, asset.path));
const source = await readFile(join(root, "../../../..", asset.source));
const localHash = createHash("sha256").update(local).digest("hex");
const sourceHash = createHash("sha256").update(source).digest("hex");
hashes[asset.path] = { source: asset.source, sha256: localHash };
if (localHash !== sourceHash) errors.push(`source hash mismatch: ${asset.path}`);
}
const mime = { ".html":"text/html", ".css":"text/css", ".js":"text/javascript", ".png":"image/png", ".ttf":"font/ttf" };
const server = createServer(async (request, response) => {
const rawPath = decodeURIComponent((request.url || "/").split("?")[0]);
const relativePath = rawPath === "/" ? "index.html" : rawPath.replace(/^\//, "");
try {
const { extname } = await import("node:path");
const body = await readFile(join(root, relativePath));
response.writeHead(200, { "content-type": mime[extname(relativePath)] || "application/octet-stream" });
response.end(body);
} catch { response.writeHead(404); response.end("Not found"); }
});
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address();
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 844, height: 390 }, deviceScaleFactor: 1 });
for (const frame of manifest.frames) {
await page.goto(`http://127.0.0.1:${port}/index.html?frame=${frame.id}`, { waitUntil: "networkidle" });
const state = await page.evaluate(() => {
const slice = document.querySelector("#slice");
const nodes = [...document.querySelectorAll("body *")];
const overflow = nodes.filter(node => {
const r = node.getBoundingClientRect();
return r.width > 0 && r.height > 0 && (r.left < -0.5 || r.top < -0.5 || r.right > 844.5 || r.bottom > 390.5);
}).map(node => `${node.tagName}.${node.className}`);
return {
ready: document.documentElement.dataset.ready,
frame: slice?.dataset.frame,
overflow,
actors: document.querySelectorAll(".layer-actors img").length,
text: document.body.innerText
};
});
if (state.ready !== "true" || state.frame !== frame.id) errors.push(`frame not ready: ${frame.id}`);
if (state.overflow.length) errors.push(`viewport overflow ${frame.id}: ${state.overflow.join(", ")}`);
if (state.actors < 2) errors.push(`missing actors: ${frame.id}`);
if (!state.text.includes("妮娅") || !state.text.includes("赛布尔")) errors.push(`identity copy missing: ${frame.id}`);
}
await browser.close();
server.close();
const generation = JSON.parse(await readFile(join(root, "generation-record.json"), "utf8"));
if (generation.paid_calls_used > generation.authorized_call_ceiling) errors.push("paid call ceiling exceeded");
if (generation.returned_cost_usd > generation.authorized_cost_ceiling_usd) errors.push("paid cost ceiling exceeded");
const report = { ok: errors.length === 0, viewport: manifest.viewport, frame_count: manifest.frames.length, asset_count: assets.assets.length, paid_calls_used: generation.paid_calls_used, paid_cost_usd: generation.returned_cost_usd, errors, hashes };
await writeFile(join(root, "evidence", "verification.json"), JSON.stringify(report, null, 2) + "\n");
console.log(JSON.stringify(report, null, 2));
if (errors.length) process.exitCode = 1;