open-standards/wai/quantum-studio/test/studio.test.mjs
Transaction Science 94fabb574d wai-quantum: whether designs converge on a model's, and a while on one's own
Feature 12 of the co-design report: a convergence monitor.

- engine::converge:
  - history: for each version the person made after a model proposed,
    its structural distance from the proposals in its line of work (the
    whole proposal) and the model's share of its gates; converging when
    most of the program is the model's or the last versions come closer
    each time, with why; versions noted "organic exploration" (the
    assistant-off probes) set beside the rest;
  - cohort: the Vendi score over 1 - distance (Jacobi eigenvalues), each
    program's distance to the centroid of the gate-token bags, the medoid
    and the outlier, and with times the score in windows and a narrowing;
  - call for hosts. novelty::bag is shared.
- The command line: converge history | cohort (a file's time from its
  history, else the file's).
- MCP: converge_cohort and converge_history.
- The Studio: "Your own way" under Versions, and "Explore on my own for a
  while": the assistant only asks and offers no program, Explore and Ask
  the model wait, the person's versions are noted; End lets the model
  propose again.
- Bindings: engine_converge in the page, converge in the desktop app.
- CI: wai-web's native engine tests count 13.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 16:05:43 -04:00

746 lines
63 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// The Studio in the installed browser, as a reader uses it:
//
// node build.mjs --pkg PATH/TO/ENGINE/PKG && node test/studio.test.mjs
//
// Serves dist/ (with the engine package in dist/pkg/); the page's index.html names the
// live job service, https://wai.transaction.science/quantum-jobs, which the service checks use.
// - the first example loads; the program is linted, drawn and planned as it is typed;
// - it runs in this browser: the stabilizer tableau, only 00 and 11, ⟨Z0 Z1⟩ = 1, the
// receipt checked here and signed by this browser's key;
// - an error in the program is marked on its line;
// - a rotation runs on the state vector, ⟨Z0⟩ = cos 0.3;
// - the same Bell job on the service gives the counts run here, shot for shot, and its
// receipt checks here as the service's (left out with STUDIO_SKIP_SERVICE=1);
// - Ctrl+Enter in the editor runs; the program and the history survive a reload;
// - Versions: each change recorded with who made it; a proposal taken change by
// change; whose hand each line and gate is in; the receipt binding it; a line
// of work from an older version merged back; kept and downloaded;
// - Rules: held lines marked, shaded and drawn; a gate set and a region; a
// proposal's breaking changes shown with their rule and never taken; the
// model told the rules;
// - the critique: its measures as the program changes and their change since the last
// run; a smell named with its lines, flagged on the diagram, shown, and fixed by the
// engine's rewrite, checked the same circuit;
// - Alternatives: Improve offers the engine's rewrite checked equivalent, plotted and
// applied; Explore without a model says it needs one; a rewrite that reaches the GHZ
// state by a different route is labelled so and flagged in Found, saved there with a
// note, and kept with the page; an alternative that is the same circuit, taken whole, is
// a choice the reader's taste learns from, and the card says how the draft becomes it, as
// a chain of checked rewrites;
// - the rewrite lens: each rewrite the program admits, its gates marked on the diagram,
// one taken as the engine's proposal, and the list following the program;
// - your own way: a while on one's own, kept in this browser, Explore waiting for it, a
// version made then noted so, the convergence line, and its end;
// - the classical-shortcut lens: a Clifford ring with a closing rotation layer priced by
// the cut-rank sum, a Clifford circuit easy at any width, a contraction searched only
// when thorough, and a check marked stale once the program changes;
// - predict first: the form stops a run until the reader predicts, refuses an outcome of
// the wrong width, and sets the prediction against the counts;
// - the assistant, against a scripted model server: a tool call run on the engine, a
// proposal the engine cannot read sent back with its reason, the corrected one checked
// and run before it is offered, and Apply; it asks questions first by default and speaks
// only when asked; offering a hint after refused runs only when the reader allows it,
// and never asking the model itself; Improve and Explore with the model, each candidate
// read, measured and compared by the engine, a rewrite that is not the same circuit
// left out with its reason;
// - generators: a program with an integer input shows it under the editor, is linted and
// drawn at its value, and runs there; the Workbench starts from it, judges a hundred
// blind edits, keeps the reader's log-depth tree as the cheapest, opens it in the
// editor, and comes back with the page; the tree is named a known construction, GHZ by
// doubling, with what it beats the reference on that the cost does not weigh; on a
// line, the tree is an aberration, offered with the rule to drop, and flagged; your taste:
// two kept generators compared and one chosen for its economy, the archive ranked by the
// profile with each one's typicality, the dial turned, a second profile kept apart, and
// the profiles kept with the page; with the
// model, its generators are judged at every size, a wrong one and one with a gate the
// specification forbids left out (an aberration), and it is told the specification and
// shown the cheapest found; its CZ idea in Explore is a different route to GHZ;
// - no console error and no failed request; at a phone's width nothing scrolls sideways.
// Exit 0 iff every check passes.
import { createServer } from 'node:http';
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, extname, join, normalize, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default;
const DIST = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
const SHOTS = resolve(dirname(fileURLToPath(import.meta.url)), 'screenshots');
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.wasm': 'application/wasm', '.json': 'application/json' };
const server = createServer((req, res) => {
let file = normalize(join(DIST, decodeURIComponent(new URL(req.url, 'http://x').pathname)));
if (!file.startsWith(DIST)) { res.writeHead(403).end(); return; }
if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html');
if (!existsSync(file)) { res.writeHead(404).end('not found'); return; }
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' });
res.end(readFileSync(file));
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const ORIGIN = `http://127.0.0.1:${server.address().port}`;
let fails = 0;
const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; };
const LONG = 120_000;
const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true });
async function open(viewport = { width: 1400, height: 950 }) {
const ctx = await browser.newContext({ viewport, colorScheme: 'dark' });
const page = await ctx.newPage();
const seen = { errors: [], failed: [] };
page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); });
page.on('pageerror', (e) => seen.errors.push(String(e)));
page.on('requestfailed', (r) => seen.failed.push(`${r.url()} (${r.failure()?.errorText})`));
page.on('response', (r) => { if (r.status() >= 400) seen.failed.push(`${r.url()} (${r.status()})`); });
await page.goto(ORIGIN + '/', { waitUntil: 'domcontentloaded' });
return { ctx, page, seen };
}
const done = (page) => page.waitForFunction(() => !/^Running|^On the service/.test(document.querySelector('.qs-status')?.textContent ?? '') && document.querySelector('#qs-run') && !document.querySelector('#qs-run').disabled, null, { timeout: LONG });
const verified = (page) => page.waitForFunction(() => /verifies|does not verify|not checked/.test(document.querySelector('#qs-verified')?.textContent ?? ''), null, { timeout: LONG });
const counts = (page) => page.$$eval('.qs-bar', (bs) => bs.map((b) => [b.querySelector('.qs-key').textContent, b.querySelector('.qs-n').textContent]));
const { ctx, page, seen } = await open();
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
check((await page.textContent('#qs-lint')) === '2 qubits · 2 gates', `the first example is linted as the engine reads it (${await page.textContent('#qs-lint')})`);
await page.waitForFunction(() => document.querySelector('#qs-circuit')?.textContent.includes('⊕'), null, { timeout: LONG });
const drawing = await page.textContent('#qs-circuit');
check(/q0: .*H.*●/.test(drawing) && /q1: .*⊕/.test(drawing), 'the circuit is drawn as the program is typed');
const svg = await page.$eval('#qs-diagram svg', (g) => ({ label: g.getAttribute('aria-label'), rings: g.querySelectorAll('.qc-ring').length, dots: g.querySelectorAll('.qc-dot').length, joins: g.querySelectorAll('.qc-join').length, meters: g.querySelectorAll('.qc-measure').length }));
check(svg.rings === 1 && svg.dots === 1 && svg.joins === 1 && svg.meters === 2 && /2 qubits/.test(svg.label), `the circuit is drawn as a diagram with a text alternative (${JSON.stringify(svg)})`);
await page.click('#qs-astext');
check(await page.isVisible('#qs-circuit') && await page.isHidden('#qs-diagram'), '“Text” shows the circuit as text to copy');
await page.click('#qs-astext');
await page.waitForFunction(() => /Stabilizer tableau/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
check(true, `the plan is shown before running (${await page.textContent('#qs-plan')})`);
await page.click('#qs-run');
await done(page);
await verified(page);
check((await page.textContent('.qs-method')) === 'Stabilizer tableau', 'run in this browser: the stabilizer tableau');
const here = await counts(page);
check(here.length === 2 && here.every(([k]) => k === '00' || k === '11'), `only 00 and 11 (${JSON.stringify(here)})`);
const zz = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
check(zz.some((t) => t.startsWith('⟨Z0 Z1⟩') && t.includes('1.000000')), `⟨Z0 Z1⟩ = 1 (${zz.slice(0, 2).join(' | ')})`);
check((await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the receipt verifies, checked here (${await page.textContent('#qs-verified')})`);
check((await page.textContent('#qs-signed-by')).startsWith('this browser’s key did:key:z6Mk'), `signed by this browser's key (${await page.textContent('#qs-signed-by')})`);
check((await page.$$('.qs-hitem')).length === 1, 'the run is in the history');
// What it looks like, for a reader of the change: the dark scheme and the light.
mkdirSync(SHOTS, { recursive: true });
await page.screenshot({ path: join(SHOTS, 'studio-dark.png'), fullPage: true });
await page.emulateMedia({ colorScheme: 'light' });
await page.screenshot({ path: join(SHOTS, 'studio-light.png'), fullPage: true });
await page.emulateMedia({ colorScheme: 'dark' });
// An error is marked on its line.
await page.click('.qs-editor .cm-content');
await page.keyboard.press('ControlOrMeta+End');
await page.keyboard.type('\nfoo q[0];');
await page.waitForFunction(() => document.querySelector('#qs-lint')?.textContent === 'problem', null, { timeout: LONG });
await page.waitForSelector('.cm-lintRange-error', { timeout: LONG });
check(true, 'an unknown gate is marked in the editor');
for (let i = 0; i < 10; i++) await page.keyboard.press('Backspace');
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
// A rotation: any angle, on the state vector.
await page.selectOption('#qs-example', 'rotation');
await page.waitForFunction(() => /State vector/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
await page.click('#qs-run');
await done(page);
const z = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
check((await page.textContent('.qs-method')) === 'State vector, double precision' && z.some((t) => t.startsWith('⟨Z0⟩') && t.includes(Math.cos(0.3).toFixed(6))), `rx(0.3) on the state vector, ⟨Z0⟩ = cos 0.3 (${z[0]})`);
// The same Bell job on the service, and here: the same counts.
// (STUDIO_SKIP_SERVICE=1 leaves it out, where a build may not reach the service.)
if (process.env.STUDIO_SKIP_SERVICE) {
console.log('SKIP the service checks (STUDIO_SKIP_SERVICE)');
} else {
await page.selectOption('#qs-example', 'bell');
await page.click('#qs-run'); await done(page); await verified(page);
const local = await counts(page);
await page.click('.qs-seg[data-where=service]');
await page.click('#qs-run'); await done(page); await verified(page);
const remote = await counts(page);
check(JSON.stringify(remote) === JSON.stringify(local), `the service gives the counts run here, shot for shot (${JSON.stringify(remote)})`);
check((await page.textContent('#qs-signed-by')).startsWith('the service’s key') && (await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the service's receipt checks here, as the service's (${await page.textContent('#qs-signed-by')})`);
await page.click('.qs-seg[data-where=local]');
}
// Ctrl+Enter runs; the program and the history survive a reload. (A job run again
// unchanged gives the same signed result, which the history keeps once; so this one
// asks for other shots.)
const before = (await page.$$('.qs-hitem')).length;
await page.fill('#qs-shots', '321');
await page.click('.qs-editor .cm-content');
await page.keyboard.press('ControlOrMeta+Enter');
await done(page);
check((await page.$$('.qs-hitem')).length === before + 1, 'Ctrl+Enter in the editor runs the program');
const program = await page.evaluate(() => window.quantumStudio.view.state.doc.toString());
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
check((await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === program, 'the program survives a reload');
await page.waitForFunction((n) => document.querySelectorAll('.qs-hitem').length >= n, before + 1, { timeout: LONG });
check(true, 'the history survives a reload');
// The critique, as the program changes.
const doc = () => page.evaluate(() => window.quantumStudio.view.state.doc.toString());
const setProgram = async (q) => { await page.evaluate((q) => window.quantumStudio.fill({ qasm: q }), q); await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, q); };
const chip = (k) => page.$eval(`#qs-measures [data-k=${k}]`, (c) => c.textContent).catch(() => '');
{
await page.selectOption('#qs-example', 'bell');
await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG });
const m = await page.textContent('#qs-measures');
check(/2 qubits/.test(m) && /1 two-qubit/.test(m) && await page.isVisible('#qs-clifford') && /fidelity, modelled/.test(m) && (await page.$$('#qs-smells li')).length === 0,
`the critique gives the Bell pair's measures, that it is Clifford, a modelled fidelity, and no smells (${m})`);
const SMELLY = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n';
await setProgram(SMELLY);
await page.waitForSelector('.qs-smell[data-kind=cancels]', { timeout: LONG });
const smell = await page.textContent('.qs-smell[data-kind=cancels] .qs-smell-text');
check(smell === 'x on qubit 1 twice in a row (lines 5 and 6): together they do nothing', `a smell is named with its lines (${smell})`);
check((await page.$$('#qs-diagram .qc-item.qc-flag')).length === 2, 'its two gates are flagged on the diagram');
await page.hover('.qs-smell[data-kind=cancels]');
check((await page.$$('#qs-diagram .qc-item.qc-smell')).length === 2 && (await page.$$('#qs-diagram .qc-wire.qc-smell')).length === 0, 'pointing at the smell marks its gates, and not their wires');
await page.click('.qs-smell[data-kind=cancels] button >> text=Show');
const sel = await page.evaluate(() => { const v = window.quantumStudio.view, d = v.state.doc, r = v.state.selection.main; return [r.from === d.line(5).from, r.to === d.line(6).to]; });
check(sel[0] && sel[1], 'Show selects its lines in the editor');
await page.click('#qs-run'); await done(page);
await page.click('.qs-smell[data-kind=cancels] .qs-fix');
await page.waitForFunction(() => !/x q\[1\];/.test(window.quantumStudio.view.state.doc.toString()) && /checked the result/.test(document.querySelector('#qs-shows')?.textContent ?? ''), null, { timeout: LONG });
const shows = await page.textContent('#qs-shows');
check(/^Fixed: x on qubit 1/.test(shows) && /same tableau/.test(shows), `Fix applies the engine's rewrite, checked the same circuit (${shows})`);
await page.waitForFunction(() => /−2/.test(document.querySelector('#qs-measures [data-k=gates]')?.textContent ?? ''), null, { timeout: LONG });
check(true, `the measures show their change since the last run (${await chip('gates')})`);
await page.click('.qs-editor .cm-content');
await page.keyboard.press('ControlOrMeta+z');
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, SMELLY, { timeout: LONG });
check(true, 'Undo brings the fixed lines back');
await setProgram('OPENQASM 3.0;\nqubit[3] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\n');
await page.waitForSelector('.qs-smell[data-kind=idle]', { timeout: LONG });
check((await page.$$('#qs-diagram .qc-wire.qc-flag')).length === 2 && !(await page.$('.qs-smell[data-kind=idle] .qs-fix')), 'qubits nothing uses are flagged on their wires, with no Fix');
}
// Alternatives: Improve, without a model; Explore says it needs one.
{
const MERGE = 'OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nrz(0.25) q[0];\nrz(0.5) q[0];\nc[0] = measure q[0];\n';
await setProgram(MERGE);
await page.click('#qs-improve');
await page.waitForSelector('.qs-cand', { timeout: LONG });
const card = await page.textContent('.qs-cand');
check(/the engine/.test(card) && /same circuit · on every basis state/.test(card) && /2 gates/.test(card), `Improve offers the engine's rewrite, checked on every basis state (${card.slice(0, 160)})`);
check(await page.isVisible('#qs-atlas') && (await page.$$('#qs-atlas .qs-dot')).length === (await page.$$('.qs-cand')).length && (await page.$$('#qs-atlas .qs-ring')).length === 1, 'the atlas plots the draft and each alternative');
check(/Connect a model/.test(await page.textContent('#qs-alt-body')), 'without a model, Improve says the engine’s are the only ones');
check(/How: 1 rewrite, each checked/.test(card) && card.includes('rz(0.25) q[0] and rz(0.5) q[0] merge into rz(0.75) q[0]'), `the card says how the draft becomes it, a chain of checked rewrites (${card.slice(0, 220)})`);
check((await doc()) === MERGE, 'nothing changes the program until an alternative is applied');
await page.click('.qs-cand .qs-apply');
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
check((await doc()) === MERGE && /A proposal from the engine \(Alternatives\)/.test(await page.textContent('#qs-review')), 'Apply offers the alternative as the engine’s proposal, to take change by change');
await page.click('#qs-review-apply');
await page.waitForFunction(() => /rz\(0\.25 \+ 0\.5\) q\[0\];/.test(window.quantumStudio.view.state.doc.toString()), null, { timeout: LONG });
check(true, 'taking it puts the alternative in the editor');
await page.waitForFunction(() => document.querySelector('#qs-taste-count')?.textContent === '1', null, { timeout: LONG });
check(/^1 choice\b/.test(await page.textContent('#qs-taste-summary')), 'taken whole, an alternative that is the same circuit is a choice the reader’s taste learns from');
await page.click('#qs-explore');
await page.waitForSelector('#qs-alt-nomodel', { timeout: LONG });
check(true, 'Explore without a model says it needs one');
}
// What is new: a rewrite that is the GHZ state, built unlike the known chain, is a
// different route to it, and Found keeps it until the reader saves or dismisses it.
{
check((await page.textContent('#qs-found-count')) === '0 flagged · 0 saved' && await page.isVisible('#qs-found-empty'), 'Found starts empty');
await setProgram('OPENQASM 3.0;\nqubit[3] q;\nh q[2];\nx q[0];\nx q[0];\ncx q[2], q[1];\ncx q[1], q[0];\n');
await page.click('#qs-improve');
await page.waitForSelector('.qs-cand .qs-novelty', { timeout: LONG });
const labels = await page.$$eval('.qs-cand .qs-novelty', (bs) => bs.map((b) => [b.textContent, b.title]));
const route = labels.find(([t]) => t === 'a different route to GHZ, by a chain of CNOTs');
check(route && await page.$('.qs-cand.find'), `a rewrite that makes the GHZ state, built unlike the chain, is labelled a different route (${labels.map(([t]) => t).join(' | ')})`);
check(/0\.\d\d apart/.test(route?.[1] ?? ''), 'the label says how far apart they are built');
await page.waitForSelector('#qs-found-list .qs-found-item[data-kind=route]', { timeout: LONG });
check((await page.textContent('#qs-found-count')) === '1 flagged · 0 saved' && /different route/.test(await page.textContent('#qs-found-list')) && /Improve/.test(await page.textContent('#qs-found-list')), 'Found flags it, and says where it came from');
await page.click('#qs-found-list .qs-found-save');
await page.fill('#qs-found-list .qs-found-item input', 'the ladder, read backwards');
await page.dispatchEvent('#qs-found-list .qs-found-item input', 'change');
check((await page.textContent('#qs-found-count')) === '1 flagged · 1 saved' && (await page.textContent('#qs-found-list .qs-found-save')) === 'Saved', 'Save keeps it, and the count says so');
}
// The rewrite lens: each rewrite that keeps the meaning, checked, taken one at a time.
{
const LENS = 'OPENQASM 3.0;\nqubit[2] q;\nh q[1];\ncx q[0], q[1];\nh q[1];\nt q[0];\nt q[0];\n';
await setProgram(LENS);
// The diagram is drawn a moment after the edit; marks go on the one drawn.
// (The measurements it draws at the end carry no gate.)
await page.waitForFunction(() => /5 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && [...document.querySelectorAll('#qs-diagram .qc-item')].filter((g) => /^\d+$/.test(g.dataset.op ?? '')).length === 5, null, { timeout: LONG });
await page.click('#qs-rewrites');
await page.waitForSelector('#qs-rewrite-list li', { timeout: LONG });
const rows = await page.$$eval('#qs-rewrite-list li', (ls) => ls.map((l) => [l.dataset.rule, l.textContent]));
check(rows.some(([r, t]) => r === 'conjugate' && t.includes('cx q[0], q[1] between two h q[1] is cz q[0], q[1]')) && rows.some(([r, t]) => r === 'merge' && t.includes('t q[0] and t q[0] merge into s q[0]')),
`the lens lists each rewrite the program admits, in words (${rows.map(([r]) => r).join(', ')})`);
await page.hover('#qs-rewrite-list li[data-rule=conjugate]');
const marked = await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-rewrite').length === 3, null, { timeout: 5000 }).then(() => true, () => false);
check(marked, `pointing at one marks its gates on the diagram (${(await page.$$('#qs-diagram .qc-item.qc-rewrite')).length} marked)`);
await page.click('#qs-rewrite-list li[data-rule=conjugate] .qs-rewrite-take');
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
check(/A proposal from the engine \(Rewrite lens\)/.test(await page.textContent('#qs-review')) && (await doc()) === LENS, 'Take offers it as the engine’s proposal, to review');
await page.click('#qs-review-apply');
await page.waitForFunction(() => /cz q\[0\], q\[1\];/.test(window.quantumStudio.view.state.doc.toString()) && !/h q\[1\]/.test(window.quantumStudio.view.state.doc.toString()), null, { timeout: LONG });
await page.waitForFunction(() => !document.querySelector('#qs-rewrite-list li[data-rule=conjugate]') && document.querySelector('#qs-rewrite-list li[data-rule=merge]'), null, { timeout: LONG });
check(true, 'taken, the lens follows the program: the conjugation is gone and the merge is still offered');
}
// Your own way: whether the versions converge on the model's, and a while on one's own.
{
check(/^Explore on my own/.test(await page.textContent('#qs-organic')) && await page.isHidden('#qs-organic-chip'), 'Versions offers a while on one’s own');
await page.click('#qs-organic');
check(await page.isVisible('#qs-organic-chip') && (await page.getAttribute('#qs-organic', 'aria-pressed')) === 'true' && (await page.evaluate(() => localStorage.getItem('wai.quantum-studio.organic'))) === '1', 'on one’s own: marked, and kept in this browser');
await page.click('#qs-explore');
await page.waitForSelector('#qs-alt-paused', { timeout: LONG });
check(true, 'Explore, which asks the model, waits');
await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nz q[1];\n');
await page.evaluate(() => window.quantumStudio.versions.record());
const note = await page.evaluate(() => window.quantumStudio.versions.history().versions.at(-1).note);
check(note === 'organic exploration', `a version made on one’s own is noted so (${note})`);
await page.waitForFunction(() => /\.$/.test(document.querySelector('#qs-converge')?.textContent ?? ''), null, { timeout: LONG });
check(true, `the line says whether the versions converge on the model’s (${await page.textContent('#qs-converge')})`);
await page.click('#qs-organic');
check(await page.isHidden('#qs-organic-chip') && /^Explore on my own/.test(await page.textContent('#qs-organic')), 'ended, the model may be asked again');
}
// Versions: each change and who made it; a proposal taken change by change;
// whose hand each line and gate is in; the run's receipt binding the count; a
// line of work from an older version, brought back in; and all of it kept.
{
const here = () => page.textContent('#qs-vs-list li.here');
const record = () => page.evaluate(() => window.quantumStudio.versions.record());
const BASE = 'OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
await setProgram(BASE);
await record();
check(/edited by you/.test(await here()), `what the reader types is recorded as theirs (${await here()})`);
const first = await page.textContent('#qs-vs-list li:last-child');
check(/^v0started from the example /.test(first), `the first version is named for where the program came from (${first})`);
const mine = await page.$eval('#qs-vs-list li.here', (l) => l.dataset.id);
const PROPOSED = 'OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[1];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n';
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'assistant', name: 'scripted' }, 'entangle the third qubit'); }, PROPOSED);
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-1', { timeout: LONG });
const offered = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent));
check(offered.length === 2 && /h q\[0\]/.test(offered[0]) && /\+ cx q\[1\], q\[2\]/.test(offered[1]) && await page.evaluate(() => window.quantumStudio.view.state.readOnly),
`a proposal is shown change by change, and the program waits (${offered.map((t) => t.replace(/\s+/g, ' ')).join(' | ')})`);
await page.uncheck('#qs-review-0');
check((await page.textContent('#qs-review-apply')) === 'Take 1 of 2', 'the reader takes the changes they want');
await page.click('#qs-review-apply');
const TAKEN = BASE.replace('cx q[0], q[1];\n', 'cx q[0], q[1];\ncx q[1], q[2];\n');
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q && !window.quantumStudio.view.state.readOnly, TAKEN, { timeout: LONG });
check(/proposed by the assistant \(scripted\)/.test(await here()) && /1 of 2 taken/.test(await here()), `only the change taken reaches the program, recorded as the assistant's (${await here()})`);
check(/1 of 2 changes taken; the rest were not taken/.test(await page.textContent('#qs-vs-detail')), 'the version keeps what was rejected');
await page.check('#qs-vs-hands');
await page.waitForFunction(() => document.querySelectorAll('.qs-hand-gutter .qs-hand:not(.qs-hand-spacer)').length === 7, null, { timeout: LONG });
const gutter = await page.$$eval('.qs-hand-gutter .qs-hand:not(.qs-hand-spacer)', (ms) => ms.map((m) => m.className.replace('qs-hand qs-hand-', '')));
await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-by-assistant').length === 1, null, { timeout: LONG });
check(gutter[5] === 'assistant' && gutter.filter((h) => h === 'assistant').length === 1 && gutter.slice(1, 3).every((h) => h === 'person'),
`“Who wrote it” marks each line in the gutter and each gate on the diagram (${gutter.join(', ')})`);
// The reader edits the model's line: in both hands.
await setProgram(TAKEN.replace('cx q[1], q[2];', 'cx q[0], q[2];'));
await record();
await page.waitForFunction(() => document.querySelectorAll('.qs-hand-gutter .qs-hand-assistant_then_person').length === 1, null, { timeout: LONG });
check(/the assistant, then you \(scripted\): 1 gate, 1 line/.test(await page.textContent('#qs-vs-share')), `a model's line the reader edits is in both hands (${await page.textContent('#qs-vs-share')})`);
await page.click('#qs-run'); await done(page);
await page.waitForFunction(() => /recomputed/.test(document.querySelector('#qs-account')?.textContent ?? ''), null, { timeout: LONG });
const wrote = await page.textContent('#qs-written-by');
check(/1 by the assistant, then you \(scripted\)/.test(wrote) && /of 3 gates/.test(wrote) && (await page.textContent('#qs-verified')).startsWith('✓ verifies'),
`the run's receipt binds whose hand each gate is in, recomputed here (${wrote}; ${await page.textContent('#qs-account')})`);
// A line of work from an older version, then brought back in.
await page.click(`#qs-vs-list li[data-id=${mine}] .qs-vs-item`);
await page.click('#qs-vs-open');
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, BASE, { timeout: LONG });
await setProgram(BASE.replace('h q[0];\n', 'h q[0];\nx q[2];\n'));
await record();
const heads = await page.$$eval('#qs-vs-list li', (ls) => ls.filter((l) => /a head|in the editor/.test(l.textContent)).length);
check(heads === 2 && (await page.$$('.qs-vs-svg .qs-vs-edge')).length >= 4, `an edit from an older version is a line of work of its own (${heads} heads)`);
const other = await page.$eval('#qs-vs-list li:not(.here)', (l) => l.dataset.id);
const otherHead = await page.$$eval('#qs-vs-list li', (ls) => ls.find((l) => /a head/.test(l.textContent))?.dataset.id);
await page.click(`#qs-vs-list li[data-id=${otherHead}] .qs-vs-item`);
await page.click('#qs-vs-merge');
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
const merging = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent));
// Keep this line's x q[2]; take the other's edited CX.
for (const [i, t] of merging.entries()) if (/− x q\[2\]/.test(t)) await page.uncheck(`#qs-review-${i}`);
await page.click('#qs-review-apply');
await page.waitForFunction(() => /merged by you/.test(document.querySelector('#qs-vs-list li.here')?.textContent ?? ''), null, { timeout: LONG });
const merged = await doc();
check(/x q\[2\];/.test(merged) && /cx q\[0\], q\[2\];/.test(merged) && /, from v\d+ and v\d+/.test(await page.textContent('#qs-vs-detail')),
`bringing a line of work back in is a merge of both (${other}, ${otherHead})`);
check(/the assistant, then you \(scripted\): 1 gate/.test(await page.textContent('#qs-vs-share')), 'the merge keeps each line’s author');
const count = (await page.$$('#qs-vs-list li')).length;
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForSelector('#qs-vs-list li.here', { timeout: LONG });
const back = (await page.$$('#qs-vs-list li')).length;
check(back === count && /merged by you/.test(await here()) && (await doc()) === merged, `the versions come back with the page (${back} of ${count}; ${await here()})`);
const files = [];
page.on('download', (d) => files.push(d));
await page.click('#qs-vs-download');
await page.waitForFunction(() => /program\.history\.json/.test(document.querySelector('.qs-status')?.textContent ?? ''), null, { timeout: LONG });
await page.waitForTimeout(500);
const hist = files.find((d) => d.suggestedFilename() === 'program.history.json');
const saved = hist && JSON.parse(readFileSync(await hist.path(), 'utf8'));
const prog = files.find((d) => d.suggestedFilename() === 'program.qasm');
check(saved?.versions?.length === count && saved.versions.every((v) => v.seal?.key?.length === 64 && v.digest.length === 64) && prog && readFileSync(await prog.path(), 'utf8') === merged,
`Download saves the program and its history, every version sealed, as wai-quantum history verify reads them (${files.map((d) => d.suggestedFilename()).join(', ')})`);
await page.uncheck('#qs-vs-hands');
}
// Rules for proposals: held lines, a gate set and a region bind every proposal,
// change by change; a change that breaks one is shown with it and cannot be taken.
{
const record = () => page.evaluate(() => window.quantumStudio.versions.record());
const BASE = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
await setProgram(BASE);
await record();
await page.evaluate(() => { const v = window.quantumStudio.view, l = v.state.doc.line(4); v.dispatch({ selection: { anchor: l.from, head: l.to } }); });
await page.click('#qs-rules > summary');
await page.click('#qs-rule-hold');
const HELD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\n// @lock\nh q[0];\n// @unlock\ncx q[0], q[1];\nc = measure q;\n';
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, HELD, { timeout: LONG });
check((await page.$$('.cm-line.qs-held-line')).length === 3, 'holding the selected lines marks them in the program and shades them');
await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-held').length === 1, null, { timeout: LONG });
check(true, 'the held gate is marked on the diagram');
await page.fill('#qs-rule-gates', 'h, cx, x');
await page.dispatchEvent('#qs-rule-gates', 'change');
await record();
// Three changes: a cz the gate set does not allow, the held line, and an x it does.
const PROPOSED = HELD.replace('bit[2] c;\n', 'bit[2] c;\ncz q[0], q[1];\n').replace('h q[0];', 'h q[1];').replace('cx q[0], q[1];\n', 'cx q[0], q[1];\nx q[1];\n');
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'assistant', name: 'scripted' }, 'three changes'); }, PROPOSED);
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-2', { timeout: LONG });
const rev = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent.replace(/\s+/g, ' ')));
check(await page.isDisabled('#qs-review-0') && await page.isDisabled('#qs-review-1') && !(await page.isDisabled('#qs-review-2')) && /only cx, h, x may be used/.test(rev[0]) && /are held/.test(rev[1]),
`a change that breaks a rule is shown with it and cannot be taken (${rev.map((t) => t.slice(0, 80)).join(' | ')})`);
check((await page.textContent('#qs-review-apply')) === 'Take 1 of 1', 'only the change the rules allow can be taken');
await page.click('#qs-review-apply');
const KEPT = HELD.replace('cx q[0], q[1];\n', 'cx q[0], q[1];\nx q[1];\n');
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, KEPT, { timeout: LONG });
check(/1 of 3 taken, 2 blocked/.test(await page.textContent('#qs-vs-list li.here')), `the version records what the rules blocked (${await page.textContent('#qs-vs-list li.here')})`);
// A region: the next proposal keeps to the selected line.
await page.evaluate(() => { const v = window.quantumStudio.view, l = v.state.doc.line(8); v.dispatch({ selection: { anchor: l.from, head: l.to } }); });
await page.click('#qs-rule-region');
check(/Only lines 8 to 8/.test(await page.textContent('#qs-rule-region-chip')), 'a region is kept to the selected lines');
const OUTSIDE = KEPT.replace('qubit[2] q;\nbit[2] c;', 'qubit[3] q;\nbit[3] c;').replace('x q[1];', 'x q[0];');
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'agent', name: 'test agent' }, 'two changes'); }, OUTSIDE);
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-1', { timeout: LONG });
const out = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent.replace(/\s+/g, ' ')));
check(await page.isDisabled('#qs-review-0') && /outside lines 8 to 8/.test(out[0]) && !(await page.isDisabled('#qs-review-1')), `a change outside the region is blocked (${out[0].slice(0, 90)})`);
await page.click('#qs-review-apply');
await page.waitForFunction(() => document.querySelector('#qs-rule-region-chip')?.hidden, null, { timeout: LONG });
check((await doc()) === KEPT.replace('x q[1];', 'x q[0];'), 'the region holds for that proposal and is then let go');
await page.fill('#qs-rule-gates', '');
await page.dispatchEvent('#qs-rule-gates', 'change');
}
// The classical-shortcut lens.
{
let RING = 'OPENQASM 3.0;\nqubit[16] q;\nbit[16] c;\nh q;\n';
for (let i = 0; i < 16; i++) RING += `cz q[${i}], q[${(i + 1) % 16}];\n`;
RING += 'rz(0.3) q;\nc = measure q;\n';
await setProgram(RING);
await page.click('#qs-shortcuts');
await page.waitForSelector('#qs-shortcuts-summary', { timeout: LONG });
const summary = await page.textContent('#qs-shortcuts-summary');
const cut = await page.textContent('#qs-routes tr[data-route=cutrank]');
check(/cut-rank sum/.test(summary) && /widest cut 2 bits/.test(cut) && await page.$('#qs-routes tr.best[data-route=cutrank]') && /does not apply/.test(await page.textContent('#qs-routes tr[data-route=stabilizer]')),
`the lens prices a Clifford ring with a closing layer by the cut-rank sum (${summary})`);
check(/not searched/.test(await page.textContent('#qs-routes tr[data-route=tensor_network]')), 'the quick check leaves the contraction search for a thorough one');
await page.click('#qs-shortcuts-thorough');
await page.waitForFunction(() => /2\^/.test(document.querySelector('#qs-routes tr[data-route=tensor_network]')?.textContent ?? ''), null, { timeout: LONG });
check(true, `a thorough check searches a contraction tree (${(await page.textContent('#qs-routes tr[data-route=tensor_network]')).trim()})`);
await page.click('.qs-editor .cm-content');
await page.keyboard.press('ControlOrMeta+End');
await page.keyboard.type('\nh q[0];');
await page.waitForSelector('#qs-shortcuts-panel.stale .qs-stale-note', { timeout: LONG });
check(true, 'a check is marked stale once the program changes');
await page.selectOption('#qs-example', 'bell');
await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG });
await page.click('#qs-shortcuts');
await page.waitForFunction(() => /^Classically easy at any width/.test(document.querySelector('#qs-shortcuts-summary')?.textContent ?? '') && !document.querySelector('#qs-shortcuts-panel.stale'), null, { timeout: LONG });
check(true, 'a Clifford circuit is classically easy at any width');
}
// Predict first.
{
await page.selectOption('#qs-example', 'bell');
await page.check('#qs-predict-first');
const ran = (await page.$$('.qs-hitem')).length;
await page.click('#qs-run');
await page.waitForSelector('#qs-predict', { timeout: LONG });
check((await page.$$('.qs-hitem')).length === ran && !(await page.$('.qs-method')), 'with Predict first, the run waits for a prediction');
await page.fill('#qs-predict-bits', '000');
await page.fill('#qs-predict-share', '50');
await page.click('#qs-predict-run');
check(/2 classical bits; the outcome has 3/.test(await page.textContent('#qs-predict-note')), 'an outcome of the wrong width is refused, with the width');
await page.fill('#qs-predict-bits', '00');
await page.click('#qs-predict-run');
await done(page);
await page.waitForSelector('#qs-predicted', { timeout: LONG });
const pred = await page.textContent('#qs-predicted');
check(/you said00 in 50%/.test(pred) && /the run00 in \d/.test(pred) && /(came out on top|Another outcome)/.test(pred) && /first prediction/.test(pred), `the prediction is set against the counts (${pred})`);
await page.uncheck('#qs-predict-first');
}
// Generators: a program with integer inputs, read at the inputs bar's values. The
// Workbench checks a family at every size and keeps the cheapest of each cell.
const CHAIN = 'OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [0:n-2] {\n cx q[i], q[i + 1];\n}\n';
const TREE = 'OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint k in [0:n] {\n for uint i in [0:2**k - 1] {\n if (2**k < n && i + 2**k < n) {\n cx q[i], q[i + 2**k];\n }\n }\n}\n';
const lintSays = (re) => page.waitForFunction((re) => new RegExp(re).test(document.querySelector('#qs-lint')?.textContent ?? ''), re.source, { timeout: LONG });
const wbLog = () => page.$$eval('#qs-wb-log li', (ls) => ls.map((l) => l.textContent));
const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-start').disabled, null, { timeout: LONG });
{
await setProgram(CHAIN.replace('qubit[n] q;\n', 'qubit[n] q;\nbit[n] c;\n') + 'c = measure q;\n');
await page.waitForSelector('#qs-inputs:not([hidden]) #qs-input-n', { timeout: LONG });
await lintSays(/4 qubits · 4 gates at n = 4/);
check(true, 'a generator shows its input under the editor, and the lint reads it at n = 4');
await page.fill('#qs-input-n', '3');
await page.dispatchEvent('#qs-input-n', 'change');
await lintSays(/3 qubits · 3 gates at n = 3/);
await page.waitForFunction(() => /^The circuit: 3 qubits/.test(document.querySelector('#qs-diagram svg')?.getAttribute('aria-label') ?? ''), null, { timeout: LONG });
check(true, 'a new value re-reads it: linted and drawn at n = 3');
await page.click('#qs-run');
await done(page);
const got = await counts(page);
check(got.length === 2 && got.every(([k]) => k === '000' || k === '111'), `it runs at its value: a GHZ state on 3 qubits (${JSON.stringify(got)})`);
await page.fill('#qs-input-n', '4');
await page.dispatchEvent('#qs-input-n', 'change');
await setProgram(CHAIN);
await page.click('#qs-wb-start');
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
const st = await page.textContent('#qs-wb-stats');
check(/Best e0: cost 17\.3 against the reference’s 17\.3, by reference/.test(st) && (await page.$$('#qs-wb-map .qs-ring')).length === 1,
`Start checks the reference at n = 3, 5 and 8 and seeds the archive with it (${st})`);
await page.click('#qs-wb-search');
await page.waitForFunction(() => /The blind search: 100 edits judged/.test(document.querySelector('#qs-wb-log')?.textContent ?? ''), null, { timeout: LONG });
await wbIdle();
const after = await page.textContent('#qs-wb-stats');
check(/^101 evaluated/.test(after), `the blind search judges a hundred edits, each by the engine (${after})`);
await setProgram(TREE);
await page.click('#qs-wb-submit');
// What is new in it is said after the outcome, above it.
await page.waitForFunction(() => [...document.querySelectorAll('#qs-wb-log li')].some((l) => /^Your program: /.test(l.textContent)), null, { timeout: LONG });
await wbIdle();
const said = (await wbLog()).find((t) => /^Your program: /.test(t));
check(/^Your program: kept as e\d+, cheaper than every generator before it \(cost 12\.3\)/.test(said) && /by person/.test(await page.textContent('#qs-wb-stats')),
`the reader's log-depth tree is kept as the cheapest, and named as theirs (${said})`);
const named = (await wbLog())[0];
check(/^e\d+: a known construction: GHZ, by doubling\.$/.test(named), `the tree is named for the known construction it is (${named})`);
await page.click('#qs-wb-map .qs-wb-best');
await page.waitForSelector('#qs-wb-open', { timeout: LONG });
const card = await page.textContent('#qs-wb-entry');
check(/cost 12\.3/.test(card) && /checked by \w+, sealed/.test(card) && /person/.test(card) && !/null/.test(card), `an entry shows its cost, how it was checked, its seal and who wrote it (${card.slice(0, 140)})`);
await page.waitForFunction(() => /a known construction/.test(document.querySelector('#qs-wb-novelty')?.textContent ?? ''), null, { timeout: LONG });
const novel = await page.textContent('#qs-wb-novelty');
check(/GHZ, by doubling/.test(novel) && /Better than the reference on what the cost does not weigh: two-qubit depth \d+ against \d+/.test(novel), `the entry says what it is and what it beats the reference on unasked (${novel})`);
// Your taste: two kept generators, which the engine proves make the same state, compared.
if (!(await page.$eval('#qs-taste', (d) => d.open))) await page.click('#qs-taste > summary');
await page.click('#qs-taste-compare');
await page.waitForSelector('.qs-taste-pick', { timeout: LONG });
const pair = await page.$$eval('.qs-taste-side', (ss) => ss.map((s) => s.dataset.name));
check(pair.length === 2 && pair[0] !== pair[1] && /proves they do the same thing \(state\)/.test(await page.textContent('#qs-taste-pair')), `Compare two offers the pair the profile is least sure of, proved equal (${pair.join(', ')})`);
await page.selectOption('#qs-taste-part', 'economy');
await page.click('.qs-taste-side >> nth=0 >> .qs-taste-pick');
await page.waitForSelector('#qs-taste-chosen', { timeout: LONG });
check(new RegExp(`^Kept: ${pair[0]} over ${pair[1]}, for its economy`).test(await page.textContent('#qs-taste-chosen')), `the choice is kept, with what it was about (${await page.textContent('#qs-taste-chosen')})`);
await page.waitForFunction(() => document.querySelector('#qs-taste-count')?.textContent === '2', null, { timeout: LONG });
check(/^2 choices \(economy 1\)/.test(await page.textContent('#qs-taste-summary')), `the profile counts its choices and their parts (${await page.textContent('#qs-taste-summary')})`);
await page.click('#qs-taste-rank');
await page.waitForSelector('#qs-taste-list li', { timeout: LONG });
const ranked = await page.$$eval('#qs-taste-list li', (ls) => ls.map((l) => l.textContent));
const kept = await page.evaluate(() => Object.keys(JSON.parse(localStorage.getItem('wai.quantum-studio.workbench')).archive.cells).length);
check(ranked.length === kept && ranked.every((t) => /typicality \d\.\d\d/.test(t) && /score -?\d/.test(t) && /cost \d/.test(t)), `Rank by my taste orders every kept generator, each with its typicality, score and cost (${ranked.length} of ${kept}: ${ranked[0]})`);
check(kept <= 3 || /The first 3 are \d\.\d\d apart in how they are built; by cost, \d\.\d\d/.test(await page.textContent('#qs-taste-ranked')), 'the ranking says whether it narrows what comes first, against the cost');
await page.evaluate(() => { window.qsListBefore = document.querySelector('#qs-taste-list'); });
await page.$eval('#qs-taste-dial', (d) => { d.value = '1'; d.dispatchEvent(new Event('change')); });
await page.waitForFunction(() => document.querySelector('#qs-taste-list') && document.querySelector('#qs-taste-list') !== window.qsListBefore, null, { timeout: LONG });
check((await page.$$('#qs-taste-list li')).length === kept, 'turning the dial ranks them again');
await page.$eval('#qs-taste-dial', (d) => { d.value = '0'; d.dispatchEvent(new Event('change')); });
await page.fill('#qs-taste-profile', 'teaching');
await page.dispatchEvent('#qs-taste-profile', 'change');
await page.waitForFunction(() => /^No choices yet/.test(document.querySelector('#qs-taste-summary')?.textContent ?? ''), null, { timeout: LONG });
check((await page.textContent('#qs-taste-count')) === '', 'a second profile is kept apart: it has no choices');
await page.fill('#qs-taste-profile', 'mine');
await page.dispatchEvent('#qs-taste-profile', 'change');
await page.waitForFunction(() => document.querySelector('#qs-taste-count')?.textContent === '2', null, { timeout: LONG });
check(true, 'and the first comes back with its choices');
await setProgram(CHAIN);
await page.click('#qs-wb-open');
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
await page.click('#qs-review-apply');
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, TREE, { timeout: LONG });
check(/proposed by you/.test(await page.textContent('#qs-vs-list li.here')), 'Open in editor offers the kept generator, recorded as its proposer’s, the reader’s');
await page.click('#qs-wb-ask');
check(/needs one/.test((await wbLog())[0]), 'Ask the model, with none connected, says it needs one');
const files = [];
page.on('download', (d) => files.push(d));
await page.click('#qs-wb-save');
await page.waitForFunction(() => /^Saved workbench-/.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
await page.waitForTimeout(500);
const read = async (d) => JSON.parse(readFileSync(await d.path(), 'utf8'));
const [specFile, archiveFile] = [files.find((d) => !/archive/.test(d.suggestedFilename())), files.find((d) => /\.archive\.json$/.test(d.suggestedFilename()))];
const savedSpec = specFile && await read(specFile), savedArchive = archiveFile && await read(archiveFile);
check(savedSpec?.reference === CHAIN && savedArchive?.spec?.length === 64 && savedArchive.entries.length >= 2 && savedArchive.entries.every((e) => e.sealed?.receipt)
&& archiveFile.suggestedFilename() === specFile.suggestedFilename().replace(/\.json$/, '.archive.json'),
`Download saves the specification and its archive side by side, every entry sealed, as the command line reads them (${files.map((d) => d.suggestedFilename()).join(', ')})`);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => /Taken up again: 102 evaluated/.test(document.querySelector('#qs-wb-log')?.textContent ?? ''), null, { timeout: LONG });
check(/by person/.test(await page.textContent('#qs-wb-stats')) && (await page.inputValue('#qs-wb-sizes')) === 'n = 3, 5, 8', 'the archive and its specification come back with the page');
await lintSays(/qubits/);
check((await page.textContent('#qs-taste-count')) === '2' && (await page.inputValue('#qs-taste-profile')) === 'mine', 'the taste profiles come back with the page');
check(/^\d+ flagged · 1 saved$/.test(await page.textContent('#qs-found-count')) && (await page.inputValue('#qs-found-list .qs-found-item.saved input')) === 'the ladder, read backwards', 'what Found keeps comes back with the page, with its note');
// On a line, the tree breaks only the rule that gates join neighbours: an aberration.
if (!(await page.$eval('#qs-wb-spec', (d) => d.open))) await page.click('#qs-wb-spec > summary');
await page.selectOption('#qs-wb-conn', 'line');
await setProgram(CHAIN);
await page.click('#qs-wb-start');
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
await setProgram(TREE);
await page.click('#qs-wb-submit');
await page.waitForFunction(() => /an aberration/.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
await wbIdle();
const ab = (await wbLog())[0];
check(/^Your program: an aberration\. It breaks only one rule \(at n = 3: `cx` acts on qubits \[0, 2\], which are not neighbours on the line\); without it, it would be verified \(\w+\) at cost 12\.3/.test(ab) && /Try without that rule/.test(ab),
`a generator that breaks only the line's rule is an aberration, with the cost it would have (${ab})`);
check(/not verified/.test((await wbLog())[1]) && /\b1 kept\b/.test(await page.textContent('#qs-wb-stats')), 'an aberration is never kept');
check(await page.$('#qs-found-list .qs-found-item[data-kind=aberration]'), 'Found flags the aberration');
await page.click('#qs-wb-log .qs-wb-try');
check((await page.inputValue('#qs-wb-conn')) === 'all' && /lets a gate join any pair/.test(await page.textContent('#qs-wb-stats')), 'Try without that rule loosens the specification, to start again under it');
}
// The assistant, against a scripted model server: it calls a tool, proposes a program
// the engine cannot read, gets the engine's reason back, and proposes a corrected one,
// which the Studio checks and runs before offering it.
{
const asked = [];
const GOOD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
const model = createServer((req, res) => {
const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'GET, POST, OPTIONS' };
if (req.method === 'OPTIONS') { res.writeHead(204, cors).end(); return; }
if (req.url.endsWith('/models')) { res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ data: [{ id: 'scripted' }] })); return; }
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
const r = JSON.parse(body);
asked.push(r);
const msgs = r.messages;
// Alternatives' calls carry no tools: candidates, one of them wrong on purpose.
if (!r.tools) {
const want = msgs[1].content;
const content = /^Write \d+ generators/.test(want)
? `Idea: double the qubits that hold the state each layer.\n\`\`\`qasm\n${TREE}\`\`\`\nIdea: forget the Hadamard.\n\`\`\`qasm\n${CHAIN.replace('h q[0];\n', '')}\`\`\`\nIdea: use CZ.\n\`\`\`qasm\n${CHAIN.replace('cx q[i], q[i + 1];', 'h q[i + 1];\n cz q[i], q[i + 1];\n h q[i + 1];')}\`\`\``
: /^Write \d+ programs that are the same circuit/.test(want)
? 'Idea: the two Hadamards cancel, and the X moves past the CNOT.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nx q[1];\nc = measure q;\n```\nIdea: drop the X.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nc = measure q;\n```'
: 'Idea: a GHZ state on three qubits.\n```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n```\n**Idea:** entangle with CZ instead.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[1];\nc = measure q;\n```';
res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message: { role: 'assistant', content }, finish_reason: 'stop' }] }));
return;
}
const told = msgs.some((m) => m.role === 'user' && /could not read the program you proposed/.test(m.content));
const toolDone = msgs.some((m) => m.role === 'tool');
let message;
if (!toolDone) message = { role: 'assistant', content: '', tool_calls: [{ id: 'c1', type: 'function', function: { name: 'lint_qasm', arguments: JSON.stringify({ qasm: GOOD }) } }] };
else if (!told) message = { role: 'assistant', content: 'It entangles two qubits. A variant:\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nfoo q[0];\n```' };
else message = { role: 'assistant', content: 'Corrected:\n```qasm\n' + GOOD + '```' };
res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message, finish_reason: message.tool_calls ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } }));
});
});
await new Promise((r) => model.listen(0, '127.0.0.1', r));
await page.click('#qs-ai-gear');
await page.fill('#qs-ai-endpoint', `http://127.0.0.1:${model.address().port}/v1`);
await page.click('#qs-ai-connect');
await page.waitForFunction(() => /connected · scripted/.test(document.querySelector('#qs-ai-state')?.textContent ?? ''), null, { timeout: LONG });
check(true, 'the assistant connects to a model server and reads its models');
await page.click('.qs-ai-quick button >> text=Explain this circuit');
await page.waitForSelector('.qs-ai-apply', { timeout: LONG });
const log = await page.$$eval('#qs-ai-log > li', (ls) => ls.map((l) => l.textContent));
check(log.some((t) => t.includes('used lint_qasm')), 'the model\'s tool call ran on the engine, and the log shows it');
check(log.some((t) => /could not read the proposed program \(line 3: unknown gate foo\)/.test(t)), `a proposal the engine cannot read goes back with the engine's reason (${log.find((t) => /could not read/.test(t)) ?? 'none'})`);
const proposal = await page.textContent('.qs-ai-proposal');
check(/checked: 2 qubits, 2 gates/.test(proposal) && /ran here on Stabilizer tableau/.test(proposal), `the corrected proposal is checked and run before it is offered (${proposal})`);
check(asked.length === 3 && asked.every((r) => r.tools?.some((t) => t.function.name === 'run_job') && r.messages[0].role === 'system' && /Never state a probability/.test(r.messages[0].content)), 'every call to the model carries the engine\'s tools and the rule that numbers come from them');
const toolMsg = asked[1].messages.find((m) => m.role === 'tool');
check(toolMsg && JSON.parse(toolMsg.content).qubits === 2, 'the tool\'s answer went back to the model');
await page.click('.qs-ai-apply');
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
check(/A proposal from the assistant \(scripted\)/.test(await page.textContent('#qs-review')), 'Apply offers the checked program as the assistant’s proposal, named');
await page.click('#qs-review-apply');
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, GOOD, { timeout: LONG });
check(true, 'taken, it is the program in the editor');
check((await page.inputValue('#qs-ai-stance')) === 'questions' && (await page.inputValue('#qs-ai-initiative')) === 'asked' && /ask one short question/.test(asked[0].messages[0].content),
'by default the assistant asks questions first and speaks only when asked');
const quick = await page.$$eval('.qs-ai-quick button', (bs) => bs.map((b) => b.textContent));
check(quick.includes('Ask me a question') && quick.includes('Hint'), `it can be asked for a question or a hint (${quick.join(', ')})`);
check(asked.every((r) => !r.tools || r.tools.some((t) => t.function.name === 'critique_qasm') && r.tools.some((t) => t.function.name === 'check_equivalent')), 'its tools include the critique and the equivalence check');
// Runs refused twice: no hint offered while it speaks only when asked; offered, not sent, when allowed.
const calls = asked.length;
await setProgram('OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n');
for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); }
check(await page.isHidden('#qs-ai-stuck'), 'while it speaks only when asked, refused runs offer no hint');
await page.selectOption('#qs-ai-initiative', 'hint');
for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); }
check(await page.isVisible('#qs-ai-stuck') && asked.length === calls, 'allowed to, it offers a hint after refused runs, and asks the model nothing');
await page.selectOption('#qs-ai-initiative', 'asked');
// Improve and Explore with the model: the engine reads, measures and compares each.
// A rule the model is told: text to avoid.
if (!(await page.$eval('#qs-rules', (d) => d.open))) await page.click('#qs-rules > summary');
await page.fill('#qs-rule-text', 'measure q[5]');
await page.dispatchEvent('#qs-rule-text', 'change');
const DRAFT = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[0];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n';
await setProgram(DRAFT);
await page.click('#qs-improve');
await page.waitForFunction(() => document.querySelectorAll('.qs-cand').length && !/looking/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG });
const cards = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent));
const refused = await page.textContent('#qs-alt-refused').catch(() => '');
check(cards.some((t) => /the model/.test(t) && /the two Hadamards cancel/.test(t) && /same circuit · equal tableaus/.test(t)) && /drop the X: not the same circuit/.test(refused),
`Improve with the model: its rewrite checked the same circuit, its wrong one left out with the reason (${refused})`);
await page.fill('#qs-explore-aim', 'another entangler');
await page.click('#qs-explore');
await page.waitForFunction(() => /a GHZ state/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG });
const ideas = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent));
const muse = asked.filter((r) => !r.tools).map((r) => r.messages[1].content);
check(ideas.length === 2 && ideas.some((t) => /3 qubits/.test(t) && /a different circuit/.test(t)) && ideas.some((t) => /entangle with CZ instead/.test(t)) && muse.some((m) => /toward this aim: another entangler/.test(m)),
`Explore: the model's ideas, each read and measured by the engine (${ideas.map((t) => t.slice(0, 60)).join(' | ')})`);
check(ideas.some((t) => /a GHZ state on three qubits/.test(t) && /a known construction: GHZ, by a chain of CNOTs/.test(t)) && ideas.some((t) => /entangle with CZ instead/.test(t) && /a different route to GHZ, by a chain of CNOTs/.test(t)),
'each idea says what is new in it: the chain is a known construction, the CZ one a different route to it');
check(muse.length >= 2 && muse.every((m) => m.includes('Add no line holding `measure q[5]`.')), 'the model is told the reader’s rules with every request for ideas');
await page.fill('#qs-rule-text', '');
await page.dispatchEvent('#qs-rule-text', 'change');
check((await doc()) === DRAFT, 'neither changes the program until one is applied');
// The Workbench asks the model for generators, and the engine judges each at every size.
await setProgram(CHAIN);
await page.click('#qs-wb-start');
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
await page.click('#qs-wb-ask');
await page.waitForFunction(() => /^use CZ: /.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
await wbIdle();
const wlog = await wbLog();
check(wlog.some((t) => /^double the qubits that hold the state each layer: kept as e\d+, cheaper than every generator before it/.test(t))
&& wlog.some((t) => /^forget the Hadamard: not verified: at n = 3: different/.test(t))
&& wlog.some((t) => /^use CZ: not verified: at n = 3: it uses `cz`, which the specification does not allow/.test(t)),
`the model's generators, each judged at every size: one kept, a wrong one and a forbidden gate left out (${wlog.slice(0, 3).join(' | ')})`);
check(wlog.some((t) => /^use CZ: an aberration\. It breaks only one rule \(at n = 3: it uses `cz`/.test(t)) && !wlog.some((t) => /^forget the Hadamard: an aberration/.test(t)),
'the forbidden gate that would be verified without the rule is an aberration; the wrong one is not');
const brief = asked.filter((r) => !r.tools).map((r) => r.messages[1].content).find((m) => /^Write 3 generators/.test(m)) ?? '';
check(/prepare the same state from \|0…0⟩/.test(brief) && /n = 3, 5, 8/.test(brief) && /only these gates: h, cx/.test(brief) && brief.includes('cx q[i], q[i + 1];') && /The cheapest found: cost 17\.3, the reference\./.test(brief),
'the model is told the specification and shown the cheapest found, with the cost the engine measured');
check(/by model: scripted/.test(await page.textContent('#qs-wb-stats')), 'the best is the model’s, and named as its');
await setProgram(DRAFT);
// The candidates come back within the editor's redraw delay: wait for the draft's diagram.
await page.waitForFunction(() => /4 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && document.querySelectorAll('#qs-diagram .qc-flag').length === 2, null, { timeout: LONG });
check(!/null/.test(await page.textContent('#qs-alt-body')) && !/null/.test(await page.textContent('#qs-measures')), 'no empty part is written out as “null”');
await page.screenshot({ path: join(SHOTS, 'studio-alternatives.png'), fullPage: true });
model.close();
}
check(seen.errors.length === 0, `no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
check(seen.failed.length === 0, `no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`);
await ctx.close();
{
const { ctx, page } = await open({ width: 390, height: 844 });
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
const over = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
check(over <= 0, `at 390 px nothing scrolls sideways (overflow ${over} px)`);
await ctx.close();
}
await browser.close();
server.close();
console.log(`\n${fails === 0 ? 'every check passed' : `${fails} check(s) failed`}`);
process.exit(fails === 0 ? 0 : 1);