wai-quantum: the person's rules for proposals, checked change by change #494

Merged
dcharlot merged 2 commits from wai/quantum-locks-r2 into main 2026-10-26 13:40:52 -04:00
23 changed files with 948 additions and 56 deletions

View file

@ -167,15 +167,15 @@ jobs:
# Its native half: the service's form and receipt, any angle on the state
# vector, what does not run saying why, plan, lint, draw and methods as JSON,
# critique, equivalence and variants as JSON, the classical shortcuts, the
# workbench run and sealed in the page, and a program's history recorded and
# blamed there.
# workbench run and sealed in the page, a program's history recorded and
# blamed there, and the reader's rules checking a proposal.
- name: The job engine in a page (native unit tests)
shell: bash
working-directory: wai/wai-web
run: |
set -euo pipefail
cargo test --locked --lib --features quantum_engine quantum_engine:: 2>&1 | tee "$RUNNER_TEMP/wai-web-engine.log"
grep -q "test result: ok. 8 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log"
grep -q "test result: ok. 9 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log"
# The crate's native unit tests, run on the host: the pure-Rust halves of the
# bindings, which wasm_tests does not run. They decode the committed integer
# payloads to their sidecars' SHA-256 (int_payload::tests) and check that the

View file

@ -1562,6 +1562,34 @@ wire format may change and such changes are recorded here.
- **`tools/ops-gpu-bench`** times each capability's decode with its graphs on the CPU and on each adapter, and reads the energy counters the host has.
- **CI:** `wai-ops-gpu.yml` runs the Metal and browser legs on the `macos` runner, and requires on a runner with no GPU that the device tests skip without `WAI_OPS_GPU_REQUIRE` and fail with it.
### wai-quantum: the person's rules for proposals, checked change by change
Feature 7 of the co-design report: region-scoped edits with locks and keep/avoid marks.
- **The rules** (`engine::locks`):
- held blocks, marked in the program between `// @lock` and `// @unlock`, so they travel with it to a model, an agent and a file;
- the gates a change may add; the qubit declarations held;
- gates and text to avoid; and the region a rewrite must keep to.
- **The check** (`locks::check`) names each change a proposal makes (`versions::changes`) and the rules it breaks.
- Only what a change adds is judged, so a gate the program already used is never a proposal's breach.
- A generator's gates are read at its inputs, and a proposal that does not read cannot pass a gate rule.
- **The brief** (`locks::brief`) states the rules as a model is told them.
- **On a version:** `versions::Review` keeps the changes the rules blocked (`History::propose_with`), and a change both blocked and accepted is refused there and in `History::verify`. A review with nothing blocked has the digest it had before.
- **The command line:**
- `wai-quantum locks check | brief`;
- `history changes` names each change's breaches;
- `history propose` refuses a blocked change, and `--accept all` takes the allowed ones;
- the rules sit beside the program as `prog.locks.json`.
- **For agents:** `history_commit` refuses an agent's edit that breaks a rule, with the rule, and `history_changes` shows the breaches and the rules.
- **The Studio's Rules:**
- "Hold the selected lines" marks them, and they are shaded in the editor, their gates marked on the diagram;
- the gates allowed, the qubit declarations, what to avoid;
- "Keep the next proposal to the selected lines".
- In the review, a change that breaks a rule is shown with it and cannot be taken, and the version records it as blocked.
- Alternatives leaves out a candidate whose every change breaks a rule, and the model is told the rules with every request.
- **For hosts:** `locks::call`, `engine_locks` in the page and `locks` in the desktop app; `locks::held_ops` gives the held gates.
- `versions::call` and `locks::check` name an unreadable program `unread`, not `error`: a page's worker reads an answer with an `error` as a failed call, so an unreadable draft had lost its lines' authors.
### wai-quantum 0.6.15 and wai-quantum-cli 0.1.3: generators, the specification-and-evaluator workbench, and a program's history, attributed
This release carries the four entries below it to the package registry, and `wai-quantum-cli`

View file

@ -58,6 +58,18 @@ The Versions pane records each change to the program and who made it:
An embedding page can offer a program as someone's proposal with `window.quantumStudio.propose(qasm, {kind, name}, note)`; it resolves once the reader has chosen.
## Rules for proposals
Under the editor, "Rules for proposals" holds what no proposal may change:
- **Held lines:** "Hold the selected lines" marks them in the program with `// @lock` and `// @unlock`. They are shaded in the editor, and their gates are marked on the diagram.
- **Gates:** the gates a proposal may add, and gates to avoid.
- **Qubits:** "Hold the qubit declarations".
- **Text to avoid.**
- **A region:** "Keep the next proposal to the selected lines", let go once that proposal is decided.
The engine checks every proposal against them change by change: the assistant's program, an alternative, a fix, a generator from the Workbench. A change that breaks a rule is shown in the review with the rule and cannot be taken. The version in Versions records it as blocked. An alternative whose every change breaks a rule is left out, with the reason. The model is told the rules with every request. What you type is yours to change.
## The assistant
The Assistant pane talks to a language model on this machine: any server with the chat-completions interface and tool calls, such as the JouleClaw runtime or a llama.cpp server. The address goes under "Model…" and is `http://127.0.0.1:8080/v1` by default. Nothing leaves the machine unless the address points elsewhere. In the desktop app the app makes the call itself, so no browser origin rule stands in the way.

View file

