open-standards/wai/quantum-conformance/browser_verify.mjs
Transaction Science d06afe78dd wai-quantum circuit2: a strict I-JSON reader, the rules stated in full, an accept corpus
A second review of the I-JSON fix upheld two defects.

- The Rust reader left the number-range rule to serde_json. Built without
  float_roundtrip, its fast path is not correctly rounded, so near the largest
  double the reader and the referee disagreed both ways, and in WQC2 on the
  class; and whether wai-rs features that enable float_roundtrip were linked
  changed the verdict.
- The spec claimed I-JSON, but neither reader refused noncharacters.

wai-quantum now reads a contract or measurement section with a strict reader of
its own (src/ijson.rs) that decides every rule from the text: UTF-8; one JSON
value with JSON whitespace; no member name twice, compared after unescaping; no
surrogate or noncharacter, raw or escaped; no number of magnitude
2^1024 − 2^970 or more, decided by std's correctly rounding parse; at most 127
levels. A number keeps its literal, and an unsigned integer is one written with
no sign, fraction or exponent. The serde dependency added for the visitor is
gone. The referee decides the range rule on the literal's digits, integers
only, and names the rule it refuses by.

quantum-sim §2 states the rules in full, says that only the class is normative
when a section breaks several, and that a reader reads integers exactly. §8
adds the noncharacter to the newly refused payloads and notes the numbers just
below the limit that some earlier builds refused.

`accept_corpus()` holds payloads at the edges of the rules that a reader must
accept, each with its circuit and measurement; the corpus gains `accept/` and
`accept.json`. `gen`, `verify`, the referee and the browser check read it. The
reject corpus gains the boundary cases (the integer 2^1024 − 2^970, a literal
just above it, the WQC2 n_qubits forms on both sides of the class order),
noncharacters, a lone low surrogate, a BOM, NaN and a raw control character,
and the referee and the crate check each JSON case's rule. The wasm32-wasip2
leg runs the reader's tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:46:02 -04:00

282 lines
11 KiB
JavaScript

// 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: `<stage> (<class>): <reason>`.
// - 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('<!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);
});
// 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();
}