The extension claims identical reconstruction native, in the browser, and at the edge. Native and wasm32-wasip2 were checkable with the existing vectors binary; the browser was not checkable by anything, so it was the one surface taken on trust. browser_verify.mjs drives the deployed wasm-bindgen bundle in headless Chromium and holds it to expected_hashes.json -- the same corpus pinned 2026-07-30, so all three surfaces are now checked against one reference rather than against each other. All four vectors pass, circuit hash and statevector hash both, with no console errors. Two things the harness had to learn, now written down: statevector_hash_hex() returns empty until run() is called, because the circuit is built eagerly and the state is not -- which reads as a hash mismatch rather than as a missing step; and the bundle must be served over HTTP rather than file:// for the wasm import to resolve. Adds the README the directory never had, covering all three invocations and stating the corpus's scope honestly: it pins the byte-exact dyadic core, where hash equality is the right bar. The f64 layers have no WQC circuit to pin and are held to exact IEEE-754 bits by unit tests instead. Co-Authored-By: Claude Opus 5 <[email protected]>
106 lines
4.3 KiB
JavaScript
106 lines
4.3 KiB
JavaScript
// Browser leg of statevector-equivalence.
|
|
//
|
|
// The native and `wasm32-wasip2` legs are checked by `wai_quantum_vectors verify`.
|
|
// This checks the third surface the extension claims — a browser — against the
|
|
// same pinned corpus, so all three are held to one reference rather than to each
|
|
// other.
|
|
//
|
|
// node browser_verify.mjs [path/to/quantum-ml/pkg]
|
|
//
|
|
// Needs Playwright with its bundled Chromium. `statevector_hash_hex()` is empty
|
|
// until `run()` is called: the circuit is built eagerly, the state is not.
|
|
import http from "node:http";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { createRequire } from "node:module";
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const PKG = process.argv[2] ||
|
|
"/Users/dcharlot/data-share/vibe-coding/wai-transaction-science-web/public/quantum-ml/pkg";
|
|
const PLAYWRIGHT_HOST = process.env.PLAYWRIGHT_HOST ||
|
|
"/Users/dcharlot/data-share/vibe-coding/joulesperbit/";
|
|
|
|
const { chromium } = createRequire(PLAYWRIGHT_HOST)("playwright");
|
|
const EXPECTED = JSON.parse(fs.readFileSync(path.join(HERE, "expected_hashes.json"), "utf8"));
|
|
|
|
// vector name -> how the catalog builds that circuit
|
|
const CASES = [
|
|
["bell.wqc", "bell", 2, 0],
|
|
["ghz3.wqc", "ghz", 3, 0],
|
|
["qft3.wqc", "qft", 3, 0],
|
|
["qft5.wqc", "qft", 5, 0],
|
|
];
|
|
|
|
const MIME = { ".js": "text/javascript", ".wasm": "application/wasm", ".html": "text/html" };
|
|
const PORT = Number(process.env.PORT || 8794);
|
|
const server = http.createServer((req, res) => {
|
|
const url = req.url.split("?")[0];
|
|
if (url === "/") {
|
|
res.writeHead(200, { "content-type": "text/html" });
|
|
return res.end('<!doctype html><meta charset="utf-8"><title>conformance</title><body>ready</body>');
|
|
}
|
|
const f = path.join(PKG, path.basename(url));
|
|
if (!fs.existsSync(f)) { res.writeHead(404); return res.end("not found"); }
|
|
res.writeHead(200, { "content-type": MIME[path.extname(f)] || "application/octet-stream" });
|
|
fs.createReadStream(f).pipe(res);
|
|
});
|
|
|
|
let browser;
|
|
process.on("unhandledRejection", (e) => { console.error("UNHANDLED:", e); process.exitCode = 1; });
|
|
|
|
try {
|
|
if (!fs.existsSync(path.join(PKG, "wai_quantum_ml.js"))) {
|
|
console.error(`no bundle at ${PKG} — pass the pkg directory as argv[1]`);
|
|
process.exit(2);
|
|
}
|
|
await new Promise((r) => server.listen(PORT, "127.0.0.1", r));
|
|
browser = await chromium.launch();
|
|
const page = await browser.newPage();
|
|
const errs = [];
|
|
page.on("console", (m) => { if (m.type() === "error") errs.push(m.text()); });
|
|
page.on("pageerror", (e) => errs.push(String(e)));
|
|
// domcontentloaded, never networkidle: the page holds no long-lived connections
|
|
// but networkidle still stalls behind them on some hosts.
|
|
await page.goto(`http://127.0.0.1:${PORT}/`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
|
|
const got = await page.evaluate(async ({ cases, port }) => {
|
|
const mod = await import(`http://127.0.0.1:${port}/wai_quantum_ml.js`);
|
|
await mod.default();
|
|
const out = {};
|
|
for (const [name, id, n, param] of cases) {
|
|
const ide = new mod.QuantumIde(n);
|
|
const ok = ide.load_algorithm(id, n, param);
|
|
if (ok) ide.run(); // the state is not computed until this
|
|
out[name] = ok
|
|
? { circuit: ide.circuit_hash_hex(), statevector: ide.statevector_hash_hex() }
|
|
: { error: "load_algorithm returned false" };
|
|
ide.free();
|
|
}
|
|
return out;
|
|
}, { cases: CASES, port: PORT });
|
|
|
|
let fail = 0;
|
|
for (const [name] of CASES) {
|
|
const e = EXPECTED[name], g = got[name];
|
|
if (g.circuit === e.circuit_hash && g.statevector === e.statevector_hash) {
|
|
console.log(`PASS ${name}`);
|
|
} else {
|
|
fail++;
|
|
console.log(`FAIL ${name}`);
|
|
console.log(` circuit browser=${g.circuit}\n corpus =${e.circuit_hash}`);
|
|
console.log(` statevector browser=${g.statevector}\n corpus =${e.statevector_hash}`);
|
|
}
|
|
}
|
|
console.log(errs.length ? `console errors: ${errs.length}` : "console errors: 0");
|
|
if (fail === 0 && errs.length === 0) {
|
|
console.log("all vectors pass: statevector-equivalence holds in the browser");
|
|
}
|
|
process.exitCode = fail || errs.length ? 1 : 0;
|
|
} catch (e) {
|
|
console.error("HARNESS ERROR:", e);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
server.close();
|
|
}
|