// Browser leg of statevector-equivalence, for both capabilities. // // 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. // It is a manual tool: run it on the bundle before the website deploys it. // // node browser_verify.mjs [path/to/quantum-ml/pkg] [module file] // // In headless Chromium, with the wasm-bindgen bundle (module file // `wai_quantum_ml.js` by default): // // - QuantumSink opens every file of `vectors/` (wai.quantum.circuit) and // `v2/vectors/` (wai.quantum.circuit2) to its manifest entry: the capability (an // entry of the circuit/1 manifest names none), n_qubits, n_ops, the circuit and // statevector hashes, and the histogram hash where the entry pins a measurement. // - QuantumSink refuses every file `reject/reject.json` lists, and its error names // the listed class in parentheses: ` (): `. // - QuantumSink reads every file `accept/accept.json` lists as listed: capability, // n_qubits, n_ops, circuit hash and measurement. A file too wide to simulate // (n_qubits 255) is read and then refused at the `simulate` stage, never at // `WQC parse`. // - QuantumIde.load_algorithm builds bell, ghz and qft in its default gateset to // `v2/expected_hashes.json`, and after `set_gateset(1)` to the circuit/1 manifest. // // Needs Playwright with its bundled Chromium. `statevector_hash_hex()` of the IDE 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 MODULE = process.argv[3] || "wai_quantum_ml.js"; const PLAYWRIGHT_HOST = process.env.PLAYWRIGHT_HOST || "/Users/dcharlot/data-share/vibe-coding/joulesperbit/"; const { chromium } = createRequire(PLAYWRIGHT_HOST)("playwright"); const readJson = (p) => JSON.parse(fs.readFileSync(p, "utf8")); const bytesOf = (p) => Array.from(fs.readFileSync(p)); // The two vector trees. An entry of the circuit/1 manifest names no capability. const TREES = [ { prefix: "", dir: HERE, capability: "wai.quantum.circuit" }, { prefix: "v2/", dir: path.join(HERE, "v2"), capability: null }, ]; const MANIFEST = Object.fromEntries(TREES.map((t) => [t.prefix, readJson(path.join(t.dir, "expected_hashes.json"))])); // Every vector file, with what the sink must report for it. const SINK = []; for (const t of TREES) { for (const [file, e] of Object.entries(MANIFEST[t.prefix])) { SINK.push({ label: t.prefix + file, bytes: bytesOf(path.join(t.dir, "vectors", file)), want: { capability: e.capability || t.capability, n_qubits: e.n_qubits, n_ops: e.n_ops, circuit: e.circuit_hash, statevector: e.statevector_hash, histogram: e.measure ? e.measure.histogram_hash : null, }, }); } } // Every reject file, with the class it must be refused as. const REJECT_DIR = path.join(HERE, "reject"); const REJECT = Object.entries(readJson(path.join(REJECT_DIR, "reject.json"))).map(([file, kind]) => ({ label: "reject/" + file, bytes: bytesOf(path.join(REJECT_DIR, file)), kind, })); // Every accept file, with its reading. seed and shots are u64, beyond what a JSON // number keeps exactly in JS, so they are compared as decimal strings. const ACCEPT_DIR = path.join(HERE, "accept"); const acceptText = fs.readFileSync(path.join(ACCEPT_DIR, "accept.json"), "utf8") .replace(/"(seed|shots)": (\d+)/g, '"$1": "$2"'); const ACCEPT = Object.entries(JSON.parse(acceptText)).map(([file, e]) => ({ label: "accept/" + file, bytes: bytesOf(path.join(ACCEPT_DIR, file)), want: { capability: e.capability, n_qubits: e.n_qubits, n_ops: e.n_ops, circuit: e.circuit_hash, measure: e.measure ? `${e.measure.seed} ${e.measure.shots}` : null, }, })); // The IDE's catalog: vector file -> (algorithm id, qubits), in each gateset against // its manifest. Gateset 2 (circuit2) is the IDE's default; 1 is set explicitly. const CATALOG = [ ["bell.wqc", "bell", 2], ["ghz3.wqc", "ghz", 3], ["qft3.wqc", "qft", 3], ["qft5.wqc", "qft", 5], ]; const IDE = []; for (const [gateset, prefix, capability] of [[2, "v2/", "wai.quantum.circuit2"], [1, "", "wai.quantum.circuit"]]) { for (const [file, id, n] of CATALOG) { const e = MANIFEST[prefix][file]; IDE.push({ label: `QuantumIde ${id}(${n}) in gateset ${gateset} = ${prefix}${file}`, id, n, gateset, want: { gateset, capability, circuit: e.circuit_hash, statevector: e.statevector_hash }, }); } } 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('conformanceready'); } 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); }); // Compare the fields of `want` with `got`; the names of those that differ. function differ(want, got) { return Object.keys(want).filter((k) => got[k] !== want[k]); } let browser; process.on("unhandledRejection", (e) => { console.error("UNHANDLED:", e); process.exitCode = 1; }); try { if (!fs.existsSync(path.join(PKG, MODULE))) { console.error(`no ${MODULE} at ${PKG}; pass the pkg directory (and the module file) as arguments`); 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 ({ sink, reject, accept, ide, port, module }) => { const mod = await import(`http://127.0.0.1:${port}/${module}`); await mod.default(); const message = (e) => String((e && e.message) || e); const out = { sink: {}, reject: {}, accept: {}, ide: {} }; for (const c of sink) { try { const s = new mod.QuantumSink(new Uint8Array(c.bytes)); out.sink[c.label] = { capability: s.capability(), n_qubits: s.n_qubits(), n_ops: s.n_ops(), circuit: s.circuit_hash_hex(), statevector: s.statevector_hash_hex(), histogram: s.histogram_hash_hex() ?? null, }; s.free(); } catch (e) { out.sink[c.label] = { error: message(e) }; } } for (const c of reject) { try { new mod.QuantumSink(new Uint8Array(c.bytes)).free(); out.reject[c.label] = { accepted: true }; } catch (e) { out.reject[c.label] = { error: message(e) }; } } for (const c of accept) { try { const s = new mod.QuantumSink(new Uint8Array(c.bytes)); out.accept[c.label] = { capability: s.capability(), n_qubits: s.n_qubits(), n_ops: s.n_ops(), circuit: s.circuit_hash_hex(), measure: s.has_measure() ? `${s.seed()} ${s.shots()}` : null, }; s.free(); } catch (e) { out.accept[c.label] = { error: message(e) }; } } for (const c of ide) { const q = new mod.QuantumIde(c.n); try { if (c.gateset !== 2 && !q.set_gateset(c.gateset)) { out.ide[c.label] = { error: `set_gateset(${c.gateset}) returned false` }; continue; } if (!q.load_algorithm(c.id, c.n, 0)) { out.ide[c.label] = { error: "load_algorithm returned false" }; continue; } q.run(); // the state is not computed until this out.ide[c.label] = { gateset: q.gateset(), capability: q.capability(), circuit: q.circuit_hash_hex(), statevector: q.statevector_hash_hex(), }; } catch (e) { out.ide[c.label] = { error: message(e) }; } finally { q.free(); } } return out; }, { sink: SINK, reject: REJECT, accept: ACCEPT, ide: IDE, port: PORT, module: MODULE }); let fail = 0; const report = (label, problem) => { if (problem) { fail++; console.log(`FAIL ${label}: ${problem}`); } else { console.log(`PASS ${label}`); } }; for (const c of SINK) { const g = got.sink[c.label]; if (g.error) { report(c.label, `refused: ${g.error}`); continue; } const bad = differ(c.want, g); report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); } for (const c of REJECT) { const g = got.reject[c.label]; if (g.accepted) { report(c.label, `accepted; it must be refused as ${c.kind}`); continue; } const m = /^[^()]+ \(([a-z-]+)\): /.exec(g.error); report(`${c.label} refused as ${c.kind}`, m && m[1] === c.kind ? null : `refused as: ${g.error}`); } for (const c of ACCEPT) { const g = got.accept[c.label]; if (g.error) { const wide = c.want.n_qubits === 255 && /^simulate \(/.test(g.error); report(wide ? `${c.label} read; too wide to simulate` : c.label, wide ? null : `refused: ${g.error}`); continue; } const bad = differ(c.want, g); report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); } for (const c of IDE) { const g = got.ide[c.label]; if (g.error) { report(c.label, g.error); continue; } const bad = differ(c.want, g); report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); } console.log(errs.length ? `console errors: ${errs.length}\n ${errs.join("\n ")}` : "console errors: 0"); if (fail === 0 && errs.length === 0) { console.log("all checks pass: statevector-equivalence holds in the browser for both capabilities, every reject file is refused as listed, and every accept file reads as listed"); } 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(); }