@ -122,12 +122,13 @@ export class DesktopModel {
export class Assistant {
// engine: the Studio's local engine; context(): {qasm, job, last} as the editor holds them;
// stance(): a key of STANCES.
constructor({ engine, model, context, stance = () => 'questions' }) {
// stance(): a key of STANCES; rules(): the reader's rules for proposals, as sentences.
constructor({ engine, model, context, stance = () => 'questions', rules = async () => [] }) {
this.engine = engine;
this.model = model;
this.context = context;
this.stance = stance;
this.rules = rules;
this.history = [];
}
@ -137,11 +138,16 @@ export class Assistant {
const res = await this.model.chat({ model: this.model.name || undefined, temperature, messages: [{ role: 'system', content: brief }, { role: 'user', content: prompt }] });
return ideas(unlatex((res.choices?.[0]?.message?.content || '').replace(/<think>[\s\S]*?<\/think>\s*/g, '')));
}
improve(qasm, k = 3) {
return this.muse(`Write ${k} programs that are the same circuit as this one (the same unitary up to a global phase, and the same measurements) but cheaper: fewer two-qubit gates, less depth or fewer T gates. Before each, one line that starts "Idea:" and says what it changes. Each program whole, in its own \`\`\`qasm block.\n\n\`\`\`qasm\n${qasm}\n\`\`\``, 0.4);
// The reader's rules, as a paragraph for a prompt, or nothing.
async ruled() {
const r = await Promise.resolve(this.rules()).catch(() => []);
return r?.length ? `\n\nThe reader's rules, which the Studio checks every change against (a change that breaks one is blocked):\n- ${r.join('\n- ')}` : '';
}
explore(qasm, aim, k = 4) {
return this.muse(`Imagine ${k} different programs in the spirit of this one${aim ? `, toward this aim: ${aim}` : ''}. Make them differ from each other in kind, not only in their angles: another structure, another entangler, another symmetry, another use of the qubits. Before each, one line that starts "Idea:" and says what it tries. Each program whole, in its own \`\`\`qasm block.\n\n\`\`\`qasm\n${qasm}\n\`\`\``, 0.9);
async improve(qasm, k = 3) {
return this.muse(`Write ${k} programs that are the same circuit as this one (the same unitary up to a global phase, and the same measurements) but cheaper: fewer two-qubit gates, less depth or fewer T gates. Before each, one line that starts "Idea:" and says what it changes. Each program whole, in its own \`\`\`qasm block.${await this.ruled()}\n\n\`\`\`qasm\n${qasm}\n\`\`\``, 0.4);
}
async explore(qasm, aim, k = 4) {
return this.muse(`Imagine ${k} different programs in the spirit of this one${aim ? `, toward this aim: ${aim}` : ''}. Make them differ from each other in kind, not only in their angles: another structure, another entangler, another symmetry, another use of the qubits. Before each, one line that starts "Idea:" and says what it tries. Each program whole, in its own \`\`\`qasm block.${await this.ruled()}\n\n\`\`\`qasm\n${qasm}\n\`\`\``, 0.9);
}
// Generators for the Workbench, from its specification and the cheapest it
// has found (`shown`, entries of the archive, cheapest first). The archive
@ -182,7 +188,7 @@ export class Assistant {
const messages = [
{ role: 'system', content: `${SYSTEM}\n${(STANCES[this.stance()] || STANCES.questions).text}` },
...this.history,
{ role: 'user', content: `${text}\n\n(The program in the editor:)\n\`\`\`qasm\n${ctx.qasm}\n\`\`\`` },
{ role: 'user', content: `${text}${await this.ruled()}\n\n(The program in the editor:)\n\`\`\`qasm\n${ctx.qasm}\n\`\`\`` },
];
let retried = false;
for (let round = 0; round < 8; round++) {

View file

@ -77,6 +77,8 @@ export class TabEngine {
// The program's history: `name` is the call (start, commit, propose, merge,
// changes, apply, blame, attest, log, check_job), its arguments in `args`.
history(name, args) { return this.call('history', { name, args }); }
// The person's rules for proposals: `name` is check, held or brief.
locks(name, args) { return this.call('locks', { name, args }); }
async signer() { return (await this.call('signer')).did; }
cancel() {
this.worker?.terminate();
@ -153,6 +155,7 @@ export class DesktopEngine {
discoverSubmit(spec, archive, source, parent, proposer, note) { return this.call('discover_submit', { spec: JSON.stringify(spec), archive: JSON.stringify(archive), source, parent: parent ?? null, proposer, note: note ?? null }); }
discoverSearch(spec, archive, seed, evaluations) { return this.call('discover_search', { spec: JSON.stringify(spec), archive: JSON.stringify(archive), seed, evaluations }); }
history(name, args) { return this.call('history', { name, args: JSON.stringify(args) }); }
locks(name, args) { return this.call('locks', { name, args: JSON.stringify(args) }); }
async signer() { return (await this.call('signer', {})).did; }
saveJob(name, job) { return this.call('save_job', { name, job: JSON.stringify(job, null, 2) }); }
saveProgram(name, text) { return this.call('save_program', { name, text }); }

View file

@ -72,7 +72,9 @@ export function atlas(draft, cands, onPick) {
// The pane. `draft()` is the editor's program; `model()` the assistant or null
// when no model is connected; `apply(qasm, note, by)` offers a program to the
// editor as `by`'s proposal: the engine's, or the model's.
export function galleryPane({ engine, draft, model, apply }) {
// `check(base, qasm)` gives the reader's rules' verdict on each change: a
// candidate whose every change breaks one is left out with the rule.
export function galleryPane({ engine, draft, model, apply, check = null }) {
const intent = el('input', { class: 'qs-input', id: 'qs-explore-aim', placeholder: 'Toward… fewer qubits, another entangler, more symmetry', autocomplete: 'off', 'aria-label': 'What Explore should aim for' });
const improveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-improve', type: 'button', text: 'Improve', title: 'The same circuit, cheaper: each checked equivalent to the draft' });
const exploreBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-explore', type: 'button', text: 'Explore', title: 'Different circuits toward the aim, from the model, each measured by the engine' });
@ -84,6 +86,16 @@ export function galleryPane({ engine, draft, model, apply }) {
let busy = false;
// A candidate {label, qasm, source, idea?, equivalence?} measured against the draft.
// Why a candidate is left out by the reader's rules: every change it makes
// breaks one. A candidate with any change the rules allow is offered, and
// the review blocks the rest.
async function breaks(src, qasm) {
if (!check) return null;
const v = await check(src, qasm).catch(() => null);
if (!v?.changes?.length || v.allowed.length) return null;
return v.changes.flatMap((c) => c.breaches.map((b) => b.why))[0] ?? null;
}
async function measure(src, c, base) {
const lint = await engine.lint(c.qasm);
if (!lint.ok) return { ...c, refused: `does not parse: ${lint.error}` };
@ -143,7 +155,9 @@ export function galleryPane({ engine, draft, model, apply }) {
const cands = [], refused = [];
for (const c of raw) {
const r = await measure(src, c, base);
const ruled = r.refused ? null : await breaks(src, r.qasm);
if (r.refused) refused.push(`${r.label}: ${r.refused}`);
else if (ruled) refused.push(`${r.label}: it breaks your rules: ${ruled}`);
else if (r.equivalence.verdict === 'different') refused.push(`${r.label}: not the same circuit (${r.equivalence.why})`);
else if (!cands.some((x) => x.qasm.trim() === r.qasm.trim()) && r.qasm.trim() !== src.trim()) cands.push(r);
}
@ -166,7 +180,9 @@ export function galleryPane({ engine, draft, model, apply }) {
const cands = [], refused = [];
for (const p of ideas) {
const r = await measure(src, { label: p.idea || 'an idea', idea: p.idea, qasm: p.qasm, source: 'model' }, base);
const ruled = r.refused ? null : await breaks(src, r.qasm);
if (r.refused) refused.push(`${r.label}: ${r.refused}`);
else if (ruled) refused.push(`${r.label}: it breaks your rules: ${ruled}`);
else if (!cands.some((x) => x.qasm.trim() === r.qasm.trim())) cands.push(r);
}
show('explore', src, base, cands, refused, '');

View file

@ -33,6 +33,7 @@ import { critiquePanel } from './critique-view.js';
import { galleryPane } from './gallery.js';
import { workbenchPane } from './workbench.js';
import { versionsPane, HANDS, whoName } from './versions.js';
import { rulesPane, heldLines, LOCK, UNLOCK } from './rules.js';
import { declared, inputsBar, withInputs } from './generators.js';
import { shortcutsPanel } from './shortcuts-view.js';
import * as Predict from './predict.js';
@ -109,6 +110,25 @@ function mount(root) {
el('div', { class: 'qs-where', role: 'group', 'aria-label': 'Run on' }, whereLocal, whereService),
predictLabel, runBtn, cancelBtn);
// ---- the reader's rules for proposals ----
const genInputs = () => (declared(text()).length ? inputs.values() : {});
const rules = rulesPane({
selection: () => {
const st = view.state, sel = st.selection.main;
const a = st.doc.lineAt(sel.from).number;
let z = st.doc.lineAt(sel.to).number;
if (z > a && sel.to === st.doc.line(z).from) z -= 1; // a selection that ends at a line's start stops before it
return [a, z];
},
hold: (a, z) => {
const doc = view.state.doc;
view.dispatch({ changes: [{ from: doc.line(a).from, insert: `${LOCK}\n` }, { from: doc.line(z).to, insert: `\n${UNLOCK}` }] });
setStatus(`Held lines ${a} to ${z}: no proposal may change them. Delete the markers to let them go.`, 'ok');
},
changed: () => paintHeld(),
});
const brief = async () => (await local.locks('brief', { base: text(), locks: rules.locks() })).rules;
// ---- the program ----
const lintBadge = el('span', { class: 'qs-badge', id: 'qs-lint', text: '…' });
const fileIn = el('input', { type: 'file', accept: '.qasm,.txt,text/plain', hidden: true, 'aria-hidden': 'true' });
@ -136,7 +156,7 @@ function mount(root) {
el('details', { class: 'qs-noise' }, el('summary', { text: 'Noise' }), el('div', { class: 'qs-row' }, ...noiseInputs.map((n) => n[1]))));
const program = el('section', { class: 'qs-pane qs-program', 'aria-labelledby': 'qs-program-h' },
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-program-h', text: 'Program' }), lintBadge, el('span', { class: 'qs-grow' }), openBtn, saveBtn, shareBtn, fileIn),
shows, reviewBox, editorHost, inputs.node, jobBox);
shows, reviewBox, editorHost, inputs.node, rules.node, jobBox);
// ---- the circuit ----
const circuit = el('pre', { class: 'qs-circuit', id: 'qs-circuit', tabindex: '0', 'aria-label': 'The circuit, drawn as text', hidden: true });
@ -176,6 +196,7 @@ function mount(root) {
draft: () => text(),
model: () => (aiConnected ? assistantFor() : null),
apply: (qasm, note, by) => offer(qasm, by, note),
check: (base, qasm) => local.locks('check', { base, proposed: qasm, locks: rules.locks(), inputs: genInputs() }),
});
// ---- the workbench ----
@ -258,7 +279,9 @@ function mount(root) {
engine: local,
text: () => text(),
setText: (s) => setText(s),
inputs: () => (declared(text()).length ? inputs.values() : {}),
inputs: genInputs,
rules: () => rules.locks(),
taken: () => { if (rules.region()) rules.setRegion(null); },
lock: (on) => { view.dispatch({ effects: editable.reconfigure(EditorState.readOnly.of(on)) }); editorHost.classList.toggle('qs-locked', on); },
reviewHost: reviewBox,
onHands: (b) => showHands(b),
@ -312,7 +335,7 @@ function mount(root) {
state: EditorState.create({
doc: '',
extensions: [
handsField, handsCol.of([]), editable.of(EditorState.readOnly.of(false)),
handsField, handsCol.of([]), editable.of(EditorState.readOnly.of(false)), heldLines,
lineNumbers(), highlightActiveLineGutter(), highlightActiveLine(), history(), drawSelection(), indentOnInput(), bracketMatching(), closeBrackets(),
highlightSelectionMatches(), openqasm, syntaxHighlighting(style), autocompletion({ override: [complete] }),
// A new value for a generator's input re-reads the program, as an edit does.
@ -369,6 +392,7 @@ function mount(root) {
legend.textContent = [...l.legend.map((c, i) => `${i + 1}: ${c}`), drawn < total ? `The first ${drawn} of ${total} columns are drawn; “Text” shows them all.` : ''].filter(Boolean).join(' · ');
critique.update(c);
paintHands();
paintHeld();
// The smells, on the diagram: each gate in one is flagged, its tooltip saying why.
const smells = c.ok ? c.smells : [];
mark(diagram, 'qc-flag', smells.flatMap((s) => s.ops), smells.flatMap((s) => s.qubits && !s.ops.length ? s.qubits : []));
@ -550,13 +574,23 @@ function mount(root) {
for (const h of Object.keys(HANDS)) mark(diagram, `qc-by-${h}`, by.flatMap((x, i) => (x === h ? [i] : [])));
}
// Each gate on a held line marked on the diagram.
async function paintHeld() {
const src = text();
const r = await local.locks('held', { source: src, inputs: genInputs() }).catch(() => null);
if (text() !== src) return;
mark(diagram, 'qc-held', r?.ops || []);
rules.show(r?.held?.length || 0);
}
// ---- a proposal, reviewed change by change ----
async function offer(qasm, by, note, opts = {}) {
if (versions.busy()) { setStatus('Choose on the proposal under review first.', 'bad'); return null; }
const r = await versions.review(qasm, by || { kind: 'person' }, note, opts);
if (r) {
shows.textContent = note || '';
setStatus(r.accepted ? `Took ${r.accepted} of ${r.offered} change${r.offered === 1 ? '' : 's'} from ${whoName(by)}, as ${r.version}.` : `Rejected the proposal from ${whoName(by)}; ${r.version} in Versions keeps it.`, 'ok');
const stops = r.blocked ? `; ${r.blocked} blocked by your rules` : '';
setStatus(r.accepted ? `Took ${r.accepted} of ${r.offered} change${r.offered === 1 ? '' : 's'} from ${whoName(by)}${stops}, as ${r.version}.` : `Took none of the proposal from ${whoName(by)}${stops}; ${r.version} in Versions keeps it.`, 'ok');
}
return r;
}
@ -580,6 +614,7 @@ function mount(root) {
const same = sameness(c.equivalence);
const r = await offer(c.qasm, { kind: 'engine', name: 'a fix' }, `Fixed: ${s.message}`, { all: true });
if (!r) return;
if (!r.accepted) { shows.textContent = ''; setStatus(`Your rules block that fix: it would change what they hold. ${r.version} in Versions keeps it.`, 'bad'); return; }
shows.textContent = `Fixed: ${s.message}. The engine checked the result: ${same.why || same.text}.`;
setStatus(`Fixed, as ${r.version}. Undo brings the lines back.`, 'ok');
} catch (e) { setStatus(e.message, 'bad'); }
@ -665,7 +700,7 @@ function mount(root) {
const assistantFor = () => {
const m = model();
if (!assistant || assistant.model.endpoint !== m.endpoint || assistant.model.name !== m.name) {
assistant = new Assistant({ engine: local, model: m, context: () => ({ qasm: text(), job: request(), last: current?.job }), stance: () => aiStance.value });
assistant = new Assistant({ engine: local, model: m, context: () => ({ qasm: text(), job: request(), last: current?.job }), stance: () => aiStance.value, rules: brief });
}
return assistant;
};
@ -748,7 +783,7 @@ function mount(root) {
forceLinting(view);
// `propose(qasm, by, note)`: a program offered as `by`'s proposal, taken
// change by change; it resolves once the reader has chosen.
return { view, run, fill, request, gallery, versions, propose: (qasm, by, note) => offer(qasm, by, note) };
return { view, run, fill, request, gallery, versions, rules, propose: (qasm, by, note) => offer(qasm, by, note) };
}
const root = document.getElementById('studio');

View file

@ -0,0 +1,103 @@
// The reader's rules for proposals (the engine's locks): blocks of the
// program held as they are, marked in it with `// @lock` and `// @unlock`;
// the gates a proposal may add; the qubit declarations held; gates and text
// to avoid; and the lines one rewrite may change. The engine checks every
// proposal against them change by change, and a change that breaks one is
// shown with the rule and cannot be taken. What the reader types is theirs to
// change.
import { Decoration, ViewPlugin } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
import { el } from './dom.js';
const SAVED = 'wai.quantum-studio.rules';
export const LOCK = '// @lock';
export const UNLOCK = '// @unlock';
const list = (s) => s.split(/[,\s]+/).map((x) => x.trim().toLowerCase()).filter(Boolean);
// The held blocks of `text`: [first, last] lines from 1, the markers included,
// as the engine reads them.
export function held(text) {
const out = [];
let open = null;
const lines = text.split('\n');
lines.forEach((l, i) => {
const t = l.trim();
if (open == null && t.startsWith(LOCK)) open = i + 1;
else if (open != null && t.startsWith(UNLOCK)) { out.push([open, i + 1]); open = null; }
});
if (open != null) out.push([open, lines.length]);
return out;
}
// The editor shades every held line.
const heldLine = Decoration.line({ class: 'qs-held-line' });
export const heldLines = ViewPlugin.fromClass(class {
constructor(view) { this.decorations = this.build(view); }
update(u) { if (u.docChanged || u.viewportChanged) this.decorations = this.build(u.view); }
build(view) {
const b = new RangeSetBuilder();
const doc = view.state.doc;
for (const [a, z] of held(doc.toString())) for (let n = a; n <= Math.min(z, doc.lines); n++) b.add(doc.line(n).from, doc.line(n).from, heldLine);
return b.finish();
}
}, { decorations: (v) => v.decorations });
// The pane. `selection()` gives the editor's selected lines [first, last];
// `hold(first, last)` puts markers around them as the reader's edit;
// `changed()` hears every change to the rules.
export function rulesPane({ selection, hold, changed }) {
let rules = { gates: '', qubits: false, avoid_gates: '', avoid_text: '', region: null };
try { Object.assign(rules, JSON.parse(localStorage.getItem(SAVED) || '{}')); } catch (e) {}
const keep = () => { try { localStorage.setItem(SAVED, JSON.stringify(rules)); } catch (e) {} };
const field = (id, label, value, placeholder) => {
const input = el('input', { class: 'qs-input', id, value, placeholder, spellcheck: 'false', autocomplete: 'off' });
return [input, el('label', { class: 'qs-field', for: id }, el('span', { text: label }), input)];
};
const [gates, gatesF] = field('qs-rule-gates', 'Gates a proposal may add', rules.gates, 'any');
const [avoidG, avoidGF] = field('qs-rule-avoid', 'Gates to avoid', rules.avoid_gates, 'none');
const [avoidT, avoidTF] = field('qs-rule-text', 'Text to avoid', rules.avoid_text, 'none, or text, one per comma');
const qubits = el('input', { type: 'checkbox', id: 'qs-rule-qubits', checked: !!rules.qubits });
const holdBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-rule-hold', type: 'button', text: 'Hold the selected lines', title: `Mark them with ${LOCK} and ${UNLOCK}: no proposal may change them` });
const regionBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-rule-region', type: 'button', text: 'Keep the next proposal to the selected lines' });
const regionChip = el('span', { class: 'qs-chip', id: 'qs-rule-region-chip', hidden: true });
const count = el('span', { class: 'qs-badge', id: 'qs-rules-count' });
const node = el('details', { class: 'qs-rules', id: 'qs-rules' },
el('summary', {}, el('span', { text: 'Rules for proposals' }), count),
el('div', { class: 'qs-rules-body' },
el('div', { class: 'qs-actions' }, holdBtn, regionBtn, regionChip),
el('div', { class: 'qs-row' }, gatesF, avoidGF),
el('div', { class: 'qs-row' }, avoidTF, el('label', { class: 'qs-check', for: 'qs-rule-qubits' }, qubits, el('span', { text: 'Hold the qubit declarations' }))),
el('p', { class: 'qs-note', text: `The engine checks every proposal against these, change by change: the assistant's, an alternative, a fix, an agent's. A change that breaks one cannot be taken. Lines between ${LOCK} and ${UNLOCK} are held. What you type is yours to change.` })));
// The rules as the engine reads them.
function locks() {
const l = {};
if (list(rules.gates).length) l.gates = list(rules.gates);
if (rules.qubits) l.qubits = true;
if (list(rules.avoid_gates).length) l.avoid_gates = list(rules.avoid_gates);
const t = rules.avoid_text.split(',').map((x) => x.trim()).filter(Boolean);
if (t.length) l.avoid_text = t;
if (rules.region) l.region = rules.region;
return l;
}
function show(heldCount = 0) {
const n = Object.keys(locks()).length + (heldCount ? 1 : 0);
count.textContent = n ? String(n) : '';
count.className = n ? 'qs-badge ok' : 'qs-badge';
regionChip.hidden = !rules.region;
if (rules.region) {
regionChip.replaceChildren(el('span', { text: `Only lines ${rules.region[0]} to ${rules.region[1]}, for the next proposal ` }),
el('button', { type: 'button', class: 'qs-chip-x', 'aria-label': 'Keep to no lines', text: '✕', onclick: () => setRegion(null) }));
}
}
function set(k, v) { rules[k] = v; keep(); show(); changed?.(); }
function setRegion(r) { set('region', r); }
gates.addEventListener('change', () => set('gates', gates.value));
avoidG.addEventListener('change', () => set('avoid_gates', avoidG.value));
avoidT.addEventListener('change', () => set('avoid_text', avoidT.value));
qubits.addEventListener('change', () => set('qubits', qubits.checked));
holdBtn.addEventListener('click', () => { const [a, z] = selection(); hold(a, z); });
regionBtn.addEventListener('click', () => setRegion(selection()));
show();
return { node, locks, setRegion, show, region: () => rules.region };
}

View file

@ -287,6 +287,20 @@
.qs-diff .qs-add { color: var(--ok-text); }
.qs-editor.qs-locked { opacity: 0.72; }
/* The reader's rules for proposals: held lines, held gates, blocked changes. */
.qs-held-line { background: repeating-linear-gradient(135deg, color-mix(in srgb, var(--qs-hand-person) 9%, transparent) 0 6px, transparent 6px 12px); }
.qc-held .qc-box, .qc-held .qc-ring { stroke-dasharray: 2 2; }
.qc-held .qc-box { fill: color-mix(in srgb, var(--qs-hand-person) 10%, var(--bg-surface)); }
.qs-rules { margin: 0 12px 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-base); }
.qs-rules > summary { display: flex; align-items: center; gap: 8px; padding: 6px 10px; cursor: pointer; font: 12px var(--font-mono, ui-monospace, monospace); color: var(--text-secondary); }
.qs-rules > summary .qs-badge:empty { display: none; }
.qs-rules-body { display: flex; flex-direction: column; gap: 8px; padding: 0 10px 10px; }
.qs-rules-body .qs-note { margin: 0; }
.qs-chip-x { margin-left: 4px; border: 0; background: none; color: inherit; cursor: pointer; font: inherit; }
.qs-chip-x:focus-visible { outline: 2px solid var(--accent); }
.qs-review-list li.qs-blocked pre { opacity: 0.6; }
.qs-blocked-why { margin: 0; font-size: 12px; color: var(--bad-text); }
/* Predict first. */
.qs-check { display: inline-flex; align-items: center; gap: 6px; font: 12px var(--font-mono, ui-monospace, monospace); color: var(--text-secondary); cursor: pointer; }
.qs-check input { accent-color: var(--accent); width: 15px; height: 15px; margin: 0; }

View file

@ -49,8 +49,10 @@ function lanes(versions) {
// `setText(s)` read and write the editor; `inputs()` gives a generator's
// values (or {} for a program with none); `lock(on)` makes the editor wait
// while a proposal is reviewed in `reviewHost`; `onHands(blame)` hears whose
// hand each line and gate is in; `save(name, body)` keeps a file.
export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, onHands, save, status }) {
// hand each line and gate is in; `save(name, body)` keeps a file; `rules()`
// gives the reader's rules for proposals (the engine's locks), and `taken()`
// hears that a proposal was decided.
export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, onHands, save, status, rules = () => ({}), taken = () => {} }) {
let history = null;
let current = null;
let selected = null;
@ -135,37 +137,44 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost,
const base = source(current);
const { changes } = await call('changes', { base, proposed });
if (!changes.length) { status?.(merge ? `${merge} holds nothing the program does not.` : 'The proposal is the program as it is.'); return null; }
const finish = async (accepted) => {
// The reader's rules bind a proposal, not their own merge.
const breaches = merge ? changes.map(() => []) : (await engine.locks('check', { base, proposed, locks: rules(), inputs: inputs() })).changes.map((c) => c.breaches);
const blocked = breaches.flatMap((b, i) => (b.length ? [i] : []));
const finish = async (picked) => {
const accepted = picked.filter((i) => !blocked.includes(i));
await serial(async () => {
if (merge) {
const { source: merged } = await call('apply', { base, proposed, accepted });
const r = await call('merge', { history, a: current, b: merge, source: merged, by: { kind: 'person' }, note: note || `${merge} brought in`, at: Date.now() });
history = r.history; current = r.version;
} else {
const r = await call('propose', { history, parent: current, proposed, accepted, by, note, at: Date.now() });
const r = await call('propose', { history, parent: current, proposed, accepted, blocked, by, note, at: Date.now() });
history = r.history; current = r.version;
}
selected = null; keep();
});
reviewing = false; lock(false); reviewHost.replaceChildren(); reviewHost.hidden = true;
setText(source(current));
if (!merge) taken();
await render();
return { offered: changes.length, accepted: accepted.length, version: current };
return { offered: changes.length, accepted: accepted.length, blocked: blocked.length, version: current };
};
if (all) return finish(changes.map((_, i) => i));
reviewing = true; lock(true);
return new Promise((resolve) => {
const boxes = changes.map((c, i) => el('input', { type: 'checkbox', id: `qs-review-${i}`, checked: true }));
const boxes = changes.map((c, i) => el('input', { type: 'checkbox', id: `qs-review-${i}`, checked: !blocked.includes(i), disabled: blocked.includes(i) }));
const take = el('button', { class: 'qs-btn qs-primary qs-small', id: 'qs-review-apply', type: 'button' });
const count = () => { const n = boxes.filter((b) => b.checked).length; take.textContent = n === changes.length ? `Take all ${changes.length}` : `Take ${n} of ${changes.length}`; take.disabled = false; };
const open = changes.length - blocked.length;
const count = () => { const n = boxes.filter((b) => b.checked).length; take.textContent = n === changes.length ? `Take all ${changes.length}` : `Take ${n} of ${open}`; take.disabled = n === 0 && open === 0; };
boxes.forEach((b) => b.addEventListener('change', count)); count();
const who = merge ? `${merge} (${whoName(history.versions.find((v) => v.id === merge)?.by)})` : whoName(by);
reviewHost.replaceChildren(
el('p', { class: 'qs-review-head' }, el('b', { text: merge ? `Bring in ${merge}` : `A proposal from ${who}` }),
el('span', { class: 'qs-note', text: ` · ${plural(changes.length, 'change')}. Take the ones you want; the program waits until you choose.` })),
el('span', { class: 'qs-note', text: ` · ${plural(changes.length, 'change')}${blocked.length ? `, ${blocked.length} blocked by your rules` : ''}. Take the ones you want; the program waits until you choose.` })),
note ? el('p', { class: 'qs-note qs-review-note', text: note }) : null,
el('ol', { class: 'qs-review-list' }, ...changes.map((c, i) => el('li', {},
el('ol', { class: 'qs-review-list' }, ...changes.map((c, i) => el('li', { class: blocked.includes(i) ? 'qs-blocked' : '' },
el('label', { class: 'qs-check', for: `qs-review-${i}` }, boxes[i], el('span', { text: `at line ${c.line}` })),
breaches[i].length ? el('p', { class: 'qs-blocked-why', text: `Blocked: ${breaches[i].map((b) => b.why).join('; ')}.` }) : null,
el('pre', { class: 'qs-diff' }, ...c.removed.map((l) => el('span', { class: 'qs-del', text: `− ${l}\n` })), ...c.added.map((l) => el('span', { class: 'qs-add', text: `+ ${l}\n` })))))),
el('div', { class: 'qs-actions' }, take,
el('button', { class: 'qs-btn qs-small', id: 'qs-review-reject', type: 'button', text: merge ? 'Not now' : 'Reject all', onclick: async () => {
@ -234,7 +243,7 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost,
detail.replaceChildren(
el('p', { class: 'qs-vs-what' }, el('b', { text: v.id }), el('span', { text: ` ${made(v)}${v.parents?.length ? `, from ${v.parents.join(' and ')}` : ''}, ${new Date(v.at).toLocaleString()}` })),
v.note ? el('p', { class: 'qs-note', text: v.note }) : null,
r ? el('p', { class: 'qs-note', id: 'qs-vs-review', text: `${r.accepted.length} of ${plural(r.offered, 'change')} taken${r.accepted.length < r.offered ? '; the rest were rejected, and the whole proposal is kept below' : ''}.` }) : null,
r ? el('p', { class: 'qs-note', id: 'qs-vs-review', text: `${r.accepted.length} of ${plural(r.offered, 'change')} taken${r.blocked?.length ? `, ${r.blocked.length} blocked by the rules` : ''}${r.accepted.length < r.offered ? '; the rest were not taken, and the whole proposal is kept below' : ''}.` }) : null,
r && r.accepted.length < r.offered ? el('details', {}, el('summary', { text: 'The whole proposal' }), el('pre', { class: 'qs-ai-code', text: r.proposed })) : null,
parent ? el('p', { class: 'qs-note', text: `+${e.added ?? 0} −${e.removed ?? 0} against ${parent}${e.sealed ? ' · sealed' : ''}` }) : null,
actions.length ? el('div', { class: 'qs-actions' }, ...actions) : null);
@ -260,7 +269,7 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost,
el('button', { type: 'button', class: 'qs-vs-item', onclick: () => showDetail(v, log) },
el('span', { class: 'qs-vs-id', text: v.id }),
el('span', { class: 'qs-vs-by', text: made(v) }),
el('span', { class: 'qs-note', text: `${e.review ? `${e.review.accepted} of ${e.review.offered} taken · ` : ''}+${e.added ?? 0} −${e.removed ?? 0}${tags.length ? ` · ${tags.join(', ')}` : ''}` })));
el('span', { class: 'qs-note', text: `${e.review ? `${e.review.accepted} of ${e.review.offered} taken${e.review.blocked ? `, ${e.review.blocked} blocked` : ''} · ` : ''}+${e.added ?? 0} −${e.removed ?? 0}${tags.length ? ` · ${tags.join(', ')}` : ''}` })));
}));
const b = await blame().catch(() => null);
share.replaceChildren(...shareOf(b?.authorship));

View file

@ -49,6 +49,7 @@ self.onmessage = async (ev) => {
case 'discover_submit': text = e().discover_submit(JSON.stringify(ev.data.spec), JSON.stringify(ev.data.archive), ev.data.source, ev.data.parent ?? undefined, ev.data.proposer, ev.data.note ?? undefined); break;
case 'discover_search': text = e().discover_search(JSON.stringify(ev.data.spec), JSON.stringify(ev.data.archive), ev.data.from >>> 0, ev.data.evaluations >>> 0); break;
case 'history': text = e().history(ev.data.name, JSON.stringify(ev.data.args ?? {})); break;
case 'locks': text = mod.engine_locks(ev.data.name, JSON.stringify(ev.data.args ?? {})); break;
default: throw new Error(`no operation ${op}`);
}
const value = JSON.parse(text);

View file

@ -15,6 +15,9 @@
// - 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;
@ -246,7 +249,7 @@ const chip = (k) => page.$eval(`#qs-measures [data-k=${k}]`, (c) => c.textConten
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 rejected/.test(await page.textContent('#qs-vs-detail')), 'the version keeps what was rejected');
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-', '')));
@ -303,6 +306,52 @@ const chip = (k) => page.$eval(`#qs-measures [data-k=${k}]`, (c) => c.textConten
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';
@ -495,6 +544,10 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-
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');
@ -510,6 +563,9 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-
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(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);

View file

@ -209,6 +209,32 @@ wai-quantum verify job.json --history prog.history.json
Through `wai-quantum mcp`, an agent's edits (`history_commit`) are recorded as an outside agent's, under the name its client gave when it connected. An agent cannot record a change as the person's.
## The person's rules for proposals
A proposal (a model's, an agent's, an engine rewrite) must keep the person's rules, and the engine checks it change by change:
- **Held blocks**, marked in the program itself: the lines from a `// @lock` comment to the next `// @unlock`, the markers included.
- **The other rules**, in `prog.locks.json` beside the program:
```json
{ "gates": ["h", "cx", "rz"], "qubits": true, "avoid_gates": ["t"], "avoid_text": ["reset"], "region": [4, 9] }
```
- `gates`: the only gates a change may add;
- `qubits`: hold the qubit declarations;
- `avoid_gates` and `avoid_text`: what no change may add;
- `region`: the lines a rewrite must keep to.
```sh
wai-quantum locks check prog.qasm theirs.qasm # each change, and the rules it breaks (exit 1 if any)
wai-quantum locks brief prog.qasm # the rules as a model is told them
```
What the person types is theirs to change; the rules bind proposals. Only what a change adds is judged, so a gate the program already uses is never a proposal's breach.
- **The history:** `history changes` names each change's breaches. `history propose` refuses to accept a change that breaks a rule, `--accept all` takes only the changes the rules allow, and the version records the rest as blocked.
- **For agents:** through `wai-quantum mcp`, `history_commit` refuses an agent's edit that breaks a rule, with the rule, and `history_changes` shows each change's breaches and the rules as sentences.
## The research tools
`wai-quantum lab …` holds the research tools:

View file

@ -9,10 +9,16 @@
//! `run --history` puts the head's account (its digest, and the gates and
//! lines in each hand) in the job's request, so the receipt binds it, and
//! `verify --history` recomputes it.
//!
//! The person's rules ([`wai_quantum::engine::locks`]) bind a proposal: held
//! blocks in the program, and `prog.locks.json` beside it. `changes` names the
//! rules each change breaks, and `propose` refuses to accept one, recording
//! it as blocked.
use clap::{Args, Subcommand};
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use wai_quantum::engine::locks::{self, Locks};
use wai_quantum::engine::versions::{By, Hand, History, Kind, changes};
#[derive(Args)]
@ -22,6 +28,9 @@ pub struct Files {
/// The history (default: beside the program, as NAME.history.json)
#[arg(long)]
history: Option<PathBuf>,
/// The rules a proposal must keep (default: beside the program, as NAME.locks.json, if there)
#[arg(long)]
locks: Option<PathBuf>,
}
#[derive(Args, Clone)]
@ -139,6 +148,29 @@ pub fn path(program: &Path, given: Option<&Path>) -> PathBuf {
})
}
/// The rules beside `program` (`NAME.locks.json`), or the ones given; none
/// when there is no such file beside it.
pub fn load_locks(program: &Path, given: Option<&Path>) -> Result<Locks, String> {
let p = given.map(Path::to_path_buf).unwrap_or_else(|| {
let stem = program.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "program".into());
program.with_file_name(format!("{stem}.locks.json"))
});
if given.is_none() && !p.exists() {
return Ok(Locks::default());
}
let text = std::fs::read_to_string(&p).map_err(|e| format!("{}: {e}", p.display()))?;
let v: Value = serde_json::from_str(&text).map_err(|e| format!("{}: not JSON: {e}", p.display()))?;
Locks::from_json(&v).map_err(|e| format!("{}: {e}", p.display()))
}
/// The changes the rules block, and why, as `locks::check` gives them.
pub fn blocked(check: &Value) -> Vec<(usize, Vec<String>)> {
check["changes"].as_array().into_iter().flatten().filter_map(|c| {
let why: Vec<String> = c["breaches"].as_array()?.iter().filter_map(|b| b["why"].as_str().map(str::to_string)).collect();
(!why.is_empty()).then(|| (c["index"].as_u64().unwrap_or(0) as usize, why))
}).collect()
}
pub fn load(path: &Path) -> Result<History, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e} (wai-quantum history start makes one)", path.display()))?;
let v: Value = serde_json::from_str(&text).map_err(|e| format!("{}: not JSON: {e}", path.display()))?;
@ -232,9 +264,12 @@ pub fn main(cmd: Cmd) {
Cmd::Changes { files, proposed, parent, json } => (|| {
let h = load(&path(&files.program, files.history.as_deref()))?;
let base = h.get(&from(&h, parent.as_deref())?).map(|v| v.source.clone()).unwrap_or_default();
let ch = changes(&base, &read(&proposed)?);
let text = read(&proposed)?;
let ch = changes(&base, &text);
let rules = locks::check(&base, &text, &load_locks(&files.program, files.locks.as_deref())?, &super::INPUTS.get().cloned().unwrap_or_default());
let held: std::collections::BTreeMap<usize, Vec<String>> = blocked(&rules).into_iter().collect();
if json {
println!("{}", json!(ch));
println!("{}", json!({ "changes": ch, "rules": rules }));
return Ok(());
}
if ch.is_empty() {
@ -242,7 +277,10 @@ pub fn main(cmd: Cmd) {
}
{
for (n, c) in ch.iter().enumerate() {
println!("change {n}, at line {}:", c.line);
match held.get(&n) {
Some(why) => println!("change {n}, at line {}: blocked, {}", c.line, why.join("; ")),
None => println!("change {n}, at line {}:", c.line),
}
for l in &c.removed {
println!(" - {l}");
}
@ -258,13 +296,24 @@ pub fn main(cmd: Cmd) {
let mut h = load(&p)?;
let parent = from(&h, parent.as_deref())?;
let text = read(&proposed)?;
let offered = changes(&h.get(&parent).map(|v| v.source.clone()).unwrap_or_default(), &text).len();
let acc = accepted(&accept, offered)?;
let v = h.propose(&parent, &text, &acc, By::new(by, name.as_deref()), note.as_deref(), now(), Some(&key(&k)))?;
let base = h.get(&parent).map(|v| v.source.clone()).unwrap_or_default();
let offered = changes(&base, &text).len();
let rules = locks::check(&base, &text, &load_locks(&files.program, files.locks.as_deref())?, &super::INPUTS.get().cloned().unwrap_or_default());
let held = blocked(&rules);
let stopped: Vec<usize> = held.iter().map(|(i, _)| *i).collect();
// `all` takes every change the rules allow; a blocked one named is refused.
let mut acc = accepted(&accept, offered)?;
if accept.trim() == "all" {
acc.retain(|i| !stopped.contains(i));
} else if let Some((i, why)) = held.iter().find(|(i, _)| acc.contains(i)) {
return Err(format!("change {i} breaks the rules: {}", why.join("; ")));
}
let v = h.propose_with(&parent, &text, &acc, &stopped, By::new(by, name.as_deref()), note.as_deref(), now(), Some(&key(&k)))?;
let (id, source) = (v.id.clone(), v.source.clone());
save(&h, &p)?;
std::fs::write(&files.program, &source).map_err(|e| format!("{}: {e}", files.program.display()))?;
println!("{id} from {parent} {}: {} of {offered} changes accepted; {} written", who_of(&h, &id), acc.len(), files.program.display());
let stops = if stopped.is_empty() { String::new() } else { format!(", {} blocked by the rules", stopped.len()) };
println!("{id} from {parent} {}: {} of {offered} changes accepted{stops}; {} written", who_of(&h, &id), acc.len(), files.program.display());
Ok(())
})(),
Cmd::Merge { files, a, b, who, key: k } => (|| {
@ -285,7 +334,10 @@ pub fn main(cmd: Cmd) {
for (e, v) in log.iter().zip(&h.versions) {
let how = e["how"].as_str().unwrap_or("");
let from = if v.parents.is_empty() { String::new() } else { format!(" from {}", v.parents.join(" and ")) };
let review = e.get("review").map(|r| format!(", {} of {} changes accepted", r["accepted"], r["offered"])).unwrap_or_default();
let review = e.get("review").map(|r| {
let stops = r["blocked"].as_u64().filter(|&n| n > 0).map(|n| format!(", {n} blocked by the rules")).unwrap_or_default();
format!(", {} of {} changes accepted{stops}", r["accepted"], r["offered"])
}).unwrap_or_default();
let head = if e["head"] == true { " (head)" } else { "" };
println!("{:<5} {how}{from} by {}: +{} −{}{review}{head}", v.id, who(&v.by), e["added"], e["removed"]);
if let Some(n) = &v.note {

View file

@ -0,0 +1,73 @@
//! `wai-quantum locks`: the person's rules for what a proposal may change
//! ([`wai_quantum::engine::locks`]). Held blocks are marked in the program
//! (`// @lock` … `// @unlock`); the other rules are JSON beside it
//! (`prog.locks.json`):
//!
//! ```json
//! { "gates": ["h", "cx", "rz"], "qubits": true, "avoid_gates": ["t"], "avoid_text": ["reset"], "region": [4, 9] }
//! ```
use clap::Subcommand;
use std::path::PathBuf;
use wai_quantum::engine::locks;
#[derive(Subcommand)]
pub enum Cmd {
/// Each change a proposed program makes to a program, and the rules it breaks
Check {
/// The program as it is
base: PathBuf,
/// The program proposed
proposed: PathBuf,
/// The rules (default: beside the program, as NAME.locks.json, if there)
#[arg(long)]
locks: Option<PathBuf>,
#[arg(long)]
json: bool,
},
/// The rules as a model is told them
Brief {
program: PathBuf,
#[arg(long)]
locks: Option<PathBuf>,
},
}
pub fn main(cmd: Cmd) {
let inputs = super::INPUTS.get().cloned().unwrap_or_default();
let r = (|| match cmd {
Cmd::Check { base, proposed, locks: l, json } => {
let rules = crate::history::load_locks(&base, l.as_deref())?;
let v = locks::check(&super::read_text(Some(&base))?, &super::read_text(Some(&proposed))?, &rules, &inputs);
if json {
println!("{v}");
} else {
let changes = v["changes"].as_array().cloned().unwrap_or_default();
if changes.is_empty() {
println!("no changes");
}
for c in &changes {
let why: Vec<&str> = c["breaches"].as_array().into_iter().flatten().filter_map(|b| b["why"].as_str()).collect();
let verdict = if why.is_empty() { "keeps the rules".to_string() } else { format!("breaks them: {}", why.join("; ")) };
println!("change {}, at line {}: {verdict}", c["index"], c["line"]);
}
}
if v["ok"] != true {
std::process::exit(1);
}
Ok(())
}
Cmd::Brief { program, locks: l } => {
let rules = crate::history::load_locks(&program, l.as_deref())?;
let brief = locks::brief(&super::read_text(Some(&program))?, &rules);
if brief.is_empty() {
println!("no rules: a proposal may change anything");
}
for line in brief {
println!("{line}");
}
Ok(())
}
})();
r.unwrap_or_else(|e: String| super::die(&e));
}

View file

@ -17,6 +17,7 @@
//! wai-quantum discover new ghz.json # the workbench: an archive for a specification
//! wai-quantum discover search ghz.json # cheaper generators, each checked at every size
//! wai-quantum history blame prog.qasm # whose hand each line and gate is in
//! wai-quantum locks check prog.qasm theirs.qasm # the rules each proposed change breaks
//! wai-quantum run prog.qasm --history prog.history.json # the receipt binds who wrote it
//! wai-quantum submit prog.qasm # on the job service
//! wai-quantum verify result.json # a result's receipt, or a lab receipt
@ -40,6 +41,7 @@
mod discover;
mod history;
mod lab;
mod locks;
mod mcp;
use clap::{Args, CommandFactory, Parser, Subcommand};
@ -192,6 +194,11 @@ enum Command {
#[command(subcommand)]
cmd: history::Cmd,
},
/// The person's rules for what a proposal may change: held blocks, gates, qubits, what to avoid, a region (`wai-quantum locks --help`)
Locks {
#[command(subcommand)]
cmd: locks::Cmd,
},
/// The research tools: error correction, DMRG, neural states, resource estimates and more (`wai-quantum lab help`)
Lab {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
@ -673,6 +680,7 @@ fn main() {
}
Command::Discover { cmd } => discover::main(cmd),
Command::History { cmd } => history::main(cmd),
Command::Locks { cmd } => locks::main(cmd),
Command::Lab { args } => lab::main(args),
Command::Completions { shell } => {
clap_complete::generate(shell, &mut Cli::command(), "wai-quantum", &mut std::io::stdout());

View file

@ -5,7 +5,9 @@
//!
//! An agent's edits to a program's history (`history_commit`) are recorded as
//! an outside agent's, under the name it gave when it connected: an agent
//! cannot record a change as the person's.
//! cannot record a change as the person's, and an edit that breaks the
//! person's rules (held blocks in the program, `prog.locks.json` beside it) is
//! refused with the rule it breaks.
use serde_json::{Value, json};
use std::io::{BufRead, Write};
@ -121,14 +123,14 @@ fn tools() -> Value {
{
"name": "history_commit",
"title": "Record your edit in a program's history",
"description": "Record your change to a program as an edit in its history (prog.qasm → prog.history.json), sealed with this user's key. It is recorded as an outside agent's, under the name you gave when you connected, so the person can see which lines and gates are yours and accept or undo them. Give the program as you leave it in qasm (the file is written), or leave qasm out to record the file as it now is. With no history yet, the file as it was starts one.",
"description": "Record your change to a program as an edit in its history (prog.qasm → prog.history.json), sealed with this user's key. It is recorded as an outside agent's, under the name you gave when you connected, so the person can see which lines and gates are yours and accept or undo them. Give the program as you leave it in qasm (the file is written), or leave qasm out to record the file as it now is. With no history yet, the file as it was starts one. The person's rules bind your change: lines between `// @lock` and `// @unlock` stay as they are, and prog.locks.json beside the program may name the gates allowed, hold the qubit declarations, name gates and text to avoid, or a region to keep to. A change that breaks one is refused with the rule; history_changes shows them first.",
"inputSchema": { "type": "object", "required": ["program"], "properties": { "program": { "type": "string", "description": "The program file's path." }, "qasm": { "type": "string", "description": "The program as you leave it; the file is written with it." }, "parent": { "type": "string", "description": "The version you changed (default: the head)." }, "note": { "type": "string", "description": "What the change does, in a sentence." }, "history": { "type": "string", "description": "The history's path, if not beside the program." } } },
},
{
"name": "history_changes",
"title": "The changes a program would make",
"description": "The changes, line by line and numbered, that a program would make to the head of a program's history (or another version): what a person reviews one by one.",
"inputSchema": { "type": "object", "required": ["program", "qasm"], "properties": { "program": { "type": "string", "description": "The program file's path." }, "qasm": { "type": "string", "description": "The proposed program." }, "parent": { "type": "string", "description": "The version to compare with (default: the head)." }, "history": { "type": "string", "description": "The history's path, if not beside the program." } } },
"description": "The changes, line by line and numbered, that a program would make to the head of a program's history (or another version): what a person reviews one by one. Each comes with the person's rules it breaks, if any, and `rules` lists the rules as sentences.",
"inputSchema": { "type": "object", "required": ["program", "qasm"], "properties": { "program": { "type": "string", "description": "The program file's path." }, "qasm": { "type": "string", "description": "The proposed program." }, "parent": { "type": "string", "description": "The version to compare with (default: the head)." }, "inputs": inputs_schema(), "history": { "type": "string", "description": "The history's path, if not beside the program." } } },
},
{
"name": "history_blame",
@ -272,6 +274,13 @@ fn history(name: &str, args: &Value, key: &SigningKey, client: &Client) -> Resul
let path = crate::history::path(program, text("history").map(Path::new));
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0);
let head = |h: &History, given: Option<&str>| -> Result<String, String> { Ok(match given { Some(v) => v.to_string(), None => h.head()?.id.clone() }) };
let rules = || crate::history::load_locks(program, None);
let inputs = || -> Result<wai_quantum::qasm::Inputs, String> {
match args.get("inputs") {
None | Some(Value::Null) => Ok(Default::default()),
Some(v) => serde_json::from_value(v.clone()).map_err(|e| format!("inputs: {e}")),
}
};
match name {
"history_commit" => {
let me = By::new(Kind::Agent, Some(client.name.as_deref().unwrap_or("an agent")));
@ -284,6 +293,13 @@ fn history(name: &str, args: &Value, key: &SigningKey, client: &Client) -> Resul
};
let source = text("qasm").map(str::to_string).unwrap_or(on_disk);
let parent = head(&h, text("parent"))?;
let base = h.get(&parent).map(|v| v.source.clone()).ok_or("no such version")?;
let check = wai_quantum::engine::locks::check(&base, &source, &rules()?, &inputs()?);
let stops = crate::history::blocked(&check);
if !stops.is_empty() {
let why: Vec<String> = stops.iter().map(|(i, w)| format!("change {i}: {}", w.join("; "))).collect();
return Err(format!("the change breaks the person's rules, so it was not recorded: {}", why.join(" | ")));
}
let id = h.commit(&parent, &source, me, text("note"), now, Some(key))?.id.clone();
std::fs::write(program, &source).map_err(|e| format!("{}: {e}", program.display()))?;
std::fs::write(&path, serde_json::to_string_pretty(&h).map_err(|e| e.to_string())? + "\n").map_err(|e| format!("{}: {e}", path.display()))?;
@ -292,15 +308,15 @@ fn history(name: &str, args: &Value, key: &SigningKey, client: &Client) -> Resul
"history_changes" => {
let h = crate::history::load(&path)?;
let base = h.get(&head(&h, text("parent"))?).map(|v| v.source.clone()).ok_or("no such version")?;
Ok(json!({ "changes": changes(&base, text("qasm").ok_or("give \"qasm\": the proposed program")?) }))
let proposed = text("qasm").ok_or("give \"qasm\": the proposed program")?;
let r = rules()?;
let check = wai_quantum::engine::locks::check(&base, proposed, &r, &inputs()?);
Ok(json!({ "changes": changes(&base, proposed), "breaches": check["changes"], "rules": wai_quantum::engine::locks::brief(&base, &r) }))
}
"history_blame" => {
let h = crate::history::load(&path)?;
let id = head(&h, text("version"))?;
let inputs: wai_quantum::qasm::Inputs = match args.get("inputs") {
None | Some(Value::Null) => Default::default(),
Some(v) => serde_json::from_value(v.clone()).map_err(|e| format!("inputs: {e}"))?,
};
let inputs = inputs()?;
Ok(json!({ "version": id, "lines": h.blame(&id)?, "authorship": h.authorship(&id, &inputs)? }))
}
"history_log" => {
@ -326,7 +342,7 @@ fn handle(msg: &Value, memory_gb: u64, key: &SigningKey, client: &mut Client) ->
"protocolVersion": params.get("protocolVersion").and_then(Value::as_str).unwrap_or(PROTOCOL),
"capabilities": { "tools": {} },
"serverInfo": { "name": "wai-quantum", "title": "wai-quantum", "version": env!("CARGO_PKG_VERSION") },
"instructions": "Quantum programs in OpenQASM, run on this machine as the quantum job service runs them. Plan or lint before a large run. Every result names its method and accuracy; quote its numbers rather than estimating them."
"instructions": "Quantum programs in OpenQASM, run on this machine as the quantum job service runs them. Plan or lint before a large run. Every result names its method and accuracy; quote its numbers rather than estimating them. When you change a person's program, record the change with history_commit: it is kept as yours, and the person sees which lines and gates you wrote. The person's rules bind your changes: lines between // @lock and // @unlock stay as they are, and NAME.locks.json beside the program may name the gates allowed, hold the qubit declarations, name what to avoid, or a region to keep to. history_changes shows what each change would break before you make it."
})),
"ping" => ok(json!({})),
"tools/list" => ok(json!({ "tools": tools() })),
@ -376,6 +392,8 @@ mod tests {
fn an_agent_initializes_lists_the_tools_and_runs_a_circuit() {
let init = handle(&json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": PROTOCOL } }), 1, &key(), &mut Client::default()).unwrap();
assert_eq!(init["result"]["serverInfo"]["name"], "wai-quantum");
let told = init["result"]["instructions"].as_str().unwrap();
assert!(told.contains("history_commit") && told.contains("// @lock"), "an agent is told to record its changes and keep the person's rules: {told}");
assert!(handle(&json!({ "jsonrpc": "2.0", "method": "notifications/initialized" }), 1, &key(), &mut Client::default()).is_none());
let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key(), &mut Client::default()).unwrap();
let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect();
@ -463,6 +481,22 @@ mod tests {
let log = tool(&mut client, "history_log", json!({ "program": p }));
assert_eq!(log["check"]["ok"], true, "{log}");
assert_eq!(log["versions"][0]["by"]["kind"], "origin");
// The person holds a block and keeps the gates to h, cx and x: an edit
// inside the block, or one adding a cz, is refused with its rule.
let held = "OPENQASM 3.0;\nqubit[2] q;\n// @lock\nh q[0];\n// @unlock\ncx q[0], q[1];\nx q[1];\n";
let c = tool(&mut client, "history_commit", json!({ "program": p, "qasm": held, "note": "the person's hold" }));
assert_eq!(c["version"], "v2", "{c}");
std::fs::write(dir.join("bell.locks.json"), r#"{ "gates": ["h", "cx", "x"] }"#).unwrap();
let call_err = |client: &mut Client, args: Value| handle(&json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "history_commit", "arguments": args } }), 1, &key(), client).unwrap()["result"].clone();
let r = call_err(&mut client, json!({ "program": p, "qasm": held.replace("h q[0];", "h q[1];") }));
assert!(r["isError"] == true && r["content"][0]["text"].as_str().unwrap().contains("are held"), "{r}");
let r = call_err(&mut client, json!({ "program": p, "qasm": held.replace("x q[1];", "cz q[0], q[1];") }));
assert!(r["isError"] == true && r["content"][0]["text"].as_str().unwrap().contains("only cx, h, x may be used"), "{r}");
let ch = tool(&mut client, "history_changes", json!({ "program": p, "qasm": held.replace("x q[1];", "cz q[0], q[1];") }));
assert_eq!(ch["breaches"][0]["breaches"][0]["rule"], "gates", "{ch}");
assert!(ch["rules"].as_array().unwrap().iter().any(|r| r.as_str().unwrap().contains("@lock")), "{ch}");
let ok = tool(&mut client, "history_commit", json!({ "program": p, "qasm": held.replace("x q[1];", "x q[0];") }));
assert_eq!(ok["version"], "v3", "{ok}");
std::fs::remove_dir_all(&dir).ok();
}

View file

@ -26,7 +26,7 @@ use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use wai_quantum::ed25519_dalek::SigningKey;
use wai_quantum::engine::discover::{Archive, Budget, Evaluator, SearchOptions, Spec, search};
use wai_quantum::engine::{catalog, critique, draw, energy, equiv, job, shortcuts, versions};
use wai_quantum::engine::{catalog, critique, draw, energy, equiv, job, locks, shortcuts, versions};
/// What a result says of its energy here.
const UNMETERED: &str = "the desktop app does not meter a run's energy";
@ -201,6 +201,17 @@ fn history(studio: tauri::State<'_, Studio>, name: String, args: String) -> Stri
history_in(&studio.key, &name, &args)
}
/// `name` on the person's rules for proposals ([`locks::call`]): the answer,
/// or `{error}`.
#[tauri::command]
fn locks(name: String, args: String) -> String {
serde_json::from_str::<Value>(&args)
.map_err(|e| format!("{name}: the arguments are not JSON: {e}"))
.and_then(|a| locks::call(&name, &a))
.map(|v| v.to_string())
.unwrap_or_else(error_json)
}
fn history_in(key: &SigningKey, name: &str, args: &str) -> String {
serde_json::from_str::<Value>(args)
.map_err(|e| format!("{name}: the arguments are not JSON: {e}"))
@ -401,7 +412,7 @@ fn main() {
};
tauri::Builder::default()
.manage(Studio { key, job_bytes: job_bytes(), running: Arc::new(Mutex::new(None)), cancelled: Arc::new(Mutex::new(false)) })
.invoke_handler(tauri::generate_handler![run_job, cancel, plan_job, methods, lint, critique, equivalent, variants, shortcuts, draw, layout, verify, signer, save_job, save_program, chat, models, discover_start, discover_submit, discover_search, discover_evaluate, instantiate, history])
.invoke_handler(tauri::generate_handler![run_job, cancel, plan_job, methods, lint, critique, equivalent, variants, shortcuts, draw, layout, verify, signer, save_job, save_program, chat, models, discover_start, discover_submit, discover_search, discover_evaluate, instantiate, history, locks])
.run(tauri::generate_context!())
.expect("the window opens");
}

View file

@ -68,7 +68,7 @@ never creates information.
| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), as `wai.quantum.circuit2` by default or as the frozen `wai.quantum.circuit` |
| `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded); **device-ready OpenQASM 2/3** (`device::device_qasm`): every gate lowered exactly to its dialect's standard library, multi-controlled gates included, every qubit measured into `c[i]`, and the service's counts read back onto the crate's basis; **native programs** (`native::compile`): a processor's own basis (`rz`/`sx`/`x`/`cz`; `prx`/`cz`; or `gpi`/`gpi2`/`zz` with angles in turns, written exactly; the Z rotations carried in the phases of the last two) on its own qubits, routed over its coupling map, and checked against the circuit by simulation before it is sent — Bloch / entanglement / purity analysis |
| `quantum_qasm` | **the OpenQASM reader the job service runs**, for every surface that takes a program: OpenQASM 3 and the OpenQASM 2 forms in common use, every register, a gate on a whole register, gate definitions with parameters, `ctrl`/`negctrl` modifiers, angle expressions, comments, and measurement in every spelling. A program that measures and then acts, resets after a gate or uses `if` is kept as dynamic steps. Generators: integer `input`s, `const`s and `for` loops (ranges, steps, sets, nested), with integer arithmetic in indices and sizes and `if` on integers decided on reading, so one program describes a family of circuits; `qasm::parse_with` reads it at given inputs and `qasm::instantiate` writes that circuit out as plain OpenQASM. `while`, `inv`, `pow` and anything else not listed are refused with their line. Lowered to each method's input exactly up to a global phase. `qasm::dyadic_circuit` gives the exact state vector's circuit (dyadic angles) for a program whose measurements all come at the end, and refuses any other with the reason |
| `quantum_engine` | **the quantum job service's engine, for every host**: a job (an OpenQASM program and what is asked of it: shots, Pauli expectation values, amplitudes, noise) planned onto the strongest method it admits, with the reason and the memory and work it will take; run; answered in the service's JSON with its accuracy class; and sealed over the request and the result in their canonical JSON (`engine::job`, `engine::receipt`), so a result from the service, the command line or a browser tab checks the same way. Also a program's circuit drawn as text (`engine::draw`); the specification-and-evaluator workbench (`engine::discover`: a specification, the engine's verdict on a generator at every size, and an archive of the cheapest verified generator in every cell of two measures, sealed); a program's history, attributed (`engine::versions`: versions in a sealed chain, each with who made it and how, proposals reviewed change by change, and each line and gate in the hand that wrote it, which a job's receipt can bind); and the CPU energy the kernel counts for one process (`engine::energy`). It builds for `wasm32-unknown-unknown`, where it runs on one thread and claims no time |
| `quantum_engine` | **the quantum job service's engine, for every host**: a job (an OpenQASM program and what is asked of it: shots, Pauli expectation values, amplitudes, noise) planned onto the strongest method it admits, with the reason and the memory and work it will take; run; answered in the service's JSON with its accuracy class; and sealed over the request and the result in their canonical JSON (`engine::job`, `engine::receipt`), so a result from the service, the command line or a browser tab checks the same way. Also a program's circuit drawn as text (`engine::draw`); the specification-and-evaluator workbench (`engine::discover`: a specification, the engine's verdict on a generator at every size, and an archive of the cheapest verified generator in every cell of two measures, sealed); a program's history, attributed (`engine::versions`: versions in a sealed chain, each with who made it and how, proposals reviewed change by change, and each line and gate in the hand that wrote it, which a job's receipt can bind); the person's rules for proposals (`engine::locks`: held blocks, the gates allowed, the qubit declarations, what to avoid and a region, checked change by change); and the CPU energy the kernel counts for one process (`engine::energy`). It builds for `wasm32-unknown-unknown`, where it runs on one thread and claims no time |
| `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector |
| `quantum_mps` | tensor-network (matrix-product-state) backend |
| `quantum_pauli` | sparse-Pauli / Heisenberg observable propagation over the dyadic gate set (up to 64 qubits) |

View file

@ -0,0 +1,322 @@
//! The person's rules for what a proposal may change: blocks of the program
//! held as they are, the gates it may use, its qubit declarations, gates and
//! text to avoid, and the region a rewrite is asked for. The engine checks a
//! proposal change by change ([`check`]). A change that breaks a rule is named
//! with the rule and is not offered, so a model or an outside agent proposes
//! inside the person's rules, and the person never has to catch a breach.
//!
//! - **Held blocks** are marked in the program itself, so they travel with it
//! to a model, an agent and a file: the lines from a `// @lock` comment to
//! the next `// @unlock` (or to the end), the markers included.
//! - **The other rules** are a [`Locks`] value: JSON beside the program for
//! the command line (`prog.locks.json`), the Studio's rules in a page.
//!
//! The rules bind proposals, not the person: what the person types is theirs
//! to change. Only what a change adds is judged, so a gate the program
//! already used is never a proposal's breach.
use super::{critique, versions};
use crate::qasm::{self, Inputs};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::BTreeSet;
/// The comment that starts a held block.
pub const LOCK: &str = "// @lock";
/// The comment that ends one.
pub const UNLOCK: &str = "// @unlock";
/// The rules beside the held blocks.
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Locks {
/// The only gates a change may add, by name; none means any.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gates: Option<Vec<String>>,
/// The qubit declarations (`qubit`, `qreg`) are held: no change may
/// touch one.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub qubits: bool,
/// Gates no change may add.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub avoid_gates: Vec<String>,
/// Text no line a change adds may hold, compared with each run of white
/// space read as one space.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub avoid_text: Vec<String>,
/// The base program's lines a rewrite is asked for, from 1, both ends
/// included: every change must lie within them.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<[usize; 2]>,
}
impl Locks {
/// The rules read from their JSON.
pub fn from_json(v: &Value) -> Result<Locks, String> {
let l: Locks = serde_json::from_value(v.clone()).map_err(|e| format!("not the rules: {e}"))?;
if let Some([a, b]) = l.region
&& (a == 0 || b < a)
{
return Err(format!("region [{a}, {b}]: lines count from 1, the first no later than the last"));
}
Ok(l)
}
fn is_empty(&self) -> bool {
*self == Locks::default()
}
}
/// The held blocks of `src`: each from its `// @lock` line to its
/// `// @unlock` line (or the last line), from 1, both ends included.
pub fn held(src: &str) -> Vec<[usize; 2]> {
let mut out = Vec::new();
let mut open = None;
let n = src.split('\n').count();
for (i, line) in src.split('\n').enumerate() {
let t = line.trim();
if open.is_none() && t.starts_with(LOCK) {
open = Some(i + 1);
} else if let (Some(a), true) = (open, t.starts_with(UNLOCK)) {
out.push([a, i + 1]);
open = None;
}
}
if let Some(a) = open {
out.push([a, n]);
}
out
}
fn squash(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn declares_qubits(line: &str) -> bool {
let t = line.trim_start();
["qubit[", "qubit ", "qreg "].iter().any(|k| t.starts_with(k))
}
fn names(list: &[String]) -> BTreeSet<String> {
list.iter().map(|g| g.trim().to_ascii_lowercase()).filter(|g| !g.is_empty()).collect()
}
/// Each change `proposed` makes to `base` ([`versions::changes`]) and the
/// rules it breaks, a generator's gates read at `inputs`:
/// `{ok, changes: [{index, line, breaches: [{rule, why}]}], allowed, held,
/// unread}`, `allowed` being the changes that break none and `unread` why the
/// proposal does not read, when a gate rule needed it to.
pub fn check(base: &str, proposed: &str, locks: &Locks, inputs: &Inputs) -> Value {
let changes = versions::changes(base, proposed);
let spans = versions::spans(base, proposed);
let blocks = held(base);
let mut breaches: Vec<Vec<Value>> = vec![Vec::new(); changes.len()];
let in_block = |l: usize| blocks.iter().find(|[a, b]| (*a..=*b).contains(&l)).copied();
for (i, c) in changes.iter().enumerate() {
let first = c.line;
let last = c.line + c.removed.len().saturating_sub(1);
// Held blocks: a line removed or replaced inside one, or one added
// between two of its lines.
let hit = if c.removed.is_empty() { blocks.iter().find(|[a, b]| first > *a && first <= *b).copied() } else { (first..=last).find_map(in_block) };
if let Some([a, b]) = hit {
breaches[i].push(json!({ "rule": "held", "why": format!("lines {a} to {b} are held ({LOCK} … {UNLOCK})") }));
}
if let Some([a, b]) = locks.region {
let outside = if c.removed.is_empty() { first < a || first > b + 1 } else { first < a || last > b };
if outside {
breaches[i].push(json!({ "rule": "region", "why": format!("it changes the program outside lines {a} to {b}, the region asked for") }));
}
}
if locks.qubits && c.removed.iter().chain(&c.added).any(|l| declares_qubits(l)) {
breaches[i].push(json!({ "rule": "qubits", "why": "the qubit declarations are held" }));
}
for t in &locks.avoid_text {
let t = squash(t);
if !t.is_empty() && c.added.iter().any(|l| squash(l).contains(&t)) {
breaches[i].push(json!({ "rule": "avoid", "why": format!("it adds `{t}`, which is to be avoided") }));
}
}
}
// Gates: each one a change adds, by the line it is on.
let allowed = locks.gates.as_deref().map(names);
let avoid = names(&locks.avoid_gates);
let mut error = None;
if allowed.is_some() || !avoid.is_empty() {
match qasm::parse_lines_with(proposed, inputs) {
Ok((p, op_lines)) => {
for (op, l) in p.ops.iter().zip(op_lines) {
let Some(i) = spans.iter().position(|&(_, _, from, to)| l > from && l <= to) else { continue };
let name = critique::kind_name(op.kind);
let (rule, why) = if avoid.contains(name) {
("avoid", format!("it adds `{name}`, which is to be avoided"))
} else if let Some(a) = allowed.as_ref().filter(|a| !a.contains(name)) {
("gates", format!("it adds `{name}`, and only {} may be used", a.iter().cloned().collect::<Vec<_>>().join(", ")))
} else {
continue;
};
let b = json!({ "rule": rule, "why": why });
if !breaches[i].contains(&b) {
breaches[i].push(b);
}
}
}
Err(e) => {
let why = format!("the proposal does not read, so its gates cannot be checked: {e}");
for b in breaches.iter_mut() {
b.push(json!({ "rule": "reads", "why": why }));
}
error = Some(e);
}
}
}
let allowed_changes: Vec<usize> = breaches.iter().enumerate().filter(|(_, b)| b.is_empty()).map(|(i, _)| i).collect();
json!({
"ok": allowed_changes.len() == changes.len(),
"changes": changes.iter().zip(&breaches).enumerate().map(|(i, (c, b))| json!({ "index": i, "line": c.line, "breaches": b })).collect::<Vec<_>>(),
"allowed": allowed_changes,
"held": blocks,
// Not `error`: a page's worker reads an answer with an `error` as a failed call.
"unread": error,
})
}
/// The rules as a model is told them, one sentence each, for `base`.
pub fn brief(base: &str, locks: &Locks) -> Vec<String> {
let mut out = Vec::new();
let blocks = held(base);
if !blocks.is_empty() {
let at = blocks.iter().map(|[a, b]| format!("{a} to {b}")).collect::<Vec<_>>().join(", ");
out.push(format!("Leave the lines between `{LOCK}` and `{UNLOCK}` exactly as they are, the comments included (lines {at})."));
}
if let Some([a, b]) = locks.region {
out.push(format!("Change only lines {a} to {b}; every other line stays as it is."));
}
if let Some(g) = &locks.gates {
out.push(format!("Use only these gates in what you add: {}.", names(g).into_iter().collect::<Vec<_>>().join(", ")));
}
if locks.qubits {
out.push("Keep the qubit declarations as they are.".into());
}
if !locks.avoid_gates.is_empty() {
out.push(format!("Add none of these gates: {}.", names(&locks.avoid_gates).into_iter().collect::<Vec<_>>().join(", ")));
}
for t in &locks.avoid_text {
out.push(format!("Add no line holding `{}`.", squash(t)));
}
out
}
/// The gates of `src` (read at `inputs`) on a held line, by their place in
/// the program (as [`crate::engine::draw::layout`] numbers them).
pub fn held_ops(src: &str, inputs: &Inputs) -> Result<Vec<usize>, String> {
let blocks = held(src);
let (_, lines) = qasm::parse_lines_with(src, inputs)?;
Ok(lines.iter().enumerate().filter(|(_, l)| blocks.iter().any(|[a, b]| (*a..=*b).contains(*l))).map(|(i, _)| i).collect())
}
/// The rules as a page or an app calls them, by name with JSON arguments:
/// `check` (`base`, `proposed`, `locks`, `inputs`), `held` (`source`, and
/// `inputs` for its gates) and `brief` (`base`, `locks`).
pub fn call(op: &str, args: &Value) -> Result<Value, String> {
let s = |k: &str| args.get(k).and_then(Value::as_str).ok_or_else(|| format!("{op}: give \"{k}\""));
let locks = || match args.get("locks") {
None | Some(Value::Null) => Ok(Locks::default()),
Some(v) => Locks::from_json(v),
};
let inputs = || -> Result<Inputs, String> {
match args.get("inputs") {
None | Some(Value::Null) => Ok(Inputs::new()),
Some(v) => serde_json::from_value(v.clone()).map_err(|e| format!("{op}: inputs: {e}")),
}
};
match op {
"check" => Ok(check(s("base")?, s("proposed")?, &locks()?, &inputs()?)),
"held" => {
let src = s("source")?;
Ok(json!({ "held": held(src), "ops": held_ops(src, &inputs()?).ok() }))
}
"brief" => {
let l = locks()?;
Ok(json!({ "rules": brief(s("base")?, &l), "none": l.is_empty() && held(s("base")?).is_empty() }))
}
_ => Err(format!("{op}: no such call on the rules")),
}
}
#[cfg(test)]
mod tests {
use super::*;
const HELD: &str = "OPENQASM 3.0;\nqubit[3] q;\n// @lock\nh q[0];\ncx q[0], q[1];\n// @unlock\nx q[2];\n";
fn breaches(v: &Value) -> Vec<Vec<String>> {
v["changes"].as_array().unwrap().iter().map(|c| c["breaches"].as_array().unwrap().iter().map(|b| b["rule"].as_str().unwrap().to_string()).collect()).collect()
}
#[test]
fn held_blocks_are_read_from_their_markers() {
assert_eq!(held(HELD), [[3, 6]]);
assert_eq!(held("// @lock\nh q[0];"), [[1, 2]], "an open block holds to the end");
assert!(held("h q[0];\n// @unlock\n").is_empty(), "an unlock alone holds nothing");
}
#[test]
fn a_change_inside_a_held_block_breaks_it_and_one_beside_it_does_not() {
let none = Locks::default();
let inside = HELD.replace("cx q[0], q[1];", "cz q[0], q[1];");
assert_eq!(breaches(&check(HELD, &inside, &none, &Inputs::new())), [vec!["held".to_string()]]);
let between = HELD.replace("h q[0];\n", "h q[0];\nz q[0];\n");
assert_eq!(breaches(&check(HELD, &between, &none, &Inputs::new())), [vec!["held".to_string()]]);
let before = HELD.replace("// @lock\n", "y q[2];\n// @lock\n");
let after = HELD.replace("// @unlock\n", "// @unlock\ny q[2];\n");
for p in [before, after] {
let v = check(HELD, &p, &none, &Inputs::new());
assert_eq!(v["ok"], true, "{v}");
}
let unmarked = HELD.replace("// @unlock\n", "");
assert!(breaches(&check(HELD, &unmarked, &none, &Inputs::new()))[0].contains(&"held".to_string()), "removing a marker is a change to the block");
}
#[test]
fn each_rule_names_the_change_that_breaks_it() {
let base = "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\nh q[0];\ncx q[0], q[1];\n";
let locks = Locks { gates: Some(vec!["h".into(), "cx".into(), "x".into()]), qubits: true, avoid_gates: vec!["cz".into()], avoid_text: vec!["x q[1]".into()], region: Some([4, 5]) };
// Four changes: the qubits (outside the region too), an h kept, a cz, an x on q[1].
let proposed = "OPENQASM 3.0;\nqubit[3] q;\nt q[0];\nh q[0];\ncz q[0], q[1];\nx q[1];\n";
let v = check(base, proposed, &locks, &Inputs::new());
let b = breaches(&v);
assert_eq!(b.len(), 2, "{v}");
assert_eq!(b[0], ["region", "qubits"], "{v}");
assert!(b[1].contains(&"avoid".to_string()) && !b[1].contains(&"gates".to_string()), "{v}");
assert_eq!(v["allowed"], json!([]));
// The t already there is never the proposal's breach.
let only_h = "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\nh q[0];\ncx q[0], q[1];\nh q[1];\n";
assert_eq!(check(base, only_h, &locks, &Inputs::new())["ok"], true);
let with_s = format!("{base}s q[1];\n");
let v = check(base, &with_s, &locks, &Inputs::new());
assert!(v["changes"][0]["breaches"][0]["why"].as_str().unwrap().contains("only cx, h, x may be used"), "{v}");
}
#[test]
fn a_generator_s_gates_are_checked_at_its_inputs() {
let 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";
let proposed = chain.replace(" cx q[i], q[i + 1];", " cz q[i], q[i + 1];");
let locks = Locks { gates: Some(vec!["h".into(), "cx".into()]), ..Locks::default() };
let at3 = Inputs::from([("n".to_string(), 3)]);
assert_eq!(breaches(&check(chain, &proposed, &locks, &at3)), [vec!["gates".to_string()]]);
let v = check(chain, &proposed, &locks, &Inputs::new());
assert_eq!(breaches(&v), [vec!["reads".to_string()]], "with no value for n its gates cannot be read: {v}");
}
#[test]
fn the_rules_are_briefed_and_called_by_name() {
let locks = Locks { gates: Some(vec!["H".into(), "cx".into()]), qubits: true, region: Some([2, 3]), ..Locks::default() };
let b = brief(HELD, &locks);
assert!(b[0].contains("lines 3 to 6") && b.iter().any(|s| s == "Use only these gates in what you add: cx, h.") && b.iter().any(|s| s.contains("lines 2 to 3")), "{b:?}");
let v = call("check", &json!({ "base": HELD, "proposed": HELD.replace("x q[2]", "y q[2]"), "locks": { "avoid_gates": ["y"] } })).unwrap();
assert_eq!(v["changes"][0]["breaches"][0]["rule"], "avoid", "{v}");
let h = call("held", &json!({ "source": HELD })).unwrap();
assert_eq!((h["held"].clone(), h["ops"].clone()), (json!([[3, 6]]), json!([0, 1])), "{h}");
assert!(call("check", &json!({ "base": "", "proposed": "", "locks": { "region": [3, 1] } })).unwrap_err().contains("region"));
assert_eq!(call("brief", &json!({ "base": "h q[0];" })).unwrap()["none"], true);
}
}

View file

@ -35,6 +35,10 @@
//! workbench. A person writes what a family of circuits must do and what it
//! costs, proposers write generators, the engine checks each at every size,
//! and an archive keeps the cheapest verified generator of every kind.
//! - [`locks`] (with `quantum_receipt`): the person's rules for what a
//! proposal may change (blocks held between `// @lock` and `// @unlock`,
//! the gates allowed, the qubit declarations, gates and text to avoid, the
//! region asked for), checked change by change.
//! - [`versions`] (with `quantum_receipt`): a program's history, attributed.
//! Every version names its parents, who made it (the person, a model, an
//! outside agent, the engine) and how (an edit, a proposal reviewed change
@ -56,6 +60,8 @@ pub mod equiv;
#[cfg(feature = "quantum_receipt")]
pub mod job;
#[cfg(feature = "quantum_receipt")]
pub mod locks;
#[cfg(feature = "quantum_receipt")]
pub mod receipt;
pub mod run;
pub mod shortcuts;

View file

@ -126,6 +126,10 @@ pub struct Review {
/// The ones the person accepted, by index, in order; the rest were
/// rejected.
pub accepted: Vec<usize>,
/// The ones the person's rules blocked ([`crate::engine::locks`]), by
/// index, in order: never offered, so never accepted.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked: Vec<usize>,
}
/// One version of the program.
@ -357,6 +361,13 @@ fn gaps(matches: &[(usize, usize)], na: usize, nb: usize) -> Vec<(usize, usize,
out
}
/// Where each of [`changes`]`(base, proposed)` lies: `(base_from, base_to,
/// proposed_from, proposed_to)`, lines from 0, ends excluded.
pub(crate) fn spans(base: &str, proposed: &str) -> Vec<(usize, usize, usize, usize)> {
let (a, b) = (lines(base), lines(proposed));
gaps(&matches(&a, &b), a.len(), b.len())
}
/// The changes that make `proposed` from `base`, line by line, in order.
pub fn changes(base: &str, proposed: &str) -> Vec<Change> {
let (a, b) = (lines(base), lines(proposed));
@ -520,6 +531,17 @@ impl History {
/// too, its source its parent's.
#[allow(clippy::too_many_arguments)]
pub fn propose(&mut self, parent: &str, proposed: &str, accepted: &[usize], by: By, note: Option<&str>, at: u64, key: Option<&SigningKey>) -> Result<&Version, String> {
self.propose_with(parent, proposed, accepted, &[], by, note, at, key)
}
/// [`History::propose`], with the changes the person's rules `blocked`
/// recorded beside the ones accepted. A change both blocked and accepted
/// is refused.
#[allow(clippy::too_many_arguments)]
pub fn propose_with(&mut self, parent: &str, proposed: &str, accepted: &[usize], blocked: &[usize], by: By, note: Option<&str>, at: u64, key: Option<&SigningKey>) -> Result<&Version, String> {
if let Some(k) = accepted.iter().find(|k| blocked.contains(k)) {
return Err(format!("change {k} is blocked by the rules, so it cannot be accepted"));
}
let p = self.index(parent)?;
let base = self.versions[p].source.clone();
let offered = changes(&base, proposed).len();
@ -530,7 +552,13 @@ impl History {
let mut accepted = accepted.to_vec();
accepted.sort_unstable();
accepted.dedup();
let review = Review { proposed: proposed.to_string(), offered, accepted };
let mut blocked = blocked.to_vec();
blocked.sort_unstable();
blocked.dedup();
if let Some(k) = blocked.iter().find(|&&k| k >= offered) {
return Err(format!("blocked change {k}: the proposal makes {offered}"));
}
let review = Review { proposed: proposed.to_string(), offered, accepted, blocked };
self.push(vec![p], source, by, How::Proposal, note, Some(review), at, key)
}
@ -583,6 +611,9 @@ impl History {
if changes(base, &r.proposed).len() != r.offered || apply(base, &r.proposed, &r.accepted).as_deref() != Ok(v.source.as_str()) {
problems.push(format!("{}: its review does not give its source", v.id));
}
if r.accepted.iter().any(|k| r.blocked.contains(k)) || r.blocked.iter().any(|&k| k >= r.offered) {
problems.push(format!("{}: its review accepts a change the rules blocked", v.id));
}
} else if v.review.is_some() != (v.how == How::Proposal) {
problems.push(format!("{}: only a proposal has a review", v.id));
}
@ -768,7 +799,7 @@ impl History {
e["note"] = json!(n);
}
if let Some(r) = &v.review {
e["review"] = json!({ "offered": r.offered, "accepted": r.accepted.len() });
e["review"] = json!({ "offered": r.offered, "accepted": r.accepted.len(), "blocked": r.blocked.len() });
}
e
})
@ -812,7 +843,8 @@ pub fn call(op: &str, args: &Value, key: Option<&SigningKey>) -> Result<Value, S
}
"propose" => {
let mut h = history()?;
let id = h.propose(need("parent")?, need("proposed")?, &accepted()?, by()?, s("note"), at, key)?.id.clone();
let blocked: Vec<usize> = serde_json::from_value(args.get("blocked").cloned().unwrap_or(json!([]))).map_err(|e| format!("{op}: blocked: {e}"))?;
let id = h.propose_with(need("parent")?, need("proposed")?, &accepted()?, &blocked, by()?, s("note"), at, key)?.id.clone();
Ok(recorded(&h, id))
}
"merge" => {
@ -826,7 +858,8 @@ pub fn call(op: &str, args: &Value, key: Option<&SigningKey>) -> Result<Value, S
let (h, id, ins) = (history()?, need("version")?, inputs()?);
// A draft that does not read still has its lines' authors.
let (authorship, gates) = (h.authorship(id, &ins), h.gate_hands(id, &ins));
Ok(json!({ "version": id, "lines": h.blame(id)?, "authorship": authorship.as_ref().ok(), "gates": gates.as_ref().ok(), "error": authorship.err() }))
// `unread`, not `error`: a page's worker reads an answer with an `error` as a failed call.
Ok(json!({ "version": id, "lines": h.blame(id)?, "authorship": authorship.as_ref().ok(), "gates": gates.as_ref().ok(), "unread": authorship.err() }))
}
"attest" => history()?.attest(need("version")?, &inputs()?),
"log" => {
@ -866,7 +899,7 @@ mod tests {
// A draft that does not read keeps its lines' authors.
let r = call("commit", &json!({ "history": r["history"], "parent": "v2", "source": format!("{BELL}x q[\n"), "by": me, "at": 4 }), Some(&k)).unwrap();
let b = call("blame", &json!({ "history": r["history"], "version": "v3" }), None).unwrap();
assert!(b["authorship"].is_null() && b["error"].is_string() && b["lines"].as_array().unwrap().len() == 6, "{b}");
assert!(b["authorship"].is_null() && b["unread"].is_string() && b.get("error").is_none() && b["lines"].as_array().unwrap().len() == 6, "{b}");
assert!(call("nonsense", &json!({}), None).unwrap_err().contains("no such call"));
}
@ -987,6 +1020,25 @@ mod tests {
assert!(h.blame("v2").unwrap().iter().all(|l| l.hand == Hand::Origin && l.version == "v0"), "{:?}", h.blame("v2"));
}
#[test]
fn a_change_the_rules_blocked_is_recorded_and_never_accepted() {
let k = key();
let mut h = History::start(BELL, person(), None, 1, Some(&k)).unwrap();
let proposed = BELL.replace("h q[0];", "h q[1];") + "x q[1];\n";
assert!(h.propose_with("v0", &proposed, &[0], &[0], By::new(Kind::Assistant, None), None, 2, Some(&k)).unwrap_err().contains("blocked"));
h.propose_with("v0", &proposed, &[1], &[0], By::new(Kind::Assistant, None), None, 2, Some(&k)).unwrap();
assert_eq!(h.versions[1].review.as_ref().unwrap().blocked, [0]);
assert_eq!(h.log()[1]["review"]["blocked"], 1);
assert!(h.verify().ok);
let mut t = h.clone();
t.versions[1].review.as_mut().unwrap().accepted = vec![0, 1];
assert!(t.verify().problems.iter().any(|p| p.contains("rules blocked")));
// A review with nothing blocked has the digest it had before the field.
let mut h2 = History::start(BELL, person(), None, 1, None).unwrap();
h2.propose("v0", &proposed, &[1], By::new(Kind::Assistant, None), None, 2, None).unwrap();
assert!(!serde_json::to_string(&h2.versions[1]).unwrap().contains("blocked"));
}
#[test]
fn a_merge_keeps_each_branch_s_authors() {
let mut h = History::start(BELL, person(), None, 1, None).unwrap();

View file

@ -8,7 +8,7 @@
use ed25519_dalek::SigningKey;
use serde_json::{Value, json};
use wai::engine::discover::{Archive, Budget, Evaluator, SearchOptions, Spec, search};
use wai::engine::{catalog, critique, draw, equiv, job, shortcuts, versions};
use wai::engine::{catalog, critique, draw, equiv, job, locks, shortcuts, versions};
use wasm_bindgen::prelude::*;
/// What a result run in a tab says of its energy.
@ -255,6 +255,17 @@ pub fn engine_critique_with(src: &str, inputs: &str) -> String {
inputs_of(inputs).map(|ins| critique::critique_with(src, &ins).to_string()).unwrap_or_else(|e| json!({ "ok": false, "error": e }).to_string())
}
/// `op` on the person's rules for proposals ([`locks::call`]), its arguments
/// as JSON: the answer, or `{error}`.
#[wasm_bindgen]
pub fn engine_locks(op: &str, args: &str) -> String {
serde_json::from_str::<Value>(args)
.map_err(|e| format!("{op}: the arguments are not JSON: {e}"))
.and_then(|a| locks::call(op, &a))
.map(|v| v.to_string())
.unwrap_or_else(error_json)
}
/// A generator written out at `inputs` (JSON, as `{"n": 5}`) as plain
/// OpenQASM: `{text}`, or `{error}`.
#[wasm_bindgen]
@ -314,6 +325,20 @@ mod tests {
assert!(e.history("nonsense", "{}").contains("no such call"));
}
/// The rules in the page: a change in a held block, and one adding a gate
/// the rules do not allow, are named with their rule.
#[test]
fn the_rules_check_a_proposal_in_the_page() {
let base = "OPENQASM 3.0;\nqubit[2] q;\n// @lock\nh q[0];\n// @unlock\ncx q[0], q[1];\n";
let proposed = base.replace("h q[0];", "h q[1];") + "cz q[0], q[1];\n";
let v: Value = serde_json::from_str(&engine_locks("check", &json!({ "base": base, "proposed": proposed, "locks": { "gates": ["h", "cx"] } }).to_string())).unwrap();
let rules: Vec<&str> = v["changes"].as_array().unwrap().iter().map(|c| c["breaches"][0]["rule"].as_str().unwrap()).collect();
assert_eq!(rules, ["held", "gates"], "{v}");
let h: Value = serde_json::from_str(&engine_locks("held", &json!({ "source": base }).to_string())).unwrap();
assert_eq!(h["ops"], json!([0]), "{h}");
assert!(engine_locks("check", "not json").contains("not JSON"));
}
fn bell(extra: &str) -> String {
format!("{{\"qasm\": \"OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];\"{extra}}}")
}