# Technical Documentation: QuipNetwork/xq-rs

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/QuipNetwork/xq-rs)

### Overview (`README.md`)

# XQuad

> **Early public release.** The instruction set, binary format, and public API
> may still change before v1.0. Production use is not recommended yet.

XQuad is a hardware-agnostic toolchain for expressing and running quadratic
optimization problems (QUBO / Ising / integer) across quantum annealers and
classical solvers. It ships as three Rust crates plus five Python distributions
built around a single virtual-machine specification.

Think of it as **LLVM for quadratic models** -- a common intermediate
representation that lets you write a problem once and retarget it to any
supported backend.

- **Spec-first.** Every behaviour is nailed down in [`spec/xqvm/SPEC.md`](spec/xqvm/SPEC.md)
  and cross-implementation parity is mechanically enforced: every committed
  conformance vector runs on both the Rust production VM (`xqvm`) and the
  Python reference VM (`xqvm_py`) in CI; disagreement fails the build.
- **Embeddable.** The Rust core supports `no_std + alloc`, so the same VM
  runs inside WASM runtimes, Substrate pallets, and native binaries.
- **Interactive.** The Python umbrella (`xquad`) is REPL and Jupyter-friendly:
  load a program once, run it with different calldata, inspect outputs by
  slot. Low-level FFI lives in `xqffi`; user-facing `Program` / `Session` /
  `RunResult` live in `xquad.program`.

## Install

### Rust (from crates.io)

```sh
cargo install xqcli           # gives you the `xquad` binary
```

### Python (from PyPI)

```sh
pip install xquad             # umbrella -- full pipeline
# or pick individual pieces:
pip install xqffi xqcp xqsa   # FFI bindings + DSL + solvers
```

Prebuilt wheels are available for Linux x86_64 and aarch64 (CPython >= 3.13). On macOS and Windows, pip builds the `xqffi` Rust extension from source -- this requires a [Rust toolchain](https://rustup.rs/) (>= 1.85).

### GPU/QPU support

The base install includes CPU simulated annealing only. To use GPU or quantum hardware solvers, install the corresponding extra:

| Solver | Install | Prerequisite |
|--------|---------|-------------|
| NVIDIA CUDA GPU (`cuda-gpu`) | `pip install xquad[cuda]` | CUDA 12.x driver + NVIDIA GPU |
| Apple Metal GPU (`metal-gpu`) | `pip install xquad[metal]` | Apple Silicon Mac with a Metal device |
| D-Wave Advantage QPU (`dwave-qpu`) | `pip install xquad[dwave]` | D-Wave Leap account + API token |
| Quip Network solver (`quip`) | `pip install xquad[quip]` | `QUIP_RPC_URL` + a configured signer |

Extras are composable: `pip install xquad[cuda,dwave]`. See [`xqsa/README.md`](xqsa/README.md#install) for driver prerequisites and per-solver quick-starts.

For a guided walkthrough that installs XQuad, verifies it, and runs a
complete problem end to end, see
[Install XQuad](docs/book/src/start/README.md) in the book.

## Package map

Three Rust crates on **crates.io**:

| Crate | Binary | Description |
|---|---|---|
| [`xqvm`](xqvm/) | none | X-Quadratic Virtual Machine interpreter, bytecode codec, opcode table (`no_std + alloc`) |
| [`xqasm`](xqasm/) | none | `.xqasm` text-format assembler |
| [`xqcli`](xqcli/) | `xquad` | Unified CLI -- `xquad asm` / `dism` / `run` / `verify` |

Five Python distributions on **PyPI**:

| Package | Description |
|---|---|
| [`xqffi`](xqffi/) | PyO3 FFI bindings for `xqvm` + `xqasm` (`xqffi.vm`, `xqffi.asm`, `xqffi.verifier`) |
| [`xqvm_py`](xqvm_py/) | Pure-Python reference VM (conformance oracle) |
| [`xqcp`](xqcp/) | Constraint-programming DSL that compiles to `.xqasm` |
| [`xqsa`](xqsa/) | Solver adapters -- CPU/GPU annealers, D-Wave QPU, Quip network |
| [`xquad`](xquad/) | Umbrella meta-package with interactive `Program` / `Session` / `RunResult` API |

The one Rust package without a crates.io artefact is the `xqffi` pyo3
crate itself -- it ships as the `xqffi` PyPI wheel rather than a reusable
Rust library.

## Quick start

### A minimal program (CLI)

```asm
; add.xqasm -- push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
```

```sh
xquad asm add.xqasm -o add.xqb && xquad run add.xqb
```

### End-to-end (Python, via the umbrella)

The recommended user-facing surface is `xquad.program.Program` +
`Session` + `RunResult`. Load once, run many times with different
calldata, inspect outputs by slot:

```python
from xquad.program import Program

program = Program.from_source("""
PUSH 0
INPUT r0
PUSH 1
INPUT r1
LOAD r0
LOAD r1
ADD
STOW r2
PUSH 0
OUTPUT r2
HALT
""")

session = program.session(output_slots=1)
session.set_calldata([40, 2])
result = session.run()
assert dict(result.outputs) == {0: 42}
```

`Program.load(bytes)` takes wire-format bytecode; `Session.run()`
returns a `RunResult` with dict-keyed outputs (unset slots present as
`None`), residual stack, and step count. See
[Running Programs](docs/book/src/running/README.md) for the full tour.

#### Low-level / conformance surface

For conformance harnesses and one-shot execution, the raw
`xqffi.vm.Vm` is still available directly:

```python
from xqffi.asm import assemble_source
from xqffi.vm import Vm

bytecode = assemble_source(src)
v = Vm()
v.set_calldata([40, 2])
v.set_output_slots(1)
v.run(bytecode)
assert v.outputs() == [42]
```

The `xquad.vm.VM` class offers a middle tier -- a unified wrapper with
backend dispatch (`VMBackend.RUST` / `VMBackend.PYTHON`) that accepts
`.xqasm` source and normalises types across the two interpreters.

### End-to-end worked example

[`examples/tsp/`](examples/tsp/) shows a full Travelling Salesman Problem
driven from the `xqcp` DSL through the VM and `xqsa` solver, runnable on
either the Python reference VM or the Rust VM:

```sh
uv run python examples/tsp/runner.py --seed 42
uv run python examples/maxcut/runner.py --seed 42 --interpreter rust

# Run every example on both interpreters, check each finds a valid solution:
make example-smoke
```

## Architecture

XQuad is a stack-based interpreter with a 256-slot register file. Registers
hold typed values -- integers, integer vectors, QUBO/Ising models
(`XqmxModel`), and candidate solutions (`XqmxSample`). A dedicated loop stack
drives `RANGE`/`ITER` iteration.

The opcode table is declared once in `xqvm/src/bytecode/types/table.rs` via
the `opcodes!` x-macro; `conformance/opcodes.yaml` is the machine-readable
mirror that all three representations (YAML, Rust macro, `xqvm_py.opcodes`)
are checked against at build time and in CI
(`scripts/check-opcode-parity.py`).

The binary format (`.xqb`) opens with a fixed 15-byte XQBC header --
magic, version, calldata/output-slot counts, code length, and a CRC-32
checksum of the payload -- followed by the instruction stream: an
opcode byte followed by its operands in big-endian byte order.

See [`conformance/opcodes.md`](conformance/opcodes.md) for
instruction-by-instruction semantics and
[`spec/xqvm/SPEC.md`](spec/xqvm/SPEC.md) for the normative spec.

## Development

### Prerequisites

```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh   # stable Rust
make deps                                                          # dev tools
```

### Local setup

```sh
make xquad
```

This syncs the Python workspace (`xqvm_py`, `xqcp`, `xqsa`, `xqffi`,
`xquad`) into `.venv/`, builds the `xqffi` pyo3 extension via maturin,
puts the repo root on `sys.path` so scripts anywhere in the repo can
`import xqcp` / `xqsa` / `xqvm_py` / `xqffi` / `xquad` naturally, and
installs the `xquad` CLI binary under `~/.cargo/bin/`.

Open a REPL with everything ready:

```sh
make repl
```

### CI-equivalent locally

```sh
make preflight    # what CI enforces, split into preflight-rs / -py / -parity / -docs / -policy
make conformance  # cross-impl parity suite (Rust + Python)
```

### Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow, commit
conventions, and DCO sign-off requirements. Agent guidelines for AI coding
assistants are in [AGENTS.md](AGENTS.md); cutting a release is documented in
[RELEASING.md](RELEASING.md).

## License

Licensed under the [GNU Affero General Public License v3.0 or later](LICENSE).

---

### Overview (`conformance/README.md`)

# XQVM Conformance Harness

Mechanical cross-implementation check that the Rust production VM
([`xqvm`](../xqvm/)) and the Python reference VM
([`xqvm_py`](../xqvm_py/)) agree on every observable behaviour defined
by [`spec/xqvm/SPEC.md`](../spec/xqvm/SPEC.md).

The harness is a Rust test crate (`xquad-conformance`). Each vector
becomes two `#[test]` functions -- one per runtime -- generated at
`cargo build` time by [`build.rs`](build.rs). CI runs both through the
`verify:parity` job's `make -k check-parity`, which keeps running
every target after one fails so a Python-side regression cannot mask a
Rust-side pass (or vice versa).

## Layout

```
conformance/
├── opcodes.yaml          single machine-readable source for the 93-opcode table
├── vectors/
│   ├── arithmetic/<name>/
│   │   ├── program.xqasm       canonical assembly source
│   │   ├── inputs.json         {"calldata": [i64, ...], "output_slots": N}
│   │   └── expected.json       {"outputs": [...], "final_stack": [...]} or {"error": FAULT}
│   ├── control-flow/<name>/
│   ├── energy/<name>/
│   ├── constraints/<name>/
│   ├── vector-ops/<name>/
│   └── xqmx-grid/<name>/
├── src/                  Rust library + manual CLI binary
├── tests/rust.rs         generated in-process Rust runner tests
└── tests/python.rs       generated subprocess Python runner tests
```

## File format

### `program.xqasm`

Canonical human-readable source. The assembler is authoritative: whatever
`xqasm::assemble_source()` produces becomes the canonical bytecode.
Prefer explicit `PUSH1`/`PUSH2`/... forms when the constant width matters
for the vector; use the `PUSH` sugar where it doesn't.

The harness assembles `program.xqasm` in-process on every run; encoding
correctness is owned by the `xqasm` crate's own test suite, so no
pre-assembled bytecode artifact is committed.

### `inputs.json`

```json
{
  "calldata": [6, 7],
  "output_slots": 16,
  "step_limit": 50,
  "memory_limit": 1048576
}
```

`output_slots` is optional (defaults to 16, matching `xquad run`). The
`calldata` array is exposed to the program in slot order: slot 0 holds
the first value, slot 1 the second, and so on. `step_limit` is optional
and defaults to 10000000, matching `xquad run`; the harness resolves the
default itself and passes it to both runners, because the two
implementations disagree on what an omitted budget means and a vector
that left it out used to run bounded on Rust and unbounded on Python.
Set it only for a vector that is specifically exercising the budget.

`memory_limit` is the same arrangement for the allocation budget: optional,
defaulting to 1073741824 (`xqvm::DEFAULT_MEMORY_LIMIT`), resolved once by
the harness and passed to both runners. It is what makes the
allocation-charge half of the fault-ordering rule below assertable, since
those faults are only reachable against a budget the charge exhausts.

### `expected.json`

A vector asserts **either** an outcome **or** a fault. Supplying both, or
neither, is rejected as a vector-authoring mistake rather than silently
preferring one half.

```json
{
  "outputs": [42],
  "final_stack": []
}
```

`outputs` is a sparse map, per the state model in `spec/xqvm/SPEC.md` --
each entry is either an `i64` (the value written by `OUTPUT`) or `null`
(the slot was reserved but explicitly zeroed by a peer entry). Trailing
unset slots are omitted entirely, so a program that writes slot 0 out of
16 reserved slots produces `[42]`, not `[42, null, ...]`.
Explicitly-written zeroes are preserved; only slots that `OUTPUT` never
touched disappear. `final_stack` is the residual stack at `HALT` (bottom
to top); an empty stack is typical.

A vector that expects the program to fault writes the fault's identity
instead:

```json
{
  "error": "DIVISION_BY_ZERO"
}
```

### Fault identities

Neither implementation's spelling can serve as the identity: Rust raises
enum variants, Python raises exception classes, and the two vocabularies
do not line up one-to-one. `expected.json` therefore names a third,
implementation-neutral vocabulary that both sides map onto.

Byte position is deliberately **not** part of the identity. Only the
fault itself is compared, so vectors do not turn brittle against
unrelated codegen changes. Position assertions can be added later if a
bug ever motivates them.

| `expected.json` | Rust `xqvm::Error` | Python `xqvm_py.errors` |
| --- | --- | --- |
| `STACK_UNDERFLOW` | `StackUnderflow` | `StackUnderflow` |
| `STACK_OVERFLOW` | `StackOverflow` | `StackOverflow` |
| `TYPE_MISMATCH` | `RegisterType`, `IncompatibleType` | `TypeMismatch`, `XQMXModeError` |
| `UNSET_REGISTER` | `UnsetRegister` | `RegisterNotFound` |
| `DIVISION_BY_ZERO` | `DivisionByZero` | `DivisionByZero` |
| `ARITHMETIC_OVERFLOW` | `ArithmeticOverflow` | `ArithmeticOverflow` |
| `INDEX_OUT_OF_BOUNDS` | `IndexOutOfBounds` | `IndexOutOfBounds` |
| `NO_ACTIVE_LOOP` | `NoActiveLoop` | `NoActiveLoop` |
| `UNMATCHED_LOOP` | `UnmatchedLoop` | `UnmatchedLoop` |
| `BAD_JUMP_TARGET` | `BadJumpTarget` | -- |
| `INVALID_LABEL` | `InvalidLabel` | `TargetNotFound` |
| `BAD_OPCODE` | `BadOpcode` | `InvalidOpcode` |
| `TRUNCATED_INSTRUCTION` | `TruncatedInstruction` | `TruncatedInstruction` |
| `CALL_DATA_INDEX` | `CallDataIndex` | `CallDataIndex` |
| `OUTPUT_INDEX` | `OutputIndex` | `OutputIndex` |
| `SIZE_MISMATCH` | `SizeMismatch` | `SizeMismatch` |
| `VEC_LENGTH_MISMATCH` | `VecLengthMismatch` | `VecLengthMismatch` |
| `STEP_LIMIT_EXCEEDED` | `StepLimitExceeded` | `StepLimitExceeded` |
| `MEMORY_LIMIT_EXCEEDED` | `MemoryLimitExceeded` | `MemoryLimitExceeded` |
| `INVALID_SHIFT` | `InvalidShift` | `InvalidShift` |
| `INVALID_GRID_DIMENSIONS` | `InvalidGridDimensions` | `InvalidGridDimensions` |
| `INVALID_INTEGER_K` | `InvalidIntegerK` | `InvalidIntegerK` |
| `SAMPLE_OUT_OF_DOMAIN` | `SampleOutOfDomain` | `SampleOutOfDomain` |
| `TRACE_FAILED` | `TraceFailed` | -- |
| `INVALID_ALLOCATION` | `InvalidAllocation` | `InvalidAllocation` |
| `LOOP_STACK_OVERFLOW` | `LoopStackOverflow` | `LoopStackOverflow` |

The Rust mapping is an exhaustive `match`, so adding a variant to
`xqvm::Error` without extending this table fails to compile rather than
silently degrading to an "unknown" fault. The Python mapping is by class
name and is necessarily partial: a `--` above means that implementation
has no distinct class for the fault yet, and a vector asserting it will
fail loudly on the Python runner until one exists.

The loop faults were that gap until QUI-1289: `xqvm_py` raised one
`LoopError` class for both `NO_ACTIVE_LOOP` and `UNMATCHED_LOOP`, which
the harness could not resolve to either identity without picking
arbitrarily, so it mapped neither. The class is now split into
`NoActiveLoop` and `UnmatchedLoop`, and each identity is pinned by its own
vector: `vectors/control-flow/next_outside_loop` and
`vectors/control-flow/range_empty_unmatched`.

### Fault ordering (QUI-1178)

`spec/xqvm/SPEC.md` fixes the order of work within one instruction: pops,
then the allocation charge, then validation. Where a register read once
preceded the pops in `xqvm_py`, the divergence was reachable two ways --
a short stack (which the verifier rejects) and an operand fault raised
ahead of the register read (which it admits). Only the second is
consensus-visible.

Of the 22 opcodes reordered, the second route reaches:

| Opcode(s) | Fault raised ahead of the register read | Pinned by |
|---|---|---|
| `RESIZE` | `InvalidGridDimensions`, unconditional | `vectors/xqmx-grid/resize_type_after_dimension_check` |
| `VECPUSH`, `SETLINE`, `ADDLINE`, `SETQUAD`, `ADDQUAD`, `EXCLUDE`, `IMPLIES`, `REDUCE` | `MemoryLimitExceeded`, only against a near-exhausted budget | `vectors/metering/vecpush_charge_before_type_check` pins `VECPUSH`; the other seven by `xqvm_py/tests/test_executor.py` alone |
| `ATLEAST` | `IndexOutOfBounds` on `k`, unconditional | `vectors/constraints/atleast_k_range_before_model_type` |
| `ATLEASTW` | `VecLengthMismatch`, unconditional | `vectors/constraints/atleastw_length_before_model_type` |
| `EQUALITY` | `VecLengthMismatch`, unconditional -- the vec lengths are compared before the model register is discriminated, and the charge follows both vec reads | `vectors/constraints/equality_length_mismatch_beats_model_type` |
| `VECGET`, `VECSET`, `GETLINE`, `GETQUAD`, `ROWFIND`, `COLFIND`, `ROWSUM`, `COLSUM` | none -- Rust type-checks immediately after its pops | short stack only, not verifier-clean |
| `ONEHOTR`, `ONEHOTC` | none, since QUI-1202 -- the charge is sized from a model-only peek on both VMs, so a sample sizes it at zero | `xqvm_py/tests/test_executor.py` -- `test_onehot_does_not_charge_for_a_sample`; a vector could express it now that `Inputs` carries `memory_limit` |

The `ONEHOTR`/`ONEHOTC` row read "none" before QUI-1202 as well, on a
justification that did not hold. `exec_one_hot_r` sizes its charge from a
peek that yields `cols` only for `RegVal::Model` and `0` for everything
else, then resolves with `as_model_mut`; neither Python runner peeked at
all -- both went pop, `_get_register_as_xqmx` (which accepts a sample),
dimensions, charge -- so a sample's real extent sized the charge and a
tight budget raised `MemoryLimitExceeded` where the Rust VM charged
nothing. `_peek_model_grid` closes it: the peek is `is_model()`-gated on
both VMs now, and the budget can no longer decide the fault.

That mattered because the register reaches `ONEHOTR` as a sample without
the verifier objecting. `check_reads` requires `R::Model` there, and a
register the verifier knows is a `Sample` is rejected statically -- but it
does not always know. Two routes got past it, of which one is still open:

- **Host calldata.** `Vm::set_calldata` takes any `RegVal`, samples
  included ("This allows passing models, samples, and vectors between
  programs"), and `INPUT` clones the entry into the register. `INPUT`
  writes `RegType::Any`, which satisfies every requirement, so
  `PUSH 0 / INPUT r0 / PUSH 0 / PUSH 1 / ONEHOTR r0` is verifier-clean and
  arrives holding whatever the embedder supplied. The `--calldata` CLI flag
  parses i64s and cannot express this; the embedding API is the reachable
  surface, and on a chain runtime it is the host that fills those slots.
- **A branch join.** `meet_reg` merged `Model` and `Sample` to
  `RegType::Any`, so a program that wrote `BQMX` on one branch and `BSMX`
  on the other, joined, and called `ONEHOTR` verified clean using nothing
  but its own instructions. QUI-1160 closed that: the join now yields
  `RegType::Grid`, which satisfies the grid operations and not `R::Model`.
  Host calldata is the remaining route, and it is open by design.

The name the surviving fault carries is settled as of QUI-1160:
`xqvm_py` rejects the sample with `XQMXModeError` and the Rust VM with
`RegisterType`, and both map onto `TYPE_MISMATCH`. The test above still
pins the charge rather than the name, because the charge is what QUI-1202
fixed; the name is pinned by the coefficient-access vectors instead.

## Authoring a new vector

1. Write `program.xqasm` with a minimal scenario that exercises the
   behaviour you care about.
2. Pick the matching vector directory under `vectors/` (or create a new
   one). These directory names are a separate, coarser vocabulary from
   `opcodes.yaml`'s own `category` field -- the two do not need to match,
   and mostly don't.
3. Write `inputs.json` with the calldata the program reads.
4. Produce `expected.json` by running one of the two impls and
   inspecting the result. The harness then asserts the *other* impl
   agrees. If they disagree, you've found spec drift -- file a bug
   ticket, and either fix the divergent impl or exclude the vector
   with a comment referencing the ticket.

## Drift policy

There is no `DRIFT.md`. Either every vector passes on both runtimes or
the build is broken. Concrete enforcement:

- **Opcode table** -- `xqvm/build.rs` asserts `opcodes.yaml` against the
  `opcodes!` x-macro at compile time; `scripts/check-opcode-parity.py`
  asserts `opcodes.yaml` against `xqvm_py/opcodes.py` in CI. The Rust
  check compares the wire byte, the mnemonic, the net stack effect and
  each operand's name and encoded byte width. It compares the net effect
  rather than the `stack_pop`/`stack_push` pair, because the x-macro
  stores only the net delta; the pair is compared on the Python side.
- **Bytecode encoding** -- owned by the `xqasm` crate's own test suite
  (`xqasm/tests/integration.rs` plus assembler unit tests).
- **Observable behaviour** -- [`check_vector`](src/lib.rs) asserts the
  observed `{outputs, final_stack}`, or the observed fault identity,
  matches `expected.json` for both runtimes.
- **Coverage** -- [`coverage.rs`](src/coverage.rs) computes which opcodes
  the vectors cover, and CI prints the report on every pipeline as the
  last step of `make check-parity`. It does not require completeness,
  which would fail today; [`tests/coverage.rs`](tests/coverage.rs) holds
  the current numbers as floors so coverage cannot regress unremarked.

## Running locally

```sh
# Full matrix (both runtimes, all vectors)
cargo test -p xquad-conformance

# Rust only
cargo test -p xquad-conformance --no-default-features --features rust

# Python only (needs python3 + xqvm_py importable from xqvm_py/)
cargo test -p xquad-conformance --no-default-features --features python

# Manual CLI for authoring / triaging a single vector
cargo run -p xquad-conformance -- --filter arithmetic/add_basic --impl both

# Which opcodes no vector covers
make conformance-coverage
```

The coverage report gives two numbers. **Present** counts opcodes
appearing in a vector's assembled program; **reached** counts those a
vector executes to completion. Reached is always the smaller: an opcode
behind an untaken branch is present but not reached, and so is the
instruction an error vector exists to fault on, since a faulting
instruction never completes a step. An opcode missing from both lists is
a genuine hole; one that is present but never reached has an error vector
and no success-path vector.

Override the Python interpreter with `XQUAD_CONFORMANCE_PYTHON=...`.

---

### Overview (`examples/README.md`)

# XQuad Examples

Fourteen self-contained optimisation problems, one directory each. Every
directory holds a `README.md` describing the formulation and a `runner.py`
that builds the model, solves it, and verifies the result end to end.

Run one from the repository root:

```sh
uv run python examples/maxcut/runner.py --seed 42
```

Every runner accepts `--seed`, `--interpreter` (`python` or `rust`),
`--solver`, and `-o`/`--output`. Problem size flags vary; each directory's
`README.md` carries the exact table.

## Graph problems

Examples that encode graph cuts, colouring, covers, independent sets, and tours.

- [Max-Cut](maxcut/README.md) -- Find a 2-colour partition of a weighted graph that maximises the cut weight.
- [Graph Coloring](graph_coloring/README.md) -- Assign colours to graph nodes so adjacent nodes do not share a colour.
- [Maximum Independent Set](max_independent_set/README.md) -- Select the largest subset of graph nodes with no selected edge between them.
- [Vertex Cover](vertex_cover/README.md) -- Select the smallest vertex subset that covers every graph edge.
- [Travelling Salesman Problem](tsp/README.md) -- Find the shortest Hamiltonian tour through a random symmetric distance matrix.

## Selection and packing

Examples that select subsets, cover demands, pack bins, and balance integer weights.

- [Knapsack](knapsack/README.md) -- Select items that maximise value while respecting a capacity constraint.
- [Bin Packing](bin_packing/README.md) -- Pack items into the minimum number of fixed-capacity bins.
- [Set Cover](set_cover/README.md) -- Select the minimum set collection whose union covers the universe.
- [Weighted Set Cover](weighted_set_cover/README.md) -- Select sets with capacities to cover element demands at minimum cost.
- [Number Partition](number_partition/README.md) -- Split positive integers into two subsets with nearly equal sums.
- [Portfolio Optimization](portfolio_opt/README.md) -- Select a fixed-size portfolio while penalising higher-order risk interactions.
- [Portfolio Rebalance](portfolio_rebalance/README.md) -- Choose signed integer asset weights against a risk matrix and a budget.

## Satisfiability and higher-order

Examples that reduce clauses and higher-order pseudo-Boolean objectives to quadratic models.

- [Max-3-SAT](max3sat/README.md) -- Find the assignment that satisfies the maximum number of 3-literal clauses.
- [Cubic Optimization](cubic_opt/README.md) -- Minimise a cubic pseudo-Boolean objective through HOBO degree reduction.
- [Quartic Optimization](quartic_opt/README.md) -- Minimise a degree-4 pseudo-Boolean objective through two-stage REDUCE chaining.

## Further reading

- [Using the Examples](../docs/book/src/examples/using-examples.md) -- what every
  directory has in common, how to run one, and how to adapt one into a problem
  of your own
- [Modelling with XQCP](../docs/book/src/modelling/README.md) -- the DSL these
  runners are written in
- [Cookbook](../docs/book/src/cookbook/README.md) -- the recurring encoding
  patterns these examples are built from

---

### Overview (`spec/README.md`)

# XQuad Toolchain Specifications

Authoritative specifications for each layer of the XQuad Toolchain. Every
specification here is the single source of truth for its layer. The
`conformance/` harness does not check either implementation against this
prose: it checks the Rust and Python implementations against each other,
and against `conformance/opcodes.yaml`. Where a divergence between a spec
and an implementation is known, the relevant book page records it.

## Layers

- **[xqvm/](xqvm/)** -- X-Quadratic Virtual Machine. Bytecode format,
  instruction semantics, stack and register model, binary encoding.
  See [`xqvm/README.md`](xqvm/README.md) for the document index.
- **[xqcp/](xqcp/)** -- X-Quadratic Constraint Programming DSL. Symbolic
  problem description and compilation to XQVM assembly. See
  [`xqcp/README.md`](xqcp/README.md).
- **[xqsa/](xqsa/)** -- X-Quadratic Solver Adapters. Solver-backend
  interface and sample/energy contracts. See
  [`xqsa/README.md`](xqsa/README.md).

## Drift policy

There is no tolerated drift between spec and implementations. Any change
to the `xqvm/` spec files that affects observable behaviour must land with:

1. A matching update to `../conformance/opcodes.yaml`.
2. Updated or new conformance vectors under `../conformance/vectors/`.
3. Both `xqvm` (Rust) and `xqvm_py` (Python) passing the full conformance
   suite.

Builds fail on mismatch -- there is no `DRIFT.md`.

---

### Overview (`xqasm/README.md`)

# xqasm

Text assembler for the [XQuad Toolchain](https://gitlab.com/quip.network/xquad).

`xqasm` turns `.xqasm` assembly source into `xqvm::Program` bytecode: a
[`pest`](https://crates.io/crates/pest) grammar parses source text into an
AST, the assembler lowers the AST to instructions via
`xqvm::InstructionBuilder`, and errors are reported as rich
[`miette`](https://crates.io/crates/miette) diagnostics with source spans.
This crate declares no binary; the `xquad asm` subcommand in
[`xqcli`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcli) is the
command-line entry point.

## Install

```sh
cargo add xqasm
```

## Quick start

The one-call entry point is `assemble_source`:

```rust
use xqasm::assemble_source;
use xqvm::Vm;

let program = assemble_source("PUSH 10\nPUSH 32\nADD\nHALT").unwrap();

let mut vm = Vm::new();
vm.run(&program).unwrap();
assert_eq!(vm.stack(), &[42]);
```

For finer control over the two phases, `parse` and `assemble` are exposed
separately:

```rust
use xqasm::{assemble, parse};

let source = "PUSH 10\nPUSH 32\nADD\nHALT";
let lines = parse(source, "<input>").unwrap();
let program = assemble(&lines, source, "<input>").unwrap();
assert!(!program.code().is_empty());
```

## Assembly syntax, briefly

- Line comments start with `;` and run to end of line.
- Jump targets are declared with `TARGET .N` and referenced as `JUMP .N` /
  `JUMPI .N`, where `.N` is a numeric label ID (`.0`, `.1`, and so on)
  assigned sequentially during pre-scan; the `.N` text is assembler-only and
  never appears in the bytecode.
- `PUSH <value>` desugars to the smallest `PUSH1`-`PUSH8` opcode that fits
  the signed integer literal.
- Registers are `r0`..`r255`.

## Links out

- [Bytecode encoding spec](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/ENCODING.md) -- assembly syntax and binary format
- [Normative VM spec](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/SPEC.md)
- [`xqvm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm) -- bytecode types and interpreter this crate targets
- [`xqcli`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcli) -- the `xquad` CLI, wraps this crate as `xquad asm`
- [Repository](https://gitlab.com/quip.network/xquad)

## License

AGPL-3.0-or-later.

---

### Overview (`xqcli/README.md`)

# xqcli

Unified CLI for the [XQuad Toolchain](https://gitlab.com/quip.network/xquad).

This crate is named `xqcli`; it builds the `xquad` binary. That is:
`cargo install xqcli` installs a command called `xquad`, not `xqcli`.
Running `xqcli --help` after installing will fail because no such binary
exists.

## Install

```sh
cargo install xqcli
xquad --help
```

## Subcommands

`xquad` has four subcommands, one per stage of the toolchain: `asm`,
`dism`, `run`, `verify`.

| Subcommand | Does |
|---|---|
| `xquad asm <INPUT>` | Assemble `.xqasm` source into `.xqb` bytecode |
| `xquad dism [FILE]` | Disassemble bytecode into a human-readable listing |
| `xquad run <FILE>` | Run bytecode (or assembly, with `--text`) on the interpreter |
| `xquad verify <FILE>` | Run the bytecode verifier (all phases) |

Run `xquad <subcommand> --help` for the full option list; the summaries
below are the parts most people need first.

- `xquad asm`: `-o, --output <OUTPUT>` (defaults to `<input>.xqb`), `--stdout` to write bytecode to stdout instead of a file.
- `xquad dism`: reads from stdin when `FILE` is omitted.
- `xquad run`: `--text` to treat `FILE` as assembly, `--calldata <CALLDATA>` (comma-separated integers), `--outputs <OUTPUTS>` (default 16), `--step-limit <STEP_LIMIT>` (default 10000000; the value is an exact limit, so `0` executes nothing), `--unlimited-steps` to run without a step limit, `--trace` with `--trace-format text|json` and `--trace-file <FILE>`.
- `xquad verify`: `--text` to treat `FILE` as assembly before verifying.

## Quick start

```sh
cat > add.xqasm <<'EOF'
; add.xqasm -- push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
EOF

xquad asm add.xqasm -o add.xqb
xquad run add.xqb
xquad dism add.xqb
xquad verify add.xqb
```

`xquad run add.xqb` prints the residual stack:

```
stack (bottom to top):
  42
```

## Links out

- [`xqvm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm) -- bytecode types and interpreter this CLI drives
- [`xqasm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqasm) -- text assembler behind `xquad asm`
- [Normative VM spec](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/SPEC.md)
- [Bytecode encoding spec](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/ENCODING.md)
- [Repository](https://gitlab.com/quip.network/xquad)

## License

AGPL-3.0-or-later.

---

### Overview (`xqcp/README.md`)

# XQCP -- X-Quadratic Constraint Programmer

A constraint-programming DSL that compiles a problem description into three
XQVM assembly programs: an encoder, a verifier, and a decoder. See
[Three Programs](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/concepts/three-programs.md)
for what those three are and why there are three.

## Install

```sh
pip install xqcp
```

## A complete example

Max-Cut for three nodes -- the same graph
[Quadratic Models](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/concepts/quadratic-models.md#a-worked-example-max-cut)
derives by hand -- built with `Problem` and compiled to `.xqasm`:

```python
from xqcp import Domain, Problem, Types

problem = Problem("Triangle")
n = problem.input("n", type=Types.Int)
edges = problem.input("edges", type=Types.Vec)
problem.define_model(size=n, domain=Domain.BINARY)

edge_count = problem.stow("edge_count", edges.veclen() // 3)
with problem.range(0, edge_count) as e:
    offset = e * 3
    i = problem.stow("i", edges.get(offset))
    j = problem.stow("j", edges.get(offset + 1))
    w = problem.stow("w", edges.get(offset + 2))
    problem.model.linear[i].add(-w)
    problem.model.linear[j].add(-w)
    problem.model.quadratic[i, j].add(w * 2)

partition = problem.output("partition", type=Types.Vec)
with problem.range(0, n) as node:
    partition.append(problem.sample.getline(node))

programs = problem.compile()
print(programs.encoder.count("\n"), "lines in the compiled encoder")
# 57 lines in the compiled encoder
```

`programs.encoder` builds the model; `programs.verifier` checks a sample
against it and computes its energy; `programs.decoder` reads a sample back
into a `partition`. Each is a standalone `.xqasm` string, runnable through
any XQVM interpreter.

`Domain` also covers integer variables (`INTEGER` with `k=`, or with
`lo=`/`hi=` for a range XQCP shifts into the model and back out through
`sample.value()`) and unordered cases (`CATEGORICAL` with `k=` and
`penalty=`, recorded as a one-hot binary grid read with `sample.case()`).
The [Modelling](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/modelling/inputs-and-model.md)
chapter covers each form and what constraints are available on it.

## How it works

`Problem` records every DSL call (`input`, `define_model`, `range`,
`stow`, `model.linear[i].add(w)`, and the rest) as an `Action`. Nothing is
assembled until `compile()` runs: it walks the recorded actions once per
program and emits assembly with automatic register allocation and loop
nesting.

## DSL reference

The full DSL -- inputs, expressions, objectives, constraints, control flow,
outputs, and what `compile()` produces -- is
[Modelling](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/modelling/README.md),
eight chapters built around this exact package. The normative reference is
[`spec/xqcp/SPEC.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqcp/SPEC.md);
this package follows it, and any divergence here is a bug.

## Examples

Fifteen complete problems built with this DSL, from
[Max-Cut](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/examples/maxcut.md)
to
[Travelling Salesman](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/examples/tsp.md),
are listed in the
[example gallery](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/examples/README.md).

## Also see

- [`xqvm_py`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm_py) -- pure-Python reference VM that runs the compiled programs.
- [`xqffi`](https://gitlab.com/quip.network/xquad/-/tree/main/xqffi) -- pyo3 FFI bindings to the Rust runtime.
- [`xqsa`](https://gitlab.com/quip.network/xquad/-/tree/main/xqsa) -- solver adapters for the models this package builds.
- [`xquad`](https://gitlab.com/quip.network/xquad/-/tree/main/xquad) -- umbrella meta-package.

## License

AGPL-3.0-or-later.

---

### Overview (`xqffi/README.md`)

# xqffi -- PyO3 FFI bindings for the XQuad Rust runtime

The Python extension that exposes the Rust [`xqvm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm) interpreter and [`xqasm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqasm) assembler. Ships as abi3 manylinux wheels (x86_64 + aarch64, CPython >= 3.13) plus a universal sdist for other platforms, all built by maturin.

`xqffi` is a **pure FFI shim** -- it only re-exposes the Rust types and entry points. User-facing conveniences (`Program` / `Session` / `RunResult`, keyword calldata, trace inspection) live in the [`xquad`](https://gitlab.com/quip.network/xquad/-/tree/main/xquad) umbrella package.

## Install

```sh
pip install xqffi
```

## Surface

### `xqffi.vm` -- low-level one-shot

What the conformance harness drives. Small wrapper over `xqvm::Vm`:

```python
from xqffi.vm import Vm
from xqffi.asm import assemble_source

vm = Vm()
vm.set_calldata([40, 2])
vm.set_output_slots(1)
vm.run(assemble_source(src))
assert vm.outputs() == [42]
```

`XqmxModel` and `XqmxSample` are thin getter/setter wrappers around the Rust types for direct model/sample manipulation. They refuse what bytecode could not build: a `size` over `MAX_ALLOCATION_SIZE` or a grid that does not fit (`ValueError`), a sample value outside its domain (`ValueError`), and a coefficient index outside `0..size`, negative included (`IndexError`).

### `xqffi.asm` -- assembly

`parse_xqasm(str)` -> wire dict; `assemble_source(str)` -> raw bytes; `disassemble(bytes)` -> human-readable listing; `instruction_count(bytes)` -> instruction count.

### `xqffi.verifier` -- static verification

`verify(bytes)` and `verify_source(str)` run the pre-execution verifier: every jump lands on a `TARGET`, every register is read after it is written, and stack depth balances. Both return `None` on success and raise `ValueError` on a rejected program. See [Verifier](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/xqvm/verifier.md).

## Interactive / multi-run API

Use [`xquad.program`](https://gitlab.com/quip.network/xquad/-/tree/main/xquad) for the REPL / notebook / script workflow:

```python
from xquad.program import Program

program = Program.from_source(src)
session = program.session(output_slots=1)
session.set_calldata([40, 2])
result = session.run()
assert dict(result.outputs) == {0: 42}
```

`Program.load(bytes)` takes wire-format bytecode; `Session.run()` returns a `RunResult` with dict-keyed outputs (unset slots present as `None`), residual stack, and step count.

## Also see

- [`xqvm_py`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm_py) -- pure-Python reference VM (conformance oracle).
- [`xqcp`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcp) -- constraint programming DSL that compiles to `.xqasm`.
- [`xqsa`](https://gitlab.com/quip.network/xquad/-/tree/main/xqsa) -- solver adapters: CPU/GPU annealers, D-Wave QPU, Quip network.
- [`xquad`](https://gitlab.com/quip.network/xquad/-/tree/main/xquad) -- umbrella meta-package with the interactive API.
- [Running Programs](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/running/README.md) -- full tour.

## License

AGPL-3.0-or-later.

---

### Overview (`xqsa/README.md`)

# xqsa -- Solver adapters for XQMX models

Pluggable solvers for quadratic optimisation models produced by the XQuad
toolchain. One interface, five backends:

| Solver | Class | Where it solves | Install |
|---|---|---|---|
| DWave CPU simulated annealing | `SolverDWaveCPU` | Locally, on the CPU | `pip install xqsa` |
| D-Wave Advantage QPU | `SolverDWaveQPU` | D-Wave Leap cloud | `pip install xqsa[dwave]` |
| CUDA GPU simulated annealing | `SolverCudaGPU` | Locally, NVIDIA GPU | `pip install xqsa[cuda]` |
| Metal GPU SA / Gibbs | `SolverMetalGPU` | Locally, Apple GPU (macOS) | `pip install xqsa[metal]` |
| Quip network (on-chain mempool) | `SolverQuip` | Quip network | `pip install xqsa[quip]` |

## Install

```sh
pip install xqsa               # CPU simulated annealing only
pip install xqsa[dwave]        # add D-Wave QPU support
pip install xqsa[cuda]         # add CUDA GPU support
pip install xqsa[metal]        # add Metal GPU support
pip install xqsa[quip]         # add Quip network support
```

Extras are composable: `pip install "xqsa[cuda,dwave]"`.

## A complete example

`build_solver` picks a backend by name, so a caller can stay
backend-agnostic across all five:

```python
from xqsa import SOLVERS, build_solver
from xqvm_py import XQMX

model = XQMX.binary_model(size=4)
model.set_linear(0, -1)
model.set_quadratic(0, 1, 2)

print(sorted(SOLVERS))
# ['cuda-gpu', 'dwave-cpu', 'dwave-qpu', 'metal-gpu', 'quip']

solver = build_solver("dwave-cpu", seed=42)
result = solver.solve(model)
print(result.sample)
print(result.energy)
# XQMX(mode=SAMPLE, domain=BINARY, size=4, linear_terms=1, quadratic_terms=0)
# -1
```

Every backend's `solve()` returns the same `SolverResult(sample, energy,
timing, metadata)`: `sample` is the best assignment found, `energy` is the
Hamiltonian recomputed independently in exact integer arithmetic (never
taken on faith from the backend), and `metadata`'s shape is per-backend.
`build_solver("dwave-cpu", ...)` is equivalent to constructing
`SolverDWaveCPU(...)` directly; reach for the class constructor instead of
`build_solver` when the backend is fixed at write time rather than chosen
by name at run time.

`SolverQuip` also splits a job into steps: `create_order(model)` returns
a draft `JobOrder` whose options (payout rule, allowlist, quality
floors) are checked before anything is signed, `quote()` prices it and
`submit()` sends it. A submitted order leaves a `JobOrderReceipt`, which
any session rebuilds from the order id with `get_receipt()`. Import them
from `xqsa.quip`.

## Reference

Driver prerequisites, per-backend parameters, embedding on a D-Wave QPU,
the Quip network job lifecycle and its job orders, and what the reported
energy means on
fixed-precision hardware are covered in
[Solving Overview](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/solving/README.md)
and its chapters. The normative reference is
[`spec/xqsa/SPEC.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqsa/SPEC.md);
this package follows it, and any divergence here is a bug.

## Also see

- [`xqvm_py`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm_py) -- pure-Python reference VM.
- [`xqffi`](https://gitlab.com/quip.network/xquad/-/tree/main/xqffi) -- pyo3 FFI bindings to the Rust runtime.
- [`xqcp`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcp) -- constraint-programming DSL that compiles to models this package can sample.
- [`xquad`](https://gitlab.com/quip.network/xquad/-/tree/main/xquad) -- umbrella meta-package.
- [Running Programs](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/running/README.md) -- end-to-end tour.

## License

AGPL-3.0-or-later.

---

### Overview (`xquad/README.md`)

# xquad -- Primary user-facing package for the XQuad toolchain

`xquad` is the Python distribution most users should install. It provides the interactive `Program` / `Session` / `RunResult` API, a unified `VM` wrapper that dispatches to either the Rust or Python interpreter, and re-exports the lower-level peer packages (`xqffi`, `xqcp`, `xqsa`) under a single namespace.

## Install

```sh
pip install xquad
```

Pulls `xqffi`, `xqcp`, `xqsa`, and `xqvm_py` as runtime dependencies.
Peer installs stay valid (`pip install xqffi xqcp xqsa`) if you only
need a subset.

## Interactive usage

Load a program once, run it with different calldata, inspect outputs by slot:

```python
from xquad.program import Program

program = Program.from_source("""
PUSH 0
INPUT r0
PUSH 1
INPUT r1
LOAD r0
LOAD r1
ADD
STOW r2
PUSH 0
OUTPUT r2
HALT
""")

session = program.session(output_slots=1)
session.set_calldata([40, 2])
result = session.run()
assert dict(result.outputs) == {0: 42}
```

`Program.load(bytes)` takes wire-format bytecode; `Session.run()` returns a `RunResult` with dict-keyed outputs (unset slots present as `None`), residual stack, and step count.

## Full pipeline

Compile a problem via the DSL, execute, sample:

```python
from xquad import cp, sa
from xquad.vm import VM, VMBackend

problem = cp.Problem("MyProblem")
# ... define inputs, model, objective, decoder ...
programs = problem.compile()

vm = VM(backend=VMBackend.RUST)   # or VMBackend.PYTHON for the reference interpreter
vm.set_output_slots(1)
vm.run(programs.encoder)
model = vm.outputs()[0]

sample = sa.SolverDWaveCPU().solve(model).sample
```

Re-exports of peer packages are identity, not copies:

```python
import xquad, xqcp
assert xquad.cp.Problem is xqcp.Problem   # always True
```

So `isinstance` works whether the caller imported directly from the peer or via the umbrella.

## Subnamespaces

| `xquad.*` | Origin | Contents |
|-----------|--------|----------|
| `xquad.program` | first-party | Interactive API -- `Program`, `Session`, `RunResult` |
| `xquad.vm` | first-party | Unified `VM` wrapper with backend dispatch (`VMBackend.RUST` / `VMBackend.PYTHON`) |
| `xquad.types` | first-party | Canonical Python type aliases used across the API |
| `xquad.asm` | re-exports `xqffi.asm` | `parse_xqasm`, `assemble_source`, `disassemble` |
| `xquad.verifier` | re-exports `xqffi.verifier` | `verify`, `verify_source` |
| `xquad.cp` | re-exports `xqcp` | DSL -- `Problem`, `Types`, expression builders |
| `xquad.sa` | re-exports `xqsa` | Solvers -- `SolverDWaveCPU`, `Solver` |

Lower-level escape hatches remain available directly via the peer packages (`xqffi.vm.Vm` for the raw FFI one-shot surface; `xqvm_py.Executor` for the pure-Python reference VM).

## Also see

- [`xqffi`](https://gitlab.com/quip.network/xquad/-/tree/main/xqffi) -- PyO3 FFI bindings (low-level / conformance).
- [`xqvm_py`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm_py) -- pure-Python reference VM (conformance oracle).
- [`xqcp`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcp) -- constraint-programming DSL.
- [`xqsa`](https://gitlab.com/quip.network/xquad/-/tree/main/xqsa) -- solver adapters.
- [Running Programs](https://gitlab.com/quip.network/xquad/-/blob/main/docs/book/src/running/README.md) -- end-to-end tour including the Program/Session, `xqffi.vm`, and `xquad.vm` surfaces.

## License

AGPL-3.0-or-later.

---

### Overview (`xqvm/README.md`)

# xqvm

X-Quadratic Virtual Machine -- bytecode interpreter for the [XQuad Toolchain](https://gitlab.com/quip.network/xquad).

`xqvm` is the runtime layer of XQuad: the opcode table, instruction types, a
bytecode builder and codec, an incremental instruction-stream reader, the
VM interpreter, and a disassembler. A problem compiled once to XQVM
bytecode runs unchanged on this interpreter or on the pure-Python
reference VM (`xqvm_py`); the two are checked for behavioural parity in CI.

The crate is `no_std + alloc`-compatible. The bytecode layer (opcode
table, instruction codec, `InstructionBuilder`, `Program`) and the
interpreter core always build without `std`. The `std` feature, enabled
by default, adds the disassembler, JSON/text execution tracers, and
`miette`-backed runtime diagnostics.

## Install

```sh
cargo add xqvm
```

For a `no_std` target (WASM runtimes, Substrate pallets, embedded):

```sh
cargo add xqvm --no-default-features
```

## Quick start

Build a program with `InstructionBuilder`, then run it on `Vm`:

```rust
use xqvm::{InstructionBuilder, Vm};

let mut builder = InstructionBuilder::new();
builder.emit_push(10).emit_push(32).emit_add().emit_halt();
let program = builder.build().unwrap();

let mut vm = Vm::new();
vm.run(&program).unwrap();
assert_eq!(vm.stack(), &[42]);
```

This builds the same four instructions (`PUSH 10`, `PUSH 32`, `ADD`,
`HALT`) that the [`xqasm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqasm)
text assembler produces from source; see that crate to load `.xqasm`
files instead of building bytecode by hand.

## What is in this crate

| Layer | Contents |
|---|---|
| Bytecode (`no_std`) | `opcodes!` x-macro (single source of truth for the opcode table), `Opcode`, `Instruction`, `Register`, `Program`, `InstructionBuilder`, `InstructionStream`, `bytecode::codec` |
| Interpreter | `Vm`, `Error`, `RegVal`, `XqmxModel` / `XqmxSample` (QUBO/Ising/integer models and candidate solutions) |
| `std`-only | `disasm::Disassembly`, `RuntimeDiagnostic`, `JsonTracer` / `TextTracer` |

## Wire format

A `.xqb` file opens with a fixed 15-byte XQBC header (magic, version,
calldata/output-slot counts, code length, and a CRC-32 checksum of the
payload), followed immediately by the instruction stream: an opcode byte
followed by its operands in big-endian byte order. See the encoding spec
linked below for the full field layout.

## Links out

- [Normative VM spec](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/SPEC.md)
- [Bytecode encoding spec](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/ENCODING.md)
- [`xqasm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqasm) -- text assembler that emits `xqvm::Program`
- [`xqcli`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcli) -- the `xquad` CLI, built on this crate
- [Repository](https://gitlab.com/quip.network/xquad)

## License

AGPL-3.0-or-later.

---

### Overview (`xqvm_py/README.md`)

# xqvm_py

> **Status -- transitional.** `xqvm_py` is the executable conformance
> oracle for the XQuad toolchain: every vector under
> [`conformance/`](https://gitlab.com/quip.network/xquad/-/tree/main/conformance) must produce identical
> observable state on both this Python reference and the Rust `xqvm`
> production runtime. The arrangement is explicitly transitional. Once
> the Rust runtime is fully battle-tested, `xqvm_py` may be **dropped
> entirely** (with conformance vectors graduating to "Rust must
> produce exactly these outputs" reference data) or **demoted to a
> prototyping sandbox** for trying out new opcodes / VM features in
> Python before they earn a spec slot. Do not build infrastructure
> that hard-depends on `xqvm_py`'s existence.

Python reference implementation of the X-Quadratic Virtual Machine.
See [`spec/xqvm/SPEC.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/SPEC.md) for the authoritative
technical specification.

## Scope

- Pure-Python **executor**, **state** model, **opcodes**, **xqmx**
  (sparse quadratic matrix), **vector**, and **tracer**.
- **No** assembler or disassembler: the Rust `xqasm` crate is the only
  implementation, exposed to Python via `xqffi.asm` (pyo3). Tests
  and the CLI shim call
  [`xqvm_py.program_from_xqasm`](https://gitlab.com/quip.network/xquad/-/blob/main/xqvm_py/program.py) to turn `.xqasm` text
  into an executable `Program`.

## Layout

```text
xqvm_py/                  <-- this directory IS the package (flat layout)
  __init__.py             re-exports the public surface (Executor, Program, ...)
  __main__.py             entry point for `python -m xqvm_py`
  executor.py             fetch-decode-execute loop
  state.py                stack, registers, loop control, jump table
  opcodes.py              Opcode enum + operand metadata
  program.py              Program dataclass + program_from_xqasm()
  xqmx.py                 sparse quadratic matrix (model + sample modes)
  vector.py               typed vec<int> / vec<xqmx>
  errors.py               typed runtime errors
  tracer.py               step-by-step execution tracer
  cli/                    python -m xqvm_py run ...
  tests/                  pytest suite (wheel-excluded)
```

## Quick start

From the xquad workspace root:

```sh
uv sync                                       # installs xqvm_py editable + xqffi via maturin
uv run pytest xqvm_py/tests                   # run the full test suite
echo "PUSH 5
PUSH 3
ADD
HALT" > /tmp/prog.xqasm
uv run python -m xqvm_py run /tmp/prog.xqasm  # CLI shim
```

Programmatic use:

```python
from xqvm_py import Executor, program_from_xqasm

prog = program_from_xqasm("PUSH 10\nPUSH 5\nADD\nSTOW r0\nHALT\n")
executor = Executor()
executor.execute(prog)
print(executor.state.get_register(0))  # 15
```

## Conformance

Behavioural parity with the Rust `xqvm` crate is enforced by the
`xquad-conformance` Rust test harness at
[`conformance/`](https://gitlab.com/quip.network/xquad/-/tree/main/conformance). New VM semantics require a new
vector. Divergence between implementations fails CI with no "drift
tracking" middle ground.

---

### Overview (`examples/bin_packing/README.md`)

# Bin Packing

Pack N items with given integer sizes into the minimum number of bins, each
with a fixed capacity C.

## QUBO formulation

- **Input**: N item sizes (Vec), number of bins B, bin capacity C
- **Model**: `(N + 1) * B` binary variables in an `(N + 1) x B` grid. Rows `0..N-1` are the assignment cells: `x[i,b] = 1` if item i is placed in bin b. Row N holds one indicator per bin: `y[b] = 1` if bin b is open.
- **Objective**: `+BIN_COST` on each indicator `y[b]`, so the energy counts the bins the packing opens. A bias spread over the assignment cells instead would be identically N on every feasible packing, because each item lands in exactly one bin, and could not tell a one-bin packing from a three-bin one.
- **Constraints**:
  - Assignment per item i: `sum_b x[i,b] = 1` (ONEHOTR, penalty 200)
  - Linking per (i, b): `x[i,b] -> y[b]` (IMPLIES, penalty 200)
  - Capacity per bin b: `sum_i s_i * x[i,b] <= C` (SLACK + EQUALITY, penalty 100)

The capacity inequality is encoded by appending binary slack variable entries
to the column index/coefficient vectors, converting it to a weighted equality.
Each bin gets its own slack block: bin b's entries start at
`(N + 1) * B + b * bitlen(C)`, past every model variable and past every
earlier bin's block. Sharing one start index across bins would let one bin's
slack absorb another's overflow, leaving the capacity constraint
under-constrained.

## DSL methods used

- `model.apply_onehot_row(row, penalty)` -- ONEHOTR assignment constraint
- `model.apply_implies(coord_a, coord_b, penalty)` -- IMPLIES linking constraint
- `problem.vec()` -- allocate untyped vector registers for indices and coefficients
- `problem.slack(indices, coeffs, start_index, capacity)` -- append slack entries
- `model.apply_equality(indices, coeffs, target, penalty)` -- EQUALITY constraint
- `xq_bitlen(value)` -- BITLEN, used to size each bin's slack block

## Pipeline overview

1. **CP** (`xqcp`) -- generate random item sizes, declare an `(N + 1) x B` binary grid, and add ONEHOTR assignment constraints per item, IMPLIES links per cell, and SLACK + EQUALITY capacity constraints per bin.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary, the per-item assignment, the bin-usage links and the per-bin capacity, then computes energy
6. **Decode** -- decoder extracts the bin assignments

## Usage

```sh
uv run python examples/bin_packing/runner.py --seed 42
uv run python examples/bin_packing/runner.py --n 5 --bins 4 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `4` | Number of items |
| `--bins` | `3` | Number of bins |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/cubic_opt/README.md`)

# Cubic Optimization

Minimise a cubic pseudo-Boolean objective over binary variables via single-stage
HOBO degree reduction.

## QUBO formulation

- **Input**: N binary variables, M cubic interaction terms `(i, j, k, c)`
- **Model**: N binary variables. Linear bias `-1` per variable rewards selection, creating tension with the positive cubic terms.
- **Objective**: `sum(c_t * x_i * x_j * x_k) - sum(x_v)`

Each cubic term `(i, j, k, c)` is degree-reduced to quadratic via:

1. `REDUCE(i, j, P_AUX) -> w` -- allocates auxiliary variable w with Rosenberg enforcement `P_AUX*(x_i*x_j - 2*x_i*w - 2*x_j*w + 3*w)`
2. `ADDQUAD(w, k, c)` -- adds `c*w*x_k = c*x_i*x_j*x_k` to the QUBO

## DSL methods used

- `model.reduce(var_a, var_b, p_aux)` -- single-stage HOBO degree reduction

## Pipeline overview

1. **CP** (`xqcp`) -- generate random cubic interaction terms, declare binary variables with linear bias, and degree-reduce each cubic term via REDUCE.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
6. **Decode** -- decoder extracts the variable assignment

## Usage

```sh
uv run python examples/cubic_opt/runner.py --seed 42
uv run python examples/cubic_opt/runner.py --n 5 --m 4 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `4` | Number of variables |
| `--m` | `3` | Number of cubic terms |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/graph_coloring/README.md`)

# Graph Coloring

Assign one of C colors to each node of an undirected graph such that no two
adjacent nodes share the same color (proper C-coloring).

## QUBO formulation

- **Input**: number of nodes N, number of colors C, edge list
- **Model**: N categorical variables over C cases. `Domain.CATEGORICAL` records that as an N x C binary grid, `x[v,c] = 1` if node v gets color c, with a one-hot row per node.
- **Objective**: none. The problem is pure constraint satisfaction: a colouring is scored only by its constraint violations. A valid C-colouring scores `-penalty * N`, not `0`, because XQMX has no constant-term field and each satisfied ONEHOTR stores `-penalty` rather than `0`. That is `-1000` at the `--n 5`, penalty `200` defaults. The colour count defaults to `4` because a G(5, 0.5) graph routinely contains a 4-clique -- the default seed's does -- and a 4-clique has no 3-colouring, so `--colors 3` would make the canonical instance unsatisfiable. See [Constraints](../../docs/book/src/modelling/constraints.md#why-the-reported-energy-is-not-just--total_value).
- **Constraints**:
  - One-hot per node: `sum_c x[v,c] = 1` (ONEHOTR, penalty 200) -- implied by the domain, not written
  - Exclusion per (edge, color): `x[u,c] + x[v,c] <= 1` (EXCLUDE, penalty 200)

### Encoding strategy

The one-hot row per node is what makes a categorical variable categorical, so
`define_model(domain=Domain.CATEGORICAL, k=C, penalty=200)` emits it: the grid
and one ONEHOTR per row, through the same record paths a hand-written loop
would use. The compiled program is what this example emitted before the
domain existed.

EXCLUDE is applied per (edge (u,v), color c) pair via a nested range loop.
The 2D coordinates (u, c) and (v, c) are resolved to flat indices using
IDXGRID: `u * num_colors + c` and `v * num_colors + c`.

Reading the answer back is `sample.case(v)`, a ROWFIND for the 1 in node v's
row. ROWFIND returns `-1` where a row holds no 1, which is the uncoloured
sentinel this example used to produce in host code.

## DSL methods used

- `problem.define_model(size=N, domain=Domain.CATEGORICAL, k=C, penalty=200)` -- the grid and its one-hot rows
- `model.apply_exclude((u, c), (v, c), penalty)` -- EXCLUDE per edge per color
- `sample.case(node)` -- the case a node took, or `-1`

## Pipeline overview

1. **CP** (`xqcp`) -- generate a random graph, declare N categorical variables over C cases, and add EXCLUDE constraints per (edge, color) pair.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary, the one-hot row sums, and the per-edge exclusions, then computes energy
6. **Decode** -- decoder reads each node's case with ROWFIND, giving one color index per node

## Usage

```sh
uv run python examples/graph_coloring/runner.py --seed 42
uv run python examples/graph_coloring/runner.py --n 6 --colors 4 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of nodes |
| `--colors` | `4` | Number of colors |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/knapsack/README.md`)

# Knapsack

The 0/1 Knapsack problem: given N items with integer weights and values,
select a subset maximising total value subject to a weight capacity constraint.

## QUBO formulation

- **Input**: N item weights and values, capacity W
- **Model**: N binary variables. `x_i = 1` means item i is selected.
- **Objective**: minimise `-sum(v_i * x_i)`
- **Constraints**: capacity `sum(w_i * x_i) <= W` (SLACK + EQUALITY)

The inequality is encoded via SLACK + EQUALITY. SLACK appends binary slack
variable entries (`s_j` with coefficients `2^j`) to the index and coefficient
vectors, converting the inequality to the equality `sum(w_i*x_i) + sum(s_j*2^j) = W`.
EQUALITY then adds the penalty term `P*(sum(a_k*x_k) - W)^2` to the QUBO.

## DSL methods used

- `problem.vec()` -- allocate untyped vector registers for indices and coefficients
- `problem.slack(indices, coeffs, start_index, capacity)` -- append slack entries
- `model.apply_equality(indices, coeffs, target, penalty)` -- EQUALITY constraint

## Pipeline overview

1. **CP** (`xqcp`) -- generate random item weights and values, declare binary variables, and encode the capacity inequality via SLACK + EQUALITY.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that the capacity constraint holds, then computes energy
6. **Decode** -- decoder extracts the item selection

## Usage

```sh
uv run python examples/knapsack/runner.py --seed 42
uv run python examples/knapsack/runner.py --n 6 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of items |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/max3sat/README.md`)

# Max-3-SAT

Given M clauses of 3 positive literals over N binary variables, find the
assignment that satisfies the maximum number of clauses.

## QUBO formulation

- **Input**: N binary variables, M clauses of 3 positive literals
- **Model**: N binary variables. `x_v in {0, 1}`.
- **Objective**: minimise `sum over clauses of P*(1-x_i)(1-x_j)(1-x_k)`

A clause (i,j,k) is violated when all three variables are 0. Expanding
the product (dropping the constant term):

    P*(-x_i - x_j - x_k + x_i*x_j + x_i*x_k + x_j*x_k - x_i*x_j*x_k)

The cubic term `-P*x_i*x_j*x_k` is degree-reduced via `REDUCE(i, j) -> w`,
introducing one auxiliary variable w per clause with Rosenberg enforcement
`P_AUX*(x_i*x_j - 2*x_i*w - 2*x_j*w + 3*w)`. The cubic term becomes the
quadratic term `-P*w*x_k`.

## DSL methods used

- `model.reduce(var_a, var_b, p_aux)` -- HOBO degree reduction; returns a
  RegLoad holding the auxiliary variable index for chaining into quadratic terms

## Pipeline overview

1. **CP** (`xqcp`) -- generate random 3-literal clauses, declare binary variables, and degree-reduce the cubic violation terms via REDUCE.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
6. **Decode** -- decoder extracts the variable assignment

## Usage

```sh
uv run python examples/max3sat/runner.py --seed 42
uv run python examples/max3sat/runner.py --n 8 --m 10 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `6` | Number of Boolean variables |
| `--m` | `8` | Number of clauses |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/max_independent_set/README.md`)

# Max Independent Set

Find the largest subset of nodes in an undirected graph such that no two
selected nodes share an edge.

## QUBO formulation

- **Input**: number of nodes N, edge list
- **Model**: N binary variables. `x_i = 1` if node i is in the independent set.
- **Objective**: minimise `-sum(x_i)` (maximise set size)
- **Constraints**: per edge (i,j): `x_i + x_j <= 1` (SLACK + EQUALITY)

Each edge inequality is encoded via SLACK + EQUALITY. A single binary slack
variable s (capacity = 1) converts `x_i + x_j <= 1` into the equality
`x_i + x_j + s = 1`, and EQUALITY adds the penalty `P*(x_i + x_j + s - 1)^2`.

Slack variable indices start at num_nodes and are allocated one per edge.

## DSL methods used

- `problem.vec()` -- allocate untyped vector registers for indices and coefficients
- `problem.slack(indices, coeffs, start_index, capacity)` -- append one slack entry per edge
- `model.apply_equality(indices, coeffs, target, penalty)` -- EQUALITY constraint

## Pipeline overview

1. **CP** (`xqcp`) -- generate a random graph, declare binary variables (one per node), and encode each edge independence constraint via SLACK + EQUALITY.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that the independence constraints hold, then computes energy
6. **Decode** -- decoder extracts the selected nodes

## Usage

```sh
uv run python examples/max_independent_set/runner.py --seed 42
uv run python examples/max_independent_set/runner.py --n 7 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of nodes |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/maxcut/README.md`)

# Max-Cut

Find a 2-colour partition of a weighted graph that maximises the total weight
of edges crossing the partition.

## QUBO formulation

- **Input**: `num_nodes` (int), `edges` (Vec of flat `(i, j, w)` triples, `3*|E|` entries)
- **Model**: `n` binary variables, one per node. `x[v] in {0, 1}` selects the side of the cut.
- **Objective**: for each edge `(i, j, w)`, add `-w*(x_i + x_j)` and `+2w*x_i*x_j`. Minimising this minimises `-sum w*[x_i != x_j]`, i.e. maximises the cut.

## DSL methods used

- `problem.input()` -- declare typed calldata inputs
- `problem.define_model()` -- allocate binary XQMX model
- `problem.stow()` -- bind intermediate computations to named registers
- `problem.range()` -- emit RANGE loops
- `model.linear[i].add()` -- accumulate linear bias on variable i
- `model.quadratic[i, j].add()` -- accumulate quadratic coupling between variables i and j
- `problem.output()` -- declare typed output slots
- `problem.sample.getline()` -- read a row from the sample bitstring

## Pipeline overview

1. **CP** (`xqcp`) -- build a random weighted complete graph, declare binary variables (one per node), and add linear/quadratic QUBO terms per edge.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary, then computes energy; this problem declares no constraints for it to check
6. **Decode** -- decoder extracts the 2-colour partition

## Usage

```sh
uv run python examples/maxcut/runner.py --seed 42
uv run python examples/maxcut/runner.py --n 6 --seed 7 -o /tmp/mc.json
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of nodes in the complete graph |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/number_partition/README.md`)

# Number Partitioning

Given N positive integers, find a way to split them into two subsets of
equal sum (or as close as possible if an exact split does not exist).

## QUBO formulation

- **Input**: N positive integers `a_i`
- **Model**: N binary variables. `x_i = 1` puts number `a_i` in subset A.
- **Objective**: minimise `P * (sum(a_i * x_i) - S/2)^2` where `S = sum(a_i)`

An exact partition exists when S is even and the penalty evaluates to zero.
The QUBO minimiser finds the balanced partition when one exists, or the
most balanced split when the total is odd.

## DSL methods used

- `problem.vec()` -- allocate untyped vector registers for indices and coefficients
- `model.apply_equality(indices, coeffs, target, penalty)` -- EQUALITY constraint

## Pipeline overview

1. **CP** (`xqcp`) -- generate random positive integers, declare binary variables (one per number), and encode the half-sum equality constraint via EQUALITY.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that the partition constraint holds, then computes energy
6. **Decode** -- decoder extracts the subset assignment

## Usage

```sh
uv run python examples/number_partition/runner.py --seed 42
uv run python examples/number_partition/runner.py --n 8 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `6` | Number of integers |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/portfolio_opt/README.md`)

# Portfolio Optimization

Select a portfolio of exactly B assets from N candidates to maximise expected
return while penalising higher-order risk cross-interactions.

## QUBO formulation

- **Input**: N asset returns, cubic risk interactions `(i, j, k, sigma)`, budget B
- **Model**: N binary variables. `x_i = 1` if asset i is selected.
- **Objective**: `-sum(r_i * x_i) + sum(sigma_ijk * x_i * x_j * x_k)` -- first term maximises return (minimising its negation), second penalises correlated three-asset risk interactions.
- **Constraints**: budget `sum(x_i) = B` (EQUALITY with unit coefficients, penalty 200)

### Encoding strategy

Return terms are linear: `ADDLINE(i, -r_i)` per asset.

Cubic risk terms (i, j, k, sigma) are degree-reduced:

1. `REDUCE(i, j, P_AUX) -> w` (Rosenberg enforcement for `w = x_i * x_j`)
2. `ADDQUAD(w, k, sigma)` (`sigma * w * x_k = sigma * x_i * x_j * x_k`)

Budget constraint builds uniform-coefficient index/coeff vecs then calls
EQUALITY with `target = B` and `penalty = 200`. EQUALITY is emitted after all
objective (body) actions because it lands in the constraint section.

## DSL methods used

- `model.reduce(var_a, var_b, p_aux)` -- HOBO degree reduction for cubic risk terms
- `problem.vec()` -- allocate index/coefficient vecs for the budget constraint
- `model.apply_equality(indices, coeffs, target, penalty)` -- budget EQUALITY

## Pipeline overview

1. **CP** (`xqcp`) -- generate random returns and cubic risk interactions, declare binary variables, degree-reduce risk terms via REDUCE, and add a budget EQUALITY constraint.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary, that the budget constraint holds, and that each REDUCE auxiliary equals the product it stands for, then computes energy
6. **Decode** -- decoder extracts the selected assets

## Usage

```sh
uv run python examples/portfolio_opt/runner.py --seed 42
uv run python examples/portfolio_opt/runner.py --n 6 --budget 3 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of assets |
| `--budget` | `2` | Number of assets to select |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/portfolio_rebalance/README.md`)

# Portfolio Rebalance

Choose a signed integer weight for each asset, trading expected return off
against a risk matrix, subject to the weights summing to a budget. Negative
weights are short positions, which is the whole reason this example is not a
binary select-or-not model.

## QUBO formulation

- **Input**: number of assets N, a return per asset, a symmetric N x N risk matrix flattened row-major
- **Model**: N integer variables `x_i` in `[-5, 5]`, declared with `lo=`/`hi=`. The model holds `y_i = x_i - lo` in `{0, ..., 10}`; coefficients are written over `x` and `sample.value()` shifts back on the way out.
- **Objective**: `-sum(r_i * x_i) + sum_{i <= j} C_ij * x_i * x_j`
- **Constraint**: `sum(x_i) = B`, as the penalty `P * (sum_i x_i - B)^2`
- **Energy**: shifted twice over, and not comparable with any other example's. The ranged domain drops the constant that substituting `x = y + lo` produces, because XQMX carries no offset field, and the budget square drops its own `P*B^2` the way EQUALITY does. Both shifts are uniform across assignments, so argmin is exact even though the number is not the objective's true value.

### Encoding strategy

The budget constraint is written out by hand rather than handed to
`apply_equality`. Every high-level constraint in the VM expands under
`x^2 = x`, which holds for binary variables only, so xqcp refuses all of them
off a binary model. Expanded, `P * (sum_i x_i - B)^2` is `P` on each diagonal,
`2P` on each off-diagonal pair, and `-2PB` on each linear coefficient. Writing
that square is the same work the HLF would have done, and it is what the open
question about per-domain expansions is about.

The ranged domain then does its own rewriting underneath. Each quadratic write
records `w*lo` against the linear coefficient of both named indices, because
`w * x_i * x_j` expands to `w*y_i*y_j + w*lo*y_i + w*lo*y_j + w*lo^2` once
`x = y + lo` is substituted. A write to the diagonal lands both corrections on
the one index, giving the `2*w*lo` that squaring asks for. None of that is
visible in the model-building code above.

`lo` and `hi` are literals. A runtime `lo` would want the decoder's single
calldata scalar, which the output loop bound already spends, and xqcp raises
naming both rather than picking one.

## DSL methods used

- `problem.define_model(size=N, domain=Domain.INTEGER, lo=-5, hi=5)` -- ranged integer weights
- `model.linear[i].add(w)` and `model.quadratic[i, j].add(w)` -- the only operations a non-binary model supports
- `sample.value(i)` -- the weight in the domain it was declared over, rather than the stored `y`

## What this example proves

`valid == 1` proves that the domain check, the record-layer shift and the
decode compose on a chosen assignment. It proves nothing about optimisation:
no solver samples an integer model yet, so `--solver` is accepted, ignored,
and the pipeline runs against a hand-picked weight vector that sits inside the
domain and sums to the budget. Integer lowering is XQSA v0.5.0 work.

## Pipeline overview

1. **CP** (`xqcp`) -- generate returns and a symmetric risk matrix, declare N ranged integer weights, and write the objective and the budget square as coefficients
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- skipped; the runner supplies the assignment itself
5. **Verify** -- verifier checks every weight is inside `{0, ..., 10}` against the `k` replayed from `define_model`, then computes energy
6. **Decode** -- decoder reads each weight and adds `lo`, giving the signed weights back

## Usage

```sh
uv run python examples/portfolio_rebalance/runner.py --seed 42
uv run python examples/portfolio_rebalance/runner.py --n 6 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of assets |
| `--solver` | `dwave-cpu` | Accepted and ignored |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/quartic_opt/README.md`)

# Quartic Optimization

Minimise a degree-4 pseudo-Boolean objective via two-stage REDUCE chaining.

## QUBO formulation

- **Input**: N binary variables, M quartic interaction terms `(i, j, k, l, c)`
- **Model**: N binary variables. Linear bias `-1` per variable rewards selection, creating tension with the positive quartic terms.
- **Objective**: `sum(c_t * x_i * x_j * x_k * x_l) - sum(x_v)`

Each quartic term (i, j, k, l, c) is encoded via two-stage REDUCE:

1. `w = REDUCE(i, j, P_AUX)` -- introduces auxiliary w; w approximates `x_i*x_j`.
2. `v = REDUCE(w, k, P_AUX)` -- introduces auxiliary v; v approximates `w*x_k = x_i*x_j*x_k`. Here w is the variable index returned from the first REDUCE.
3. `ADDQUAD(v, l, c)` -- adds `c*v*x_l = c*x_i*x_j*x_k*x_l` to the QUBO.

Each quartic term allocates 2 auxiliary variables. With M terms, the model
grows by 2*M variables beyond the original N.

## DSL methods used

- `model.reduce(var_a, var_b, p_aux)` -- two chained HOBO degree reductions;
  the RegLoad returned by the first REDUCE is passed as var_a to the second

## Pipeline overview

1. **CP** (`xqcp`) -- generate random quartic interaction terms, declare binary variables with linear bias, and two-stage degree-reduce each quartic term via chained REDUCE.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that each Rosenberg REDUCE auxiliary equals the product it stands for, then computes energy
6. **Decode** -- decoder extracts the variable assignment

## Usage

```sh
uv run python examples/quartic_opt/runner.py --seed 42
uv run python examples/quartic_opt/runner.py --n 6 --m 3 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of variables |
| `--m` | `2` | Number of quartic terms |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/set_cover/README.md`)

# Set Cover

Given a universe of E elements and a collection of S sets, find the minimum
sub-collection whose union equals the universe.

## QUBO formulation

- **Input**: number of elements E, number of sets S, coverage membership matrix (flat Vec of E*S entries, `covers[e][s] = 1` if set s covers element e)
- **Model**: S binary variables. `x_s = 1` if set s is selected.
- **Objective**: minimise `sum(x_s)`
- **Constraints**: per element e: `sum_{s: covers[e][s]=1} x_s >= 1` (ATLEAST with k=1)

For each element, the encoder iterates over all sets and uses a branch to
conditionally push only covering set indices into the element's index vector.
ATLEAST then enforces that at least one covering set is selected.

## DSL methods used

- `problem.vec()` -- allocate a vector register for each element's covering set indices
- `problem.branch(cond, arm, default)` -- conditional VECPUSH based on coverage membership
- `model.apply_atleast(indices, k, penalty)` -- ATLEAST constraint with k=1

## Pipeline overview

1. **CP** (`xqcp`) -- generate a random coverage matrix, declare binary variables (one per set), and encode per-element coverage constraints via conditional branching and ATLEAST.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that every element is covered, then computes energy
6. **Decode** -- decoder extracts the selected sets

## Usage

```sh
uv run python examples/set_cover/runner.py --seed 42
uv run python examples/set_cover/runner.py --num-sets 6 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--num-elements` | `4` | Number of elements in the universe |
| `--num-sets` | `5` | Number of sets |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/tsp/README.md`)

# TSP -- Travelling Salesman Problem

Find the shortest Hamiltonian tour through N cities given a random symmetric
distance matrix.

## QUBO formulation

- **Input**: `num_cities` (int), `distance_matrix` (Vec, flat upper triangle, `n*(n-1)/2` entries)
- **Model**: an `n x n` binary grid. `x[i, p] = 1` means city `i` is at tour position `p`.
- **Objective**: sum of distances between consecutive positions in the tour.
- **Constraints**: one-hot row (each city at exactly one position) and one-hot column (each position holds exactly one city), both with penalty 100.

## Hamiltonian

The Hamiltonian is the sum of three terms:

$$H = H_{\text{dist}} + H_{\text{row}} + H_{\text{col}}$$

\\(H_{\text{dist}}\\) is the tour length. For every pair of cities \\(i < j\\) and
every position \\(p\\), a term fires whichever direction the tour visits them
in -- city `i` at `p` and city `j` at the next position, or the reverse:

$$H_{\text{dist}} = \sum_{i<j} \sum_{p=0}^{n-1} d_{ij} \left( x_{i,p}\, x_{j,(p+1) \bmod n} + x_{j,p}\, x_{i,(p+1) \bmod n} \right)$$

\\(H_{\text{row}}\\) and \\(H_{\text{col}}\\) are the one-hot constraints
`apply_onehot_row` and `apply_onehot_col` add, at penalty \\(P = 100\\):

$$H_{\text{row}} = P \sum_{i=0}^{n-1} \left( \sum_{p=0}^{n-1} x_{i,p} - 1 \right)^2, \qquad H_{\text{col}} = P \sum_{p=0}^{n-1} \left( \sum_{i=0}^{n-1} x_{i,p} - 1 \right)^2$$

XQMX has no field for a constant term. Each satisfied one-hot constraint
drops its `+P` and leaves `-P` in the stored model instead of `0` -- see
[Constraints](../../docs/book/src/modelling/constraints.md#why-the-reported-energy-is-not-just--total_value).

\\(H_{\text{row}}\\) forces each city onto exactly one position; \\(H_{\text{col}}\\)
forces each position to hold exactly one city. Both terms are zero *as
written* on a valid tour, so they never change which valid tour is
shortest -- but because of the dropped constant above, what a satisfied
constraint actually contributes is `-P`, not `0`. That is the
`-2 * P * n = -800` term in the worked example below. A violation (a
city visiting two positions, or a position holding two cities) still
costs more than a satisfied constraint.

Whether that makes every invalid grid's energy exceed every valid
tour's depends on \\(P\\) relative to the tour-length difference a
violation can buy back.
[Constraints](../../docs/book/src/modelling/constraints.md#choosing-a-penalty-weight)
gives the general criterion. At \\(P = 100\\) it holds for this
seed-42, 4-city instance. The best invalid grid scores `-581`, `6`
above the worst valid tour's `-587`. It does not hold at every instance
where \\(P = 100\\) is used. At `--n 5 --seed 7`, the second command
under Usage below, an invalid grid scores `-770`, below `40` of the
`120` valid tours.

## DSL methods used

- `problem.input()` -- declare typed calldata inputs
- `problem.define_model()` -- allocate binary 2D grid XQMX model
- `problem.stow()` -- bind intermediate computations to named registers
- `problem.range()` -- emit RANGE loops
- `model.quadratic[(city_i, pos), (city_j, pos)].add()` -- accumulate quadratic coupling using 2D grid coordinates
- `model.apply_onehot_row()` -- ONEHOTR constraint per city
- `model.apply_onehot_col()` -- ONEHOTC constraint per position
- `problem.output()` -- declare typed output slots
- `problem.sample.colfind()` -- find the row index with value 1 in a given column

## Pipeline overview

1. **CP** (`xqcp`) -- build a random symmetric distance matrix, declare an `n x n` binary grid, and add quadratic distance terms plus one-hot row/column constraints.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and the one-hot row and column constraints, then computes energy
6. **Decode** -- decoder extracts the tour as a sequence of city indices

Steps 3-6 are three independent XQVM programs plus a solver call in between,
sharing data only through calldata and outputs:

```mermaid
flowchart LR
    ENC[Encoder] -->|model| SLV[xqsa solver]
    ENC -->|model| VER[Verifier]
    SLV -->|sample| VER
    SLV -->|sample| DEC[Decoder]
    VER -->|energy, valid| OUT[Host program]
    DEC -->|tour| OUT
```

The encoder's calldata is `num_cities, distance_matrix`; the verifier's is
`model, sample, num_cities`; the decoder's is `sample, num_cities`. See
[Three Programs](../../docs/book/src/concepts/three-programs.md) for why the
split exists.

## Worked example

`uv run python examples/tsp/runner.py --n 4 --seed 42` builds this
4-city instance (edge labels are `distance_matrix`, indexed by the
upper-triangle position of each city pair):

```mermaid
graph LR
    0 ---|82| 1
    0 ---|15| 2
    0 ---|95| 3
    1 ---|4| 2
    1 ---|36| 3
    2 ---|32| 3
```

Both interpreters return the same tour:

```json
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "energy": -650,
  "n": 4,
  "tour": [0, 2, 1, 3],
  "tour_distance": 150,
  "valid": 1
}
```

`tour_distance` is `d(0,2) + d(2,1) + d(1,3) + d(3,0) = 15 + 4 + 36 + 95 =
150`, matching \\(H_{\text{dist}}\\) for a valid tour. `energy` is
`H_{\text{dist}} + H_{\text{row}} + H_{\text{col}}`: on a valid tour, each
of the 4 rows and 4 columns contributes its one active variable's `-P`
linear term and nothing from the quadratic term (no two variables in the
same row or column are both 1), for `-P * n` per constraint family, so
`energy = tour_distance - 2 * P * n = 150 - 2 * 100 * 4 = -650`, matching
the run.

## Usage

```sh
uv run python examples/tsp/runner.py --seed 42
uv run python examples/tsp/runner.py --n 5 --seed 7 -o /tmp/tsp.json
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `4` | Number of cities |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/vertex_cover/README.md`)

# Vertex Cover

Find the minimum subset of vertices such that every edge in an undirected
graph has at least one endpoint in the subset.

## QUBO formulation

- **Input**: number of nodes N, edge list
- **Model**: N binary variables. `x_v = 1` if vertex v is in the cover.
- **Objective**: minimise `sum(x_v)`
- **Constraints**: per edge (i,j): `x_i + x_j >= 1` (ATLEAST with k=1)

The at-least-1 constraint is encoded directly with ATLEAST. For each edge,
ATLEAST allocates one slack variable at model.size and adds the penalty
`P*(x_i + x_j - 1 - s)^2`, where `s in {0,1}` accounts for the case when
both endpoints are selected (sum = 2).

## DSL methods used

- `problem.vec()` -- allocate a vector register for the two endpoint indices per edge
- `model.apply_atleast(indices, k, penalty)` -- ATLEAST constraint with k=1

## Pipeline overview

1. **CP** (`xqcp`) -- generate a random graph, declare binary variables (one per vertex), and encode per-edge coverage constraints via ATLEAST.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that every edge is covered, then computes energy
6. **Decode** -- decoder extracts the selected vertices

## Usage

```sh
uv run python examples/vertex_cover/runner.py --seed 42
uv run python examples/vertex_cover/runner.py --n 7 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--n` | `5` | Number of nodes |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`examples/weighted_set_cover/README.md`)

# Weighted Set Cover

A generalisation of Set Cover where each set s has a coverage capacity cap[s]
and each element e has a demand demand[e]. The goal is to select sets of
minimum total cost such that the total capacity of covering selected sets
meets each element's demand.

## QUBO formulation

- **Input**: number of elements E, number of sets S, set costs, set capacities, element demands, coverage membership matrix
- **Model**: S binary variables. `x_s = 1` if set s is selected.
- **Objective**: minimise `sum(cost[s] * x_s)`
- **Constraints**: per element e: `sum_{s: covers[e][s]=1} cap[s] * x_s >= demand[e]` (ATLEASTW)

For each element, a branch conditionally pushes (set index, capacity) pairs
into per-element index/coefficient vectors, then ATLEASTW enforces the
weighted threshold.

## DSL methods used

- `problem.vec()` -- allocate vector registers for covering set indices and capacities
- `problem.branch(cond, arm, default)` -- conditional VECPUSH based on coverage membership
- `model.apply_atleastw(indices, coeffs, k, penalty)` -- ATLEASTW constraint

## Pipeline overview

1. **CP** (`xqcp`) -- generate a random weighted coverage instance, declare binary variables (one per set), and encode per-element weighted demand constraints via conditional branching and ATLEASTW.
2. **Assemble** -- `.xqasm` text to bytecode via `xquad.asm`
3. **Encode** -- run encoder on chosen XQVM to produce the XQMX model
4. **Sample** -- solver runs SA/QPU/GPU over the model
5. **Verify** -- verifier checks the sample is binary and that every demand is met, then computes energy
6. **Decode** -- decoder extracts the selected sets

## Usage

```sh
uv run python examples/weighted_set_cover/runner.py --seed 42
uv run python examples/weighted_set_cover/runner.py --num-sets 6 --interpreter rust
```

| Flag | Default | Description |
|------|---------|-------------|
| `--num-elements` | `4` | Number of elements in the universe |
| `--num-sets` | `5` | Number of sets |
| `--solver` | `dwave-cpu` | Solver backend (see Choosing a solver) |
| `--interpreter` | `python` | XQVM backend: `python` or `rust` |
| `--seed` | `42` | Random seed |
| `-o` | stdout | Write JSON result to file |

## Choosing a solver

| Name | Hardware | Install |
|------|----------|---------|
| `dwave-cpu` | CPU (default) | `pip install xquad` |
| `dwave-qpu` | D-Wave Leap account | `pip install xquad[dwave]` |
| `cuda-gpu` | NVIDIA CUDA GPU | `pip install xquad[cuda]` |
| `metal-gpu` | Apple Silicon (macOS) | `pip install xquad[metal]` |
| `quip` | Quip Network (remote miner, env-configured) | `pip install xqsa[quip]` |

See [GPU/QPU installation](../../README.md#gpuqpu-support) for driver
prerequisites and [xqsa solver quick-starts](../../xqsa/README.md) for
per-solver parameter tuning.

Non-default solvers will not reproduce the canonical output (different
RNG/hardware). `example-smoke` always runs `dwave-cpu`.

## Canonical output

`example-smoke` validates both interpreters produce `valid == 1` with
`--seed 42 --solver dwave-cpu`. The smoke test is invariant-based --
it checks validity, not exact output.

---

### Overview (`spec/xqcp/README.md`)

# XQCP Specification

Authoritative specification for X-Quadratic Constraint Programming. The spec is split across focused documents; each is the single source of truth for its topic.

## Documents

| File | Content |
|------|---------|
| [SPEC.md](SPEC.md) | DSL overview, three-program architecture, problem lifecycle, compilation contract |
| [TYPES.md](TYPES.md) | Symbolic value types, expression tree, operator algebra, free functions |
| [CONSTRAINTS.md](CONSTRAINTS.md) | Constraint taxonomy, method signatures, HLF expansion cross-references |
| [COMPILER.md](COMPILER.md) | Compilation pipeline: encoder/verifier/decoder generation, register allocation, action recording |

## Reference implementation

[`../../xqcp/`](../../xqcp/) -- Python package. The spec is reverse-engineered from this reference; once landed, the spec is authoritative and any divergence in the reference is a bug.

## Related specifications

- [XQVM](../xqvm/README.md) -- the virtual machine that executes the compiled programs
- [XQSA](../xqsa/README.md) -- solver adapters that sit between the encoder and verifier in the pipeline

---

### Overview (`spec/xqsa/README.md`)

# XQSA Specification

Authoritative specification for X-Quadratic Solver Adapters. The spec is split across focused documents; each is the single source of truth for its topic.

## Documents

| File | Content |
|------|---------|
| [SPEC.md](SPEC.md) | Architecture overview, pipeline position, plugin model |
| [INTERFACE.md](INTERFACE.md) | `Solver` abstract class, `solve()` contract, `SolverResult` type, parameter conventions |
| [ENERGY.md](ENERGY.md) | Energy computation formula, precision contract, sparse representation |
| [DOMAINS.md](DOMAINS.md) | Domain support matrix, sample encoding, grid metadata, capability negotiation |
| [SOLVERS.md](SOLVERS.md) | Solver registry, naming convention, algorithm families, dependency model |

## Reference implementation

[`../../xqsa/`](../../xqsa/) -- Python package. v0.3.0 implements `SolverDWaveCPU` wrapping `dwave-samplers` simulated annealing.

## Related specifications

- [XQVM](../xqvm/README.md) -- the virtual machine that executes encoder, verifier, and decoder programs around the solver
- [XQCP](../xqcp/README.md) -- the constraint-programming DSL that compiles problems into XQVM programs and XQMX models

---

### Overview (`spec/xqvm/README.md`)

# XQVM Specification

Authoritative specification for the X-Quadratic Virtual Machine. The spec is split across focused documents; each is the single source of truth for its topic.

## Documents

| File | Content |
|------|---------|
| [SPEC.md](SPEC.md) | Machine overview, three-program architecture, state model, determinism, type system, runtime limits |
| [ISA.md](ISA.md) | Instruction set architecture: notation, opcode tables by category, semantic notes, reserved opcodes |
| [HLF.md](HLF.md) | High-level function expansion formulas and derivations (QUBO penalty terms for combinatorial constraints) |
| [ENCODING.md](ENCODING.md) | File formats (`.xqasm` text, `.xqb` bytecode), assembly syntax, binary bytecode encoding |
| [VERIFIER.md](VERIFIER.md) | Bytecode verification: phase pipeline, error semantics, composable architecture, per-opcode stack effects |
| [METERING.md](METERING.md) | Step metering: what a step is, the cost constants, the per-opcode charge table, the formulas, and conformance |

---

### Overview (`docs/book/src/README.md`)

# Introduction

XQuad is a hardware-agnostic toolchain for expressing and running quadratic
optimisation problems -- QUBO, Ising, and integer formulations -- on quantum
annealers and classical solvers. A problem is written once against the XQVM
instruction set and runs unchanged on any backend the toolchain supports.

Think of it as **LLVM for quantum computing**: write a problem once, compile
it to XQVM bytecode, and run it on any supported backend.

## What XQuad solves

Combinatorial optimisation problems -- travelling salesman, graph colouring,
knapsack, set cover, and their relatives -- can be expressed as quadratic
binary models and handed to a quantum annealer or a classical sampler. XQuad
gives that pipeline a single intermediate representation, XQVM bytecode, and
a reference virtual machine, so the same compiled program runs on a D-Wave
QPU, a local simulated-annealing sampler, or the Quip network without a
rewrite. See [Quadratic Models](concepts/quadratic-models.md) for what a
QUBO/Ising model actually is, and [Backends](concepts/backends.md) for the
solvers XQuad targets.

## The real components

The toolchain is dual-language: a Rust core (VM, assembler, bytecode, CLI)
with Python interfaces (reference VM, constraint-programming DSL, solver
adapters, FFI bindings).

Three Rust crates, published to crates.io:

| Crate | Binary | Role |
|---|---|---|
| [`xqvm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm) | -- | Bytecode definitions, opcode table, instruction builder, binary codec, an incremental instruction-stream reader, the VM interpreter, and a disassembler |
| [`xqasm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqasm) | -- | Text assembler: `.xqasm` source to bytecode |
| [`xqcli`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcli) | `xquad` | The unified CLI -- `xquad asm`, `xquad dism`, `xquad run`, `xquad verify` |

A fourth crate, [`xqffi`](https://gitlab.com/quip.network/xquad/-/tree/main/xqffi),
is a PyO3 bridge that exposes `xqvm` and `xqasm` to Python. It ships as a
Python wheel rather than a crates.io library.

Five Python distributions, published to PyPI:

| Package | Role |
|---|---|
| [`xqvm_py`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm_py) | Pure-Python reference VM, used as the cross-implementation conformance oracle |
| [`xqcp`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcp) | High-level constraint-programming DSL that compiles to XQVM assembly |
| [`xqsa`](https://gitlab.com/quip.network/xquad/-/tree/main/xqsa) | Solver adapters for XQMX models: local simulated annealing, D-Wave, and the Quip network |
| [`xqffi`](https://gitlab.com/quip.network/xquad/-/tree/main/xqffi) | The PyO3 FFI bindings crate above, packaged as a wheel |
| [`xquad`](https://gitlab.com/quip.network/xquad/-/tree/main/xquad) | Umbrella package re-exporting `xqffi`, `xqcp`, and `xqsa` under one namespace, with an interactive `Program` / `Session` / `RunResult` API |

Parity on every committed conformance vector between the Rust `xqvm`
interpreter and the Python `xqvm_py` reference VM is enforced mechanically:
each vector runs on both implementations in CI, and disagreement fails the
build. See [Conformance](embedding/conformance.md) for what a vector does
and does not cover.

## Architecture at a glance

XQVM is a stack-based interpreter with a 256-slot register file. The value
stack holds `i64` integers; registers hold typed values (`RegVal`): integers,
integer vectors, QUBO/Ising/integer models (`XqmxModel`), model vectors, and
candidate solutions (`XqmxSample`). A dedicated loop stack drives `RANGE` and
`ITER` iteration.

The instruction set comprises 93 instructions, declared once in the
`opcodes!` table at
[`xqvm/src/bytecode/types/table.rs`](https://gitlab.com/quip.network/xquad/-/blob/main/xqvm/src/bytecode/types/table.rs).
The `Opcode` enum, the `Instruction` enum, mnemonic strings, and operand
arity are all derived from that single table.

Every `.xqb` file opens with a fixed 15-byte XQBC header -- magic bytes,
format version, calldata/output-slot counts, instruction-stream length, and
a CRC-32 checksum -- followed by the raw instruction stream: an opcode byte
followed by its operands in big-endian byte order. See
[Bytecode Format](xqvm/bytecode-format.md) for the full field layout.

## Where to go next

- **[Getting Started](start/)** -- install the toolchain and run
  your first program.
- **[Concepts](concepts/)** -- what a QUBO/Ising model is, the ways
  to use XQuad, and the backends it targets.
- **[Modelling with XQCP](modelling/)** -- write a problem with the
  constraint-programming DSL.
- **[Running Programs](running/)** and
  **[Solving](solving/)** -- execute a compiled program and hand it
  to a solver.
- **[XQVM Reference](xqvm/)** -- the machine model, assembly
  language, instruction set, and bytecode format.
- **[Embedding](embedding/)** -- using the Rust crates directly,
  `no_std` support, and cross-implementation conformance.
- **[Examples](examples/)** -- worked problems including a
  Travelling Salesman Problem.

## License

Licensed under the
[GNU Affero General Public License v3.0 or later](https://www.gnu.org/licenses/agpl-3.0.html).

---

### Overview (`docs/book/src/concepts/README.md`)

# Toolchain Map

XQuad turns one problem description into bytecode that runs unchanged on
any backend it supports. This page names the pieces that make that true,
and what each one consumes and produces.

```mermaid
flowchart LR
    P["DSL (xqcp) or hand-written .xqasm"] --> ASM[Assembler]
    ASM --> B[XQVM bytecode]
    B --> V["VM: xqvm or xqvm_py"]
    H[Host program] -->|calldata| V
    V -->|model-building program| M[XQMX model]
    M --> H
    H --> SLV[xqsa solver]
    SLV -->|sample| H
    H -->|sample as calldata| V
    V -->|verifier / decoder programs| H
```

| Component | Role | Hands off |
|---|---|---|
| DSL (`xqcp`) or `.xqasm` source | Describes the problem: a program whose job is to build a model, in Python or in assembly text | `.xqasm` source |
| Assembler (`xqasm`) | Parses `.xqasm` text and resolves labels | XQVM bytecode (`.xqb`) |
| Bytecode | The portable artifact: identical bytes run on the Rust `xqvm` interpreter or the Python `xqvm_py` reference VM | Input to the VM |
| VM (`xqvm` / `xqvm_py`) | Executes bytecode against [calldata](../xqvm/io.md) the host program supplies | An `XqmxModel`, when the program's job is building one; decoded output, when its job is reading a sample back |
| Model (`XqmxModel`) | The Hamiltonian a model-building program assembled: the model's energy function, covered in [Quadratic Models](quadratic-models.md) | Input to a solver |
| Solver (`xqsa`) | Minimises the model's Hamiltonian | An `XqmxSample`: the best assignment found |
| Host program | The script or CLI session driving every stage above | Reads the model out of the VM and hands it to the solver, then feeds the sample back in as calldata (the numbered input slots a host fills before each VM run) for the next run |

## Following the Pipeline

A hand-written program can skip most of this. Save the following as
`add.xqasm`:

```asm
; push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
```

```sh
$ xquad asm add.xqasm -o add.xqb
assembled 4 instructions (21 bytes) -> add.xqb
$ xquad run add.xqb
stack (bottom to top):
  42
```

Here the pipeline is just two rows: the assembler and the VM. The `xquad`
CLI plays the host program's role, reading `add.xqb` from disk and
printing the result, and that already runs the real toolchain end to end.
[XQVM Reference](../xqvm/) walks through this same `add.xqasm`
again, from the machine's side: what each instruction does to the stack as
it runs.

A problem worth handing to a solver uses every row in the table above.
`xqcp` compiles a `Problem` into three separate `.xqasm` programs, not one
-- see [Three Programs](three-programs.md) for what those three are and
why. The first one builds a model; `xqsa` solves it; the other two check
and decode the answer. A Python host program -- `xquad.program`,
`xqffi.vm`, or a full `xqcp` pipeline -- drives it, injecting calldata
before each VM run and reading outputs back after. See
[Ways to Use XQuad](ways-to-use.md) for which surface plays that host-program
role in each of the six ways to work with XQuad.

## Where to Go From Here

- **[Ways to Use XQuad](ways-to-use.md)** -- the six surfaces that build a
  model and drive execution, and how to pick one.
- **[Quadratic Models](quadratic-models.md)** -- what the model in the
  middle of this pipeline actually is, and why constraints become part of
  it.
- **[Three Programs](three-programs.md)** -- why `xqcp` compiles a problem
  into three separate programs instead of one.
- **[Backends](backends.md)** -- the solvers that sit in the gap between
  encoder and verifier.

Past this chapter: [Modelling](../modelling/) builds a model with
the DSL, [Running Programs](../running/) and
[Solving Overview](../solving/) execute it and hand it to a
solver, and [XQVM Reference](../xqvm/) is the machine this whole
pipeline compiles down to.

---

### Overview (`docs/book/src/cookbook/README.md`)

# Cookbook

The fourteen examples under `examples/` each solve one problem --
Knapsack, TSP, Graph Coloring -- and each already contains a working
encoding. Nothing in the repository names those encodings as patterns you
can reuse on a problem that is not itself Knapsack or TSP. This chapter
names five recurring shapes instead, each grounded in the example or
examples that embody it and shown as code that actually ran. One further
page covers getting the arithmetic right once a shape is chosen.

This chapter assumes [Modelling](../modelling/) -- it builds
problems with the same `xqcp` calls that chapter introduces and does not
re-explain them. It also assumes
[Quadratic Models](../concepts/quadratic-models.md) for what a penalty
term is and why one exists at all.

## The Five Shapes

| Page | Reach for it when | Canonical example |
| --- | --- | --- |
| [Permutations](permutations.md) | N things need a full ordering: each thing gets exactly one position and each position gets exactly one thing | `examples/tsp/` |
| [Assignment](assignment.md) | N things each pick one of B slots, with no requirement that every slot is used or that N equals B | `examples/bin_packing/` |
| [Selection Under Budget](selection-under-budget.md) | Choose a subset of weighted things without a total exceeding a fixed capacity | `examples/knapsack/` |
| [Mutual Exclusion](mutual-exclusion.md) | Two choices conflict, or one requires another | `examples/graph_coloring/`, `examples/max_independent_set/` |
| [Soft vs. Hard Constraints](soft-vs-hard-constraints.md) | A rule that must hold sits next to a preference that should hold | `examples/portfolio_opt/`, `examples/max3sat/` |

Permutations and Assignment are the same grid with one constraint family
removed -- see [Assignment](assignment.md#failure-mode)
for the direction that mistake most often runs. Selection Under Budget and
Mutual Exclusion overlap at one boundary: a capacity of exactly `1` on a
pair is the same instruction family `EXCLUDE` gives you directly, covered
in [Mutual Exclusion](mutual-exclusion.md#the-same-rule-encoded-without-exclude).
`examples/bin_packing/` and `examples/graph_coloring/` each compose two of
these shapes in one problem (assignment plus a capacity, and assignment
plus exclusion, respectively) -- reading a problem as "which of these
shapes does each part look like" scales to a combined problem the same
way it does to a single-pattern one.

## The Arithmetic Page

[Integer Scaling](integer-scaling.md) is not a shape -- every pattern
above needs it, sooner or later, while turning real-valued problem data
into the integer coefficients every pattern above is written in terms of.
Sizing the penalty weight itself, and reading a solver's output to tell
whether a cheap constraint violation bought a good-looking energy, is
[Constraints](../modelling/constraints.md#enumerate-the-failure-not-the-intuition)'s
territory, not a cookbook page of its own.

## Where This Chapter Stops

None of these pages re-derives what a one-hot, `EXCLUDE`, or `EQUALITY`
constraint expands to in linear and quadratic coefficients -- that table
is [High-Level Constraints](../xqvm/instructions/constraints.md), already
written, and this chapter links to it rather than repeating it. This
chapter is about recognising which instruction family a new problem
needs and what it costs once chosen, not about the instructions
themselves.

---

### Overview (`docs/book/src/embedding/README.md`)

# Embedding Overview

Everything so far in this book drives XQuad from Python or the `xquad` CLI.
This chapter is for embedding the Rust crates directly. That means building
bytecode with a fluent Rust API instead of `.xqasm` text, running it without
a Python process in the loop, and compiling for `no_std` targets.

## Crate Map

| Crate | Role | `no_std` |
|---|---|---|
| [`xqvm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqvm) | Opcode table, `InstructionBuilder`, the wire codec, and the `Vm` interpreter | Yes, with `--no-default-features` |
| [`xqasm`](https://gitlab.com/quip.network/xquad/-/tree/main/xqasm) | Parses `.xqasm` text into an `xqvm::Program` | No |
| [`xqcli`](https://gitlab.com/quip.network/xquad/-/tree/main/xqcli) | Builds the `xquad` CLI binary on top of the two crates above | No |

A Rust program that only needs to build and run bytecode depends on `xqvm`
alone; add `xqasm` to parse `.xqasm` source instead of building bytecode by
hand with [`InstructionBuilder`](builder-api.md). `xqcli` is the CLI's own
crate, useful as a reference for wiring the other two together rather than
as a library dependency.

## The `no_std` Story

`xqvm` builds `no_std + alloc`. Two layers are always compiled without
`std`:

- **Bytecode** -- the opcode table, `Instruction`, `Register`,
  `InstructionBuilder`, `Program`, and the wire codec.
- **Interpreter core** -- `Vm`, `Error`, `RegVal`, `XqmxModel` /
  `XqmxSample`, the bytecode verifier, and the `Tracer` trait with its
  no-op implementation.

The `std` feature, enabled by default, adds three things a `no_std`
embedder gives up: the disassembler (`disasm::Disassembly`), `miette`-backed
runtime diagnostics (`RuntimeDiagnostic`), and the `JsonTracer` /
`TextTracer` implementations. None of these change what a program computes;
they change how a fault or a trace is reported. Build for a `no_std` target
with:

```sh
cargo add xqvm --no-default-features
```

This is exercised, not just claimed: `fixtures/xqvm-wasm` is a freestanding
`wasm32-unknown-unknown` test crate depending on `xqvm` with
`default-features = false`, and CI's `test:wasm` job runs `cargo build -p
xqvm --target wasm32v1-none --no-default-features` followed by
`wasm-pack test --node` against it. The job is path-gated: it runs on
merge requests and feature branches whenever the diff can reach `xqvm` or
the fixture, and unconditionally on protected refs and release tags.

## Running In-Process, Without `.xqasm`

[Builder API](builder-api.md) covers `InstructionBuilder` in full: emitting
instructions one at a time, resolving forward and backward labels, and
reading back the resulting `JumpTable`. It is the API a `no_std` embedder
uses when parsing text is not an option.

## On-Chain: The Pallet Fixture

[Pallet Fixture](pallet.md) documents `fixtures/pallet-xqvm`, a Substrate
FRAME pallet that embeds `xqvm` inside a runtime as an integration gate, not
a production deployment path. It is a real, CI-tested example of a `no_std`
embedder, and the one currently in this repository.

## On-Chain: Which Opcodes a Chain May Admit

[On-Chain Admissibility](admissibility.md) answers which of the 93 opcodes a
runtime embedding `xqvm` may accept from an untrusted account. The answer is
all of them, because chain compatibility is a property of the instruction set
rather than a per-embedder decision; the page states the bar an opcode has to
clear, where each half of it is enforced, what every family charges, and
which parts of that boundary the tree does not yet enforce.

## Checking an Implementation Against the Spec

[Conformance](conformance.md) covers the harness that holds the Rust `xqvm`
and the Python `xqvm_py` to the same observable behaviour. Read it if you
are embedding `xqvm` somewhere that needs to trust its output matches the
reference implementation.

---

### Overview (`docs/book/src/examples/README.md`)

# Examples

<a id="graph-problems"></a>

## Graph problems

Examples that encode graph cuts, colouring, covers, independent sets, and tours.

- [Max-Cut](maxcut.md) -- Find a 2-colour partition of a weighted graph that maximises the cut weight.
- [Graph Coloring](graph_coloring.md) -- Assign colours to graph nodes so adjacent nodes do not share a colour.
- [Maximum Independent Set](max_independent_set.md) -- Select the largest subset of graph nodes with no selected edge between them.
- [Vertex Cover](vertex_cover.md) -- Select the smallest vertex subset that covers every graph edge.
- [Travelling Salesman Problem](tsp.md) -- Find the shortest Hamiltonian tour through a random symmetric distance matrix.

<a id="selection-and-packing"></a>

## Selection and packing

Examples that select subsets, cover demands, pack bins, and balance integer weights.

- [Knapsack](knapsack.md) -- Select items that maximise value while respecting a capacity constraint.
- [Bin Packing](bin_packing.md) -- Pack items into the minimum number of fixed-capacity bins.
- [Set Cover](set_cover.md) -- Select the minimum set collection whose union covers the universe.
- [Weighted Set Cover](weighted_set_cover.md) -- Select sets with capacities to cover element demands at minimum cost.
- [Number Partition](number_partition.md) -- Split positive integers into two subsets with nearly equal sums.
- [Portfolio Optimization](portfolio_opt.md) -- Select a fixed-size portfolio while penalising higher-order risk interactions.
- [Portfolio Rebalance](portfolio_rebalance.md) -- Choose signed integer asset weights against a risk matrix and a budget.

<a id="satisfiability-and-higher-order"></a>

## Satisfiability and higher-order

Examples that reduce clauses and higher-order pseudo-Boolean objectives to quadratic models.

- [Max-3-SAT](max3sat.md) -- Find the assignment that satisfies the maximum number of 3-literal clauses.
- [Cubic Optimization](cubic_opt.md) -- Minimise a cubic pseudo-Boolean objective through HOBO degree reduction.
- [Quartic Optimization](quartic_opt.md) -- Minimise a degree-4 pseudo-Boolean objective through two-stage REDUCE chaining.

---

### Overview (`docs/book/src/modelling/README.md`)

# Modelling Lifecycle

XQCP is a Python-embedded DSL that turns a problem description into three
XQVM assembly programs. You declare a `Problem`'s runtime inputs, allocate
the quadratic model those inputs fill, add objective terms and
constraints, declare what you want back, then call `compile()`. XQCP
records every call you make against `Problem` as you make it and only
turns that recording into `.xqasm` when `compile()` runs -- see
[Compiling](compiling.md) for how the recording becomes three programs.

A problem definition follows one call sequence, in order:

```text
Problem(name) -> input()* -> define_model() -> body* -> output()* -> compile()
```

`input()` calls come first: [Inputs and Model Shape](inputs-and-model.md)
covers why, and what a declared input becomes at run time. `define_model()`
runs exactly once and makes `problem.model` available for the rest of the
body: objective terms, constraints, and loops, in any order, covered
across [Expressions](expressions.md), [Objectives](objectives.md),
[Constraints](constraints.md), and [Control Flow](control-flow.md). Your
first `output()` call marks the start of the decoder section, covered in
[Outputs and Decoding](outputs-and-decoding.md).

## Why XQCP, Not Assembly

[Ways to Use XQuad](../concepts/ways-to-use.md) names six surfaces for
getting a problem into XQuad; XQCP is one of them. Reach for it when you
are modelling a combinatorial problem from scratch and would otherwise
hand-write the same loop-over-variables, add-a-coefficient pattern in
`.xqasm` three times over, once per program -- `Problem` records that
pattern once, and the compiler emits all three. Write `.xqasm` directly
instead when a program is small enough that the three-program duplication
costs nothing to write by hand. Reach for
[`xqvm::InstructionBuilder`](../concepts/ways-to-use.md#rust-embedding)
instead when a Rust host is generating programs and adding a Python step
to emit them buys nothing.

## One Problem, Once

Every page in this chapter builds on the same running example:
`examples/knapsack/runner.py`. It takes item weights, item values, and a
capacity, and chooses the subset of items maximising total value without
exceeding the capacity. `build_problem` is the whole of it:

```python
def build_problem(n: int, weights: list[int], values: list[int], capacity: int) -> Problem:
    problem = Problem("Knapsack")

    num_items = problem.input("num_items", type=Types.Int)
    weights_in = problem.input("weights", type=Types.Vec)
    values_in = problem.input("values", type=Types.Vec)
    capacity_in = problem.input("capacity", type=Types.Int)

    problem.define_model(size=num_items, domain=Domain.BINARY)

    # Objective: minimise -sum(v_i * x_i)
    with problem.range(0, num_items) as i:
        vi = problem.stow("vi", values_in.get(i))
        problem.model.linear[i].add(-vi)

    # Constraint: sum(w_i * x_i) <= W
    indices = problem.vec()
    coeffs = problem.vec()
    with problem.range(0, num_items) as i:
        indices.push(i)
        coeffs.push(weights_in.get(i))

    problem.slack(indices, coeffs, num_items, capacity_in)
    problem.model.apply_equality(indices, coeffs, capacity_in, 100)

    selected = problem.output("selected", type=Types.Vec)
    with problem.range(0, num_items) as i:
        selected.append(problem.sample.getline(i))

    return problem
```

One binary variable per item: `x_i = 1` means item `i` is selected.
Four inputs, one 1D binary model sized to `num_items`, a range loop
building the objective, a second range loop paired with `slack` and
`apply_equality` encoding the capacity constraint, and a decoder loop
reading the solved sample back into a `selected` vector. Running it end
to end:

```sh
$ uv run python examples/knapsack/runner.py --seed 42
{
  "_note": "canonical CI golden",
  "_seed": 42,
  "capacity": 18,
  "energy": -32446,
  "n": 5,
  "selection": [
    1,
    1,
    1,
    0,
    1
  ],
  "total_value": 46,
  "total_weight": 12,
  "valid": 1,
  "values": [
    5,
    4,
    18,
    3,
    19
  ],
  "weights": [
    2,
    1,
    5,
    4,
    4
  ]
}
```

`_note` and `_seed` are the runner's own bookkeeping, the same as in
[Three Programs](../concepts/three-programs.md#a-concrete-run). `energy`
is the full model's Hamiltonian, capacity constraint included, so it is
not simply `-total_value` here the way Max-Cut's `energy` is `-cut_weight`
-- [Objectives](objectives.md) isolates just the objective term's
contribution, and [Constraints](constraints.md) covers what the rest of
`energy` is paying for.

This run is not the optimum: the five weights sum to `16`, under the
capacity of `18`, so every item fits and the true best value is `49`, not
the `46` printed above. A heuristic solver returns a good sample, not a
proven best one. `valid: 1` reports only the verifier's binary-domain
check here, not the capacity constraint; see
[Compiling](compiling.md#what-compile_verifier-and-compile_decoder-do) for
what a verifier actually checks.

[Inputs and Model Shape](inputs-and-model.md) starts with the four
`problem.input()` calls and the `define_model()` line above; the rest of
the chapter works through the remaining lines in the order they appear.

## Where to Go From Here

- **[Inputs and Model Shape](inputs-and-model.md)** -- runtime inputs and
  the 1D/2D model they fill.
- **[Expressions](expressions.md)** -- the operators and functions that
  build a coefficient or a loop bound.
- **[Objectives](objectives.md)** -- turning "minimise this" into
  `linear`/`quadratic` coefficients, sign included.
- **[Constraints](constraints.md)** -- folding a rule into the objective
  as a penalty.
- **[Control Flow](control-flow.md)** -- `range`, `iter`, and `branch`.
- **[Outputs and Decoding](outputs-and-decoding.md)** -- reading a solved
  sample back into your problem's own terms.
- **[Compiling](compiling.md)** -- what `problem.compile()` actually
  produces.

---

### Overview (`docs/book/src/running/README.md`)

# Running Programs

This page covers driving an already-compiled `.xqasm` or `.xqb` program from
Python: loading it, supplying calldata, running it, and reading outputs back.
[Ways to Use XQuad](../concepts/ways-to-use.md) names the surfaces; this page
is the full coverage for three of them: `xquad.program`, and the two VM
wrappers underneath and alongside it. For writing a problem with the `xqcp`
DSL, see
[Modelling](../modelling/). For what `xquad verify` checks and how
to fix a rejected program, see [Verification](verification.md).

Every example on this page ran against the Rust backend, the default for
both `xquad.program` and `xquad.vm.VM`.

## Program and Session

`xquad.program.Program` loads a program once; `Program.session()` gives you
a `Session` you call `.run()` on repeatedly, each time with fresh calldata.
Each run returns a `RunResult` with dict-keyed outputs, the residual stack,
and the step count:

```python
from xquad.program import Program

src = """
PUSH 0
INPUT r0
PUSH 1
INPUT r1
LOAD r0
LOAD r1
ADD
STOW r2
PUSH 0
OUTPUT r2
HALT
"""

program = Program.from_source(src)
assert program.instruction_count == 11
assert program.source is not None   # retained for debugging

session = program.session(output_slots=4)
session.set_calldata([40, 2])
result = session.run()

assert dict(result.outputs) == {0: 42, 1: None, 2: None, 3: None}
assert result.stack == []
assert result.steps == 11

# Re-run with different calldata -- sessions carry no hidden state.
session.set_calldata([100, 200])
assert dict(session.run().outputs) == {0: 300, 1: None, 2: None, 3: None}
```

`RunResult.outputs` is a dict keyed by slot index; a slot the program never
wrote reads as `None` rather than raising. `Program` is immutable and
reusable; `Session` carries the mutable calldata and output-slot count.
Deriving a second `Session` from the same `Program` gives you two runners
that share no state.

### Step limits

`Session.set_step_limit(n)` caps how many instructions a run may execute
before `run()` raises. The limit is exact: `0` permits no instructions at
all. A fresh `Session` starts at `xquad.vm.DEFAULT_STEP_LIMIT`, which is
10,000,000. `None` is the only unbounded spelling, and a caller has to
write it -- unbounded execution is asked for, never inherited.

```python
session = program.session(output_slots=1)
session.set_calldata([1, 2])
assert "step_limit=10000000" in repr(session)

session.set_step_limit(3)
try:
    session.run()
except RuntimeError as e:
    assert "StepLimitExceeded" in str(e)

# 0 is not a sentinel for "unlimited" -- it executes nothing, so this
# 11-step program fails at its first instruction.
session.set_step_limit(0)
try:
    session.run()
except RuntimeError as e:
    assert "StepLimitExceeded" in str(e)

# None removes the bound. A program that never halts will not return,
# and it does not take SIGALRM: the GIL is held inside the Rust run.
session.set_step_limit(None)
assert "step_limit=unlimited" in repr(session)
assert dict(session.run().outputs) == {0: 3}
```

A budget that exactly covers the program succeeds. This one needs 11
steps, so `set_step_limit(11)` runs it and `set_step_limit(10)` does not.

## Loading bytecode directly

`Program.load(bytes)` parses raw wire-format bytes -- a `.xqb` blob produced
by `xquad asm` or by `program.bytecode()`. No source is retained:

```python
blob = program.bytecode()
reloaded = Program.load(blob)
assert reloaded.source is None
assert reloaded.instruction_count == program.instruction_count
```

`Program.load` never validates its input; a malformed blob decodes
successfully as a `Program` object and only fails once a `Session` tries to
run it. A one-byte blob is too short even for the wire-format header:

```python
p = Program.load(b"\x43")
s = p.session()
try:
    s.run()
except RuntimeError as e:
    print(e)
# decode error: TruncatedHeader
```

`Program.from_source` validates at load time instead: an unassemblable
string raises `ValueError` immediately, before a `Session` exists.

## Calldata types

A calldata list may mix any of `int`, `list[int]`, `xqffi.vm.XqmxModel`,
`xqffi.vm.XqmxSample`, and `None` (unset). `Session.set_calldata` checks
every element's type as soon as you call it, not at run time:

```python
from xqffi.vm import XqmxModel, XqmxSample

model = XqmxModel("binary", size=4)
model.set_linear(0, -1)
model.set_quad(0, 1, 2)

sample = XqmxSample("spin", values=[-1, 1, -1, 1])

session.set_calldata([model, sample, [1, 2, 3], 42])
# session.run() now sees four typed input slots.

try:
    XqmxModel("binary", size=4, rows=2, cols=3)
except ValueError as e:
    print(e)
# a 2 x 3 grid does not fit 4 variables; rows and cols must both be 0,
# or both positive with rows * cols <= size

try:
    session.set_calldata([object()])
except TypeError as e:
    print(e)
# unsupported calldata element type: object; expected int, list[int],
# XqmxModel, XqmxSample, or None
```

The constructors also refuse a `size` over
`xqffi.vm.MAX_ALLOCATION_SIZE`, the largest allocation bytecode can
make, with `ValueError`, and `XqmxSample` refuses a value outside its
domain the same way. The coefficient accessors (`set_linear`,
`get_linear`, `set_quad`, `get_quad`) raise `IndexError` for a variable
index outside `0..size`, a negative index included:

```python
try:
    model.set_linear(4, 1)
except IndexError as e:
    print(e)
# variable index 4 is out of range for a model of size 4
```
## Inspecting a model or sample

`XqmxModel` and `XqmxSample` are pyo3 objects; there is no dict-conversion
helper in this package today. Read a model's coefficients through its own
accessors:

```python
model = XqmxModel("binary", size=4)
model.set_linear(0, -1)
model.set_linear(2, 3)
model.set_quad(0, 1, 2)

model.domain          # "binary"
model.size            # 4
list(model.linear_items())     # [(0, -1), (2, 3)]
list(model.quadratic_items())  # [((0, 1), 2)]
model.get_linear(0)   # -1
model.get_quad(0, 1)  # 2
repr(model)            # 'XqmxModel(domain=binary, size=4)'
```

`__repr__` is intentionally minimal. Convert to `xquad.types.XQMX` (the
canonical, backend-independent type covered next) if you want a Python
object you can inspect with normal attribute access, or serialise by
iterating `linear_items()` / `quadratic_items()` yourself.

## The other two VM surfaces

Two lower-level wrappers sit underneath and alongside `xquad.program`,
both worth knowing about directly.

**`xqffi.vm.Vm`** is the thinnest possible wrapper over the Rust
interpreter: construct it, call `set_calldata` / `set_output_slots`, call
`.run(bytecode)`, read `.outputs()` as a positional list. It always targets
the Rust backend. `.reset()` clears the stack, registers, loop stack, and
step counter, but calldata and output slots are untouched by it -- both
persist across runs until you call `set_calldata()` or `set_output_slots()`
again. It is what `Session.run()` builds internally:

```python
from xqffi.vm import Vm

vm_src = "PUSH 7\nPUSH 5\nADD\nSTOW r0\nPUSH 0\nOUTPUT r0\nHALT\n"
bytecode = Program.from_source(vm_src).bytecode()

vm = Vm()
vm.set_output_slots(1)
vm.run(bytecode)
assert vm.outputs() == [12]
```

[Ways to Use XQuad](../concepts/ways-to-use.md#xqffivm--the-raw-ffi)
covers when to reach for it directly instead of `Session`.

**`xquad.vm.VM`** is a separate wrapper that every example runner under
`examples/` actually uses (`examples/maxcut/runner.py`,
`examples/knapsack/runner.py`). It selects between the Rust interpreter and
the pure-Python reference VM via `VMBackend`, converts FFI
objects to and from the canonical `xquad.types.XQMX` at the boundary, and
runs `.xqasm` source text directly rather than pre-assembled bytecode:

```python
from xquad.vm import VM, VMBackend

src = "PUSH 7\nPUSH 5\nADD\nSTOW r0\nPUSH 0\nOUTPUT r0\nHALT\n"
v = VM(backend=VMBackend.RUST)  # RUST is also the default with no argument
v.set_output_slots(1)
v.run(src)
assert v.outputs() == [12]
assert v.stack() == []
```

`VM.outputs()` returns `xquad.types.XQMX` instances for model and sample
outputs, where `xqffi.vm.Vm.outputs()` and `Session.run().outputs` return
the raw pyo3 `XqmxModel` / `XqmxSample` objects. Pick `VM` when you need
Python-backend parity checking or when your calldata and outputs are
already in `xquad.types` terms; pick `Session` for the dict-keyed,
slot-sparse ergonomics shown above; pick `xqffi.vm.Vm` only if you are
building your own layer on top of the raw Rust FFI.

`VM.set_calldata()` applies the same size, grid and index rules to every
`xquad.types.XQMX` it is given, with the same errors on both backends,
and applies them again at `run()`, since it keeps your objects rather
than copying them. A sample value outside its domain raises
`xqvm_py.errors.SampleOutOfDomain`, a `ValueError` subclass, from
`set_calldata()` on both backends. A sample changed out of its domain
after `set_calldata()` is caught again at `run()`, where `RUST` raises a
plain `ValueError` instead.

## A complete run across three programs

A compiled problem is three independent programs -- see [Three
Programs](../concepts/three-programs.md) for why. Driving all three from
`xquad.program` looks like this, using the exact `build_problem` function
`examples/knapsack/runner.py` defines (see [Compiling](../modelling/compiling.md)
for what `compile()` emits):

```python
from examples.knapsack.runner import build_problem
from xquad.program import Program
from xqffi.vm import XqmxSample

n = 4
weights = [2, 3, 4, 5]
values = [3, 4, 5, 8]
capacity = 8

problem = build_problem(n, weights, values, capacity)
programs = problem.compile()

# Encoder: inputs in, model out.
encoder = Program.from_source(programs.encoder)
enc_session = encoder.session(output_slots=1)
enc_session.set_calldata([n, weights, values, capacity])
model = enc_session.run().outputs[0]

# A hand-picked sample -- items 1, 2, 3 selected, no solver involved.
sample = XqmxSample("binary", values=[0, 1, 1, 1, 0, 0, 0, 0])

# Verifier: the encoder's own inputs, then model and sample; (energy, valid) out.
verifier = Program.from_source(programs.verifier)
ver_session = verifier.session(output_slots=2)
ver_session.set_calldata([n, weights, values, capacity, model, sample])
ver_result = ver_session.run()
energy, valid = ver_result.outputs[0], ver_result.outputs[1]

# Decoder: sample + N in, decoded selection out.
decoder = Program.from_source(programs.decoder)
dec_session = decoder.session(output_slots=1)
dec_session.set_calldata([sample, n])
selected = dec_session.run().outputs[0]

assert (energy, valid, selected) == (-4817, 0, [0, 1, 1, 1])
```

Items 1, 2 and 3 weigh `3 + 4 + 5 = 12` against a capacity of `8`, so the
sample violates the capacity constraint and `valid` comes back `0`.
`energy` is still computed: `ENERGY` recomputes the objective whatever the
sample's feasibility, so the two outputs answer different questions. Swap
in `[1, 1, 0, 0, 0, 0, 0, 0]` -- items 0 and 1, weighing `5` -- and the same
run gives `(-5507, 1)`.
[Verification](verification.md#what-the-generated-verifiers-valid-flag-covers)
says what `valid` covers.

The verifier takes the encoder's calldata because it replays the encoder to
rebuild the constraint data, then appends the model and the sample. Each
program's own `.set_calldata` order and `.set_output_slots` count are fixed
by what it was compiled from -- [Compiling](../modelling/compiling.md#the-calldata-and-output-contract)
has the full table. A real pipeline replaces the hand-picked `sample` above
with `xqsa.build_solver(...).solve(model).sample`; see [Solving
Overview](../solving/).

## What this page does not cover

- **Tracing.** Step-by-step execution inspection is not part of `Session`
  today.
- **Keyword calldata** (`session.set_calldata(n=4, ...)`). `Session` takes a
  positional list only; there is no input-slot labelling to key against.
- **A `_repr_html_` for notebooks.** `__repr__` is the only representation
  `Program`, `Session`, `RunResult`, `XqmxModel` and `XqmxSample` provide.
- **A `to_numpy()` helper.** Convert via `linear_items()` /
  `quadratic_items()` and `numpy.asarray(...)` on your own side; this
  package does not carry a numpy dependency.

---

### Overview (`docs/book/src/solving/README.md`)

# Solving Overview

[Backends](../concepts/backends.md) names the five solvers and what
distinguishes them. This chapter covers each one in enough depth to run
it: installation, parameters, and what its result actually contains.

`xqsa` is the package that does this. It defines one abstract interface,
`Solver`, and five classes built on it -- `SolverDWaveCPU`,
`SolverDWaveQPU`, `SolverCudaGPU`, `SolverMetalGPU`, and `SolverQuip`. A
model built from `xqcp` or by hand in XQVM bytecode does not know or care
which one samples it.

## The Solver Interface

Every backend defines the same single method:

```python
class Solver(ABC):
    @abstractmethod
    def solve(self, model: XQMX, **kwargs: Any) -> SolverResult: ...
```

`model` is an XQMX in MODEL mode, binary or spin domain. `**kwargs`
override that solver's constructor defaults for this call only --
`solver = SolverDWaveCPU(num_reads=100)` then `solver.solve(model,
num_reads=500)` runs 500 reads without building a new solver. `solve()`
raises on failure (hardware unreachable, no embedding found, connection
lost); it never returns a partial or empty result.

The return value is a frozen dataclass:

```python
@dataclass(frozen=True)
class SolverResult:
    sample: XQMX
    energy: int
    timing: float
    metadata: dict[str, Any] = field(default_factory=dict)
```

`sample` is the solution, an XQMX in SAMPLE mode. `energy` is the
authoritative Hamiltonian energy. `timing` is wall-clock seconds spent
solving. `metadata` defaults to an empty dict; its actual shape is
per-backend, covered below.

`energy` is never the backend's own reported value taken on faith. Every
`solve()` recomputes it with the same integer formula the XQVM `ENERGY`
opcode uses, over the model and the returned sample. See
[Energy and Precision](energy-and-precision.md) for what that buys you
and what it costs on hardware with limited precision.

`metadata`'s shape is not identical across all five backends. The three
classical simulated-annealing backends -- `dwave-cpu`, `cuda-gpu`, and
`metal-gpu` -- carry `seed` and `reads` at the top level plus a `params`
dict of solver-specific detail (sweep counts, the raw pre-recompute
energy). `dwave-qpu` carries a `params` dict the same way, but omits
`seed` (there is no seed on physical hardware) and adds `solver` and
`qpu_timing` at the top level, alongside `reads`; see
[D-Wave QPU](dwave-qpu.md#how-a-qpu-result-differs). `quip` returns a
different set entirely -- no `seed`, `reads`, or `params` at all; see
[Quip Network](quip-network.md#metadata). Check the backend you are
calling before indexing into `result.metadata`.

## Picking a Backend by Name

`build_solver` constructs any of the five from a short string, so a
caller -- an example runner, a CLI flag, a script -- can stay
backend-agnostic:

```python
from xqsa import SOLVERS, DEFAULT_SOLVER, build_solver

print(sorted(SOLVERS))
# ['cuda-gpu', 'dwave-cpu', 'dwave-qpu', 'metal-gpu', 'quip']
print(DEFAULT_SOLVER)
# dwave-cpu

solver = build_solver("dwave-cpu", seed=42)
result = solver.solve(model)
```

`seed` reaches the three classical simulated-annealing backends
(`dwave-cpu`, `cuda-gpu`, `metal-gpu`) and is ignored for `dwave-qpu`
(physical hardware has no seed) and `quip` (configured from the
environment; the miner's own randomness is out of the caller's control).

`build_solver` otherwise uses each backend's own constructor defaults,
with one exception: for `cuda-gpu` and `metal-gpu` it raises `num_reads`
to 200 and `num_sweeps` to 2000, rather than the 100 and 1000 the bare
constructor defaults to. [Local Solvers](local.md#gpu-backends-cuda-gpu-and-metal-gpu)
has the constructor defaults for every backend.

## Swapping Backends Without Changing the Model

This is the claim the whole toolchain rests on, so here it is checked
rather than asserted. `examples/maxcut/runner.py` builds one Max-Cut
model with `xqcp`, runs the encoder on the Rust XQVM, and samples the
result with whichever solver `--solver` names, built through
`build_solver`. Run it against two different backends -- `dwave-cpu`
(CPU simulated annealing, 100 reads and 1000 sweeps) and `metal-gpu` (an
Apple Silicon GPU kernel, a different process on different hardware, and
-- per `build_solver`'s override above -- 200 reads and 2000 sweeps) --
and nothing about the model or the encoder changes:

```sh
uv run python examples/maxcut/runner.py --n 6 --seed 42 --interpreter rust --solver dwave-cpu
uv run python examples/maxcut/runner.py --n 6 --seed 42 --interpreter rust --solver metal-gpu
```

Both print the same result: `"energy": -571`, `"cut_weight": 571`,
`"valid": 1`, the identical partition. The encoder produced one QUBO;
two unrelated solvers minimised it and agreed. That agreement is not
guaranteed in general -- a harder model can leave different backends in
different local optima -- but the *model* they were handed, and the
*verifier* that checked what came back, never changed.

## The Domain Every Backend Rejects

XQVM has three domains: binary, spin, and integer. Every current
`xqsa` solver's `_validate_model()` accepts binary and spin and raises
`ValueError` on integer:

```python
from xqsa import SolverDWaveCPU
from xqvm_py.xqmx import XQMX

model = XQMX.integer_model(size=3, k=4)
SolverDWaveCPU().solve(model)
# ValueError: Unsupported domain for solving: INTEGER
```

An integer `XqmxModel` is a real thing you can build in XQVM bytecode
today -- see [Three Domains](../concepts/quadratic-models.md#three-domains)
-- but nothing in this chapter can solve one. `spec/xqsa/DOMAINS.md`
calls this "reserved": a future solver may relax the check, but none of
the current five does. Re-encoding the problem in binary or spin is the
only route around it today.

## Choosing Among the Rest

| You want | Read |
|---|---|
| A baseline that runs anywhere, no hardware or credentials required | [Local Solvers](local.md), `dwave-cpu` |
| A local GPU for a bigger or faster run | [Local Solvers](local.md), `cuda-gpu` / `metal-gpu` |
| Real quantum annealing hardware | [D-Wave QPU](dwave-qpu.md) |
| A decentralised, miner-solved compute market | [Quip Network](quip-network.md) |
| What the numbers in `SolverResult` actually mean | [Energy and Precision](energy-and-precision.md) |

That baseline requirement -- runs anywhere, needs nothing -- is exactly
`DEFAULT_SOLVER`: every other backend is worth comparing against
`dwave-cpu` before you trust its answer.

---

### Overview (`docs/book/src/start/README.md`)

# Install XQuad

This page installs XQuad, confirms the install works, and gets the
runnable examples this book uses. It also covers what a GPU or QPU solver
needs beyond the base install. [First Problem](first-problem.md) picks up
from here with a real run.

## Install

The [root README's Install section](https://gitlab.com/quip.network/xquad/-/blob/main/README.md#install)
is the canonical install reference for both ecosystems this project ships
into -- prebuilt wheel platforms, the Rust toolchain required to build the
`xqffi` extension from source, and installing individual Python packages
instead of the umbrella. The two commands most readers need:

```sh
cargo install xqcli      # gives you the `xquad` binary
pip install xquad        # Python umbrella: the full pipeline
```

## Verify the Install

`xquad --version` prints the installed CLI's version, confirming the
`xqcli` binary is on `PATH` and runs. Importing the umbrella package
confirms the Python half:

```sh
$ python -c "import xquad; print('ok')"
ok
```

## Get the Examples

Every `uv run python examples/...` command in this book, including on
the next page, runs against a checkout of this repository -- the
examples are not part of any published package, so `cargo install` and
`pip install` above do not put them on disk. Get one and set up the
Python workspace once:

```sh
git clone https://gitlab.com/quip.network/xquad.git
cd xquad
make deps-py
```

`make deps-py` needs [`uv`](https://docs.astral.sh/uv/getting-started/installation/)
and the Rust toolchain `cargo install xqcli` above already needed. It
syncs the Python workspace and wires up cross-package imports, so
`uv run python examples/<name>/runner.py` then works from the repository
root -- the same command every example and cookbook page in this book
uses.

## GPU and QPU Install Prerequisites

The base install runs CPU simulated annealing only, through `xqsa`'s
`dwave-cpu` backend. Every other solver -- a local GPU or a real D-Wave
QPU -- needs an extra on top, and the extra needs hardware or credentials
this page cannot install for you. `xqsa/pyproject.toml` defines the
extras, and `xquad/pyproject.toml` forwards to the ones it re-exports:

- `cuda` -- `cupy-cuda12x>=13.0`, `nvidia-cuda-nvrtc-cu12`, and
  `nvidia-cuda-runtime-cu12`, for a local NVIDIA GPU.
- `dwave` -- `dwave-system>=1.0`, for the real D-Wave QPU.
- `metal` -- `pyobjc-framework-Metal>=11.0`, marked `sys_platform ==
  'darwin'`, for a local Apple GPU. The marker means `pip install
  xquad[metal]` succeeds on Linux and installs nothing.
- `quip` -- `substrate-interface>=1.7.4,<2` plus
  `quip-signer>=0.3.0,<0.4`,
  for the Quip network solver, which needs a `QUIP_RPC_URL` and a
  configured signer; see [Quip Network](../solving/quip-network.md) for
  what that solver does.

`xqcp`, `xqffi`, and `xqvm_py` define no optional dependencies at all.

The only extra this page can fully specify is `[dwave]`: it needs a D-Wave
Leap account and a `DWAVE_API_TOKEN`, and `dwave ping` confirms both. A
local GPU (`[cuda]` or `[metal]`) is another option; see [Local
Solvers](../solving/local.md#driver-prerequisites) for those two extras
and their driver checks.

`pip install xquad[cuda]`, `xquad[metal]`, `xquad[dwave]`, and
`xquad[quip]` each forward to the matching `xqsa` extra, and extras are
composable: `pip install "xquad[cuda,dwave]"`.

A missing extra does not break the base install: `import xqsa` never
fails just because an optional extra is absent. Only constructing the
solver class that needs it does, raising `ImportError` with a
`pip install xqsa[...]` hint naming the extra to add.

Per-solver parameters, driver-level troubleshooting, and what a QPU
result contains that a CPU one does not are covered in
[Solving Overview](../solving/), [Local Solvers](../solving/local.md),
and [D-Wave QPU](../solving/dwave-qpu.md). This section only covers what
to install before you get there.

## Where to Go From Here

- **[First Problem](first-problem.md)** -- run a complete problem end to
  end and get a real answer back.
- **[What Happened](what-happened.md)** -- the explanation of what that
  run just did.
- **[XQVM Reference](../xqvm/)** -- a minimal hand-written
  `.xqasm` program, if you want to see the machine underneath before
  running anything larger.
- **[Toolchain Map](../concepts/)** -- the pieces XQuad is built
  from, and how they hand off to each other.

---

### Overview (`docs/book/src/xqvm/README.md`)

# XQVM Reference

XQVM is the virtual machine at the core of XQuad: a stack-based interpreter
with a 256-slot register file that executes compiled quadratic-optimisation
programs. This section is the reference for that machine -- its execution
model, assembly language, instruction set, binary format, and verifier. For
what a QUBO/Ising model is and why you would want one, start at
[Concepts](../concepts/) instead; this section assumes you already
have a program, or want to write one directly in `.xqasm`, and want to know
exactly what the machine does with it.

## A minimal program

Save this as `add.xqasm`, then assemble and run it:

```asm
; push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
```

```sh
$ xquad asm add.xqasm -o add.xqb
assembled 4 instructions (21 bytes) -> add.xqb
$ xquad run add.xqb
stack (bottom to top):
  42
```

The program pushes 10 and 32 onto the value stack, adds them, and halts. The
result remains on the stack and is printed by `xquad run`. `xquad run --text
add.xqasm` skips the separate assembly step and runs the source directly.

## What this section covers

- **[VM Architecture](machine-model.md)** -- the value stack, the 256-slot
  register file, and the loop stack.
- **[Loop Stack](loops.md)** -- `RANGE`/`ITER`/`NEXT` iteration.
- **[Calldata and Outputs](io.md)** -- getting values into a program with
  `INPUT` and out with `OUTPUT`.
- **[Execution](execution.md)** -- the fetch-decode-execute cycle and
  execution tracing.
- **[Assembly](assembly.md)** -- the `.xqasm` text syntax.
- **[Assembly Examples](assembly-examples.md)** -- worked `.xqasm` programs.
- **[Instructions](instructions/)** -- all 93 instructions, by
  category.
- **[Opcode Reference](opcodes.md)** -- the generated opcode table.
- **[Bytecode Format](bytecode-format.md)** -- the `.xqb` wire format.
- **[Verifier](verifier.md)** -- what the pre-execution verifier checks.
- **[CLI](cli/)** -- the `xquad` command-line tool.
- **[Limits and Errors](limits-and-errors.md)** -- fixed and configurable
  limits, and every runtime and verifier error.

---

### Overview (`docs/book/src/xqvm/cli/README.md`)

# CLI Reference

`xquad` is the unified command-line driver for the XQVM toolchain. It is
built by the `xqcli` crate, but the crate and the binary have different
names: `cargo install xqcli` installs a command called `xquad`, not
`xqcli`. Running `xqcli --help` after installing fails because no such
binary exists.

## Installation

```sh
cargo install xqcli
xquad --help
```

Or build from this repository:

```sh
cargo build -p xqcli --release
```

The binary is at `target/release/xquad`.

## Subcommands

`xquad` has four subcommands, one per stage of the toolchain:

| Command | Description |
|---------|-------------|
| [`xquad asm`](asm.md) | Assemble `.xqasm` source into binary bytecode. |
| [`xquad dism`](dism.md) | Disassemble bytecode into a human-readable listing. |
| [`xquad run`](run.md) | Execute bytecode or assembly, with optional tracing. |
| [`xquad verify`](verify.md) | Run the bytecode verifier over a program without running it. |

## General usage

```sh
xquad <COMMAND> [OPTIONS] [ARGS]
xquad --help
xquad <COMMAND> --help
```

`xquad asm` reads `.xqasm` source and writes bytecode; it has no `--text`
flag and rejects a `.xqb` file. `xquad dism` reads bytecode from a file or
stdin, also with no `--text` flag. `xquad run` and `xquad verify` read
bytecode by default and read `.xqasm` assembly instead when `--text` is
passed.

Error reporting is not uniform across the four subcommands. Only `xquad
asm` propagates the real `xqasm::Error`, with its `NamedSource` and
`SourceSpan`, so only `asm` prints a highlighted source snippet pointing
at the failing token. `xquad run --text` and `xquad verify --text` both
flatten an assembly failure through `miette::miette!("{e}")`, which
formats the error to a plain string and discards the span; `xquad verify`
on a `.xqb` file flattens a verifier failure the same way. Decode errors
(a malformed `.xqb` file) go through `.into_diagnostic()`, which also
carries no span. So an assembly, verification or decode failure reaches
you as a bare message -- see [`xquad verify`](verify.md#a-failing-program)
for what that looks like in practice. A VM runtime fault under `xquad
run` is the one exception: it goes through `into_diagnostic()`, which
disassembles the program around the failing offset and annotates it, so
that path does print a snippet. Argument-parsing failures, such as
`asm`'s `-o`/`--stdout` conflict, are plain `clap` errors, a third and
unrelated error path.

## A minimal round trip

```sh
cat > add.xqasm <<'EOF'
; add.xqasm -- push two integers and add them
PUSH 10
PUSH 32
ADD
HALT
EOF

xquad asm add.xqasm -o add.xqb
xquad verify add.xqb
xquad run add.xqb
```

`xquad run add.xqb` prints the residual stack:

```
stack (bottom to top):
  42
```

---

### Overview (`docs/book/src/xqvm/instructions/README.md`)

# Instruction Set Reference

This section documents all 93 XQVM instructions, organised into the 14
categories below. Each page covers the semantics, error conditions, and
worked examples for one category. For the opcode byte, mnemonic, operand
layout, and stack effect of every instruction, see the generated
[Opcode Reference](../opcodes.md) -- that table is the single source of
truth for wire-level facts, so the pages in this section carry prose and
teaching content instead of repeating it.

## Notation

- **Stack effect** -- described in prose on this page ("Pop `b`, then `a`")
  and summarised on the generated [Opcode Reference](../opcodes.md) as a
  `Stack` column of the form `2 → 1`, meaning two values popped and one
  pushed. The value stack is last-in-first-out: whichever value was pushed
  most recently is popped first, so `PUSH a; PUSH b; SUB` computes
  `a - b`, not `b - a`. Every "pop `x`, then `y`" operand order on this
  section's pages, including the ones on [constraints.md](constraints.md) and
  [index-math.md](index-math.md), depends on this rule.
- **`reg`** -- the `u8` operand encoded in the instruction byte stream,
  identifying a register slot (r0--r255).
- **`label`** -- a sequential id assigned to each `TARGET` instruction, in
  program order, during the load-time pre-scan described in
  [Bytecode Format](../bytecode-format.md). It is not a byte offset, and not
  the `.N` token used in `.xqasm` source -- that token is assembler-only and
  is resolved to the sequential id before encoding. `JUMP1`/`JUMPI1` encode
  the id as a single `u8` operand; `JUMP2`/`JUMPI2` encode it as a `u16`
  operand (big-endian). See [Control Flow](control-flow.md) for how the
  assembler picks between the two.
- Assignments use \\(\leftarrow\\) (register write) and \\(\to\\) (stack push).
- **Iverson brackets** -- \\([P]\\) equals \\(1\\) if \\(P\\) is true, \\(0\\) otherwise.
- **Checked arithmetic** -- every integer operation is checked against the
  `i64` range. A result that would leave it raises `ArithmeticOverflow`
  rather than wrapping or panicking; see
  [Arithmetic](arithmetic.md#overflow).

### Register Effect Vocabulary

A handful of pages, [constraints.md](constraints.md) and
[energy.md](energy.md), annotate individual opcodes with a `**Register
effect:**` line using three terms:

- **`read`** -- register contents are inspected but not changed.
- **`write`** -- register is replaced wholesale with a new value.
- **`mutate`** -- register's existing value is modified in-place (e.g.
  appending to a vec, incrementing a coefficient).

Most category pages group opcodes by topic or mechanism rather than
walking through them one at a time, so they carry no such line.

## `RegVal` -- The Register Value Type

Each of the 256 registers holds one variant of `RegVal` -- `Unset`, `Int`,
`VecInt`, `VecXqmx`, `Model`, or `Sample`. See [VM
Architecture](../machine-model.md#regval-variants) for the full table.

Type mismatches at runtime produce a `RegisterType` error with the expected
and actual variant names; reading an `Unset` register via `LOAD` or `OUTPUT`
produces `UnsetRegister` instead, since there is no variant to compare
against.

## Categories

| Category | Page | Opcodes |
|---|---|---|
| Control Flow | [control-flow.md](control-flow.md) | `TARGET`, `JUMP1`, `JUMP2`, `JUMPI1`, `JUMPI2`, `NEXT`, `LVAL`, `LIDX`, `RANGE`, `ITER`, `NOP`, `HALT` |
| Register I/O | [register-io.md](register-io.md) | `LOAD`, `STOW`, `DROP`, `INPUT`, `OUTPUT` |
| Stack Manipulation | [stack-manipulation.md](stack-manipulation.md) | `POP`, `PUSH1`-`PUSH8`, `SCLR`, `SWAP`, `COPY` |
| Arithmetic | [arithmetic.md](arithmetic.md) | `ADD`, `SUB`, `MUL`, `DIV`, `MOD`, `SQR`, `ABS`, `NEG`, `MIN`, `MAX`, `INC`, `DEC`, `BITLEN` |
| Comparison | [comparison.md](comparison.md) | `EQ`, `LT`, `GT`, `LTE`, `GTE` |
| Logical Boolean | [logical.md](logical.md) | `NOT`, `AND`, `OR`, `XOR` |
| Bitwise | [bitwise.md](bitwise.md) | `BAND`, `BOR`, `BXOR`, `BNOT`, `SHL`, `SHR` |
| Allocators | [allocators.md](allocators.md) | `BQMX`, `SQMX`, `XQMX`, `BSMX`, `SSMX`, `XSMX`, `VEC`, `VECI`, `VECX` |
| Vector Operations | [vector-ops.md](vector-ops.md) | `VECPUSH`, `VECGET`, `VECSET`, `VECLEN`, `SLACK` |
| Index Math | [index-math.md](index-math.md) | `IDXGRID`, `IDXTRIU` |
| Coefficient Access | [coefficient-access.md](coefficient-access.md) | `GETLINE`, `SETLINE`, `ADDLINE`, `GETQUAD`, `SETQUAD`, `ADDQUAD` |
| Grid Operations | [grid.md](grid.md) | `RESIZE`, `ROWFIND`, `COLFIND`, `ROWSUM`, `COLSUM` |
| High-Level Constraints | [constraints.md](constraints.md) | `ONEHOTR`, `ONEHOTC`, `EXCLUDE`, `IMPLIES`, `EQUALITY`, `ATLEAST`, `ATLEASTW`, `REDUCE` |
| Energy Evaluation | [energy.md](energy.md) | `ENERGY` |

## Reserved Opcodes

Every byte value not listed in [Opcode Reference](../opcodes.md) is illegal;
the decoder rejects it. The 93 assigned bytes are not contiguous -- there are
gaps both between and within the ranges above, plus one large unassigned
block above the normal instruction space. The full gap set, derived by
diffing the 93 assigned byte values against the complete `0x00`-`0xFF` space:

| Range | Unassigned bytes |
|---|---|
| Register I/O | `0x0D` |
| Stack Manipulation | `0x19`, `0x1D`-`0x1F` |
| Arithmetic | `0x2D`-`0x2F` |
| Comparison | `0x35` |
| Allocators | `0x46`-`0x49`, `0x4D`-`0x4F` |
| Vector Operations | `0x55`-`0x59` |
| Index Math / Coefficient Access | `0x5C`-`0x5F` |
| Grid Operations | `0x6B`-`0x6F` |
| High-Level Constraints | `0x78`-`0x7E` |
| Outside the normal instruction space | `0x80`-`0xEF`, `0xF1`-`0xFE` |

`NOP` (`0xF0`) and `HALT` (`0xFF`) are the only two assigned bytes outside
`0x00`-`0x7F`; every other byte in `0x80`-`0xFF` is illegal. See
[`spec/xqvm/ISA.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/ISA.md#reserved-opcodes)
for the normative list, organised the same way.

---

### Book Src Appendix Spec Index (`docs/book/src/appendix/spec-index.md`)

# Spec Index

The book teaches; `spec/` is what XQuad actually is. Every file below is
the normative source for its topic's intent. The conformance harness does
not check either implementation against these documents: it checks the
Rust and Python implementations against each other and against
`conformance/opcodes.yaml`. The implementation is what ships, and the two
can diverge; where a divergence is known, the relevant book page says so.
[Conformance](../embedding/conformance.md#coverage-is-per-opcode-and-incomplete)
explains why a green run is evidence about the programs a vector covers
rather than a blanket guarantee.

The lists below are not closed. For anything they omit, check the book
chapter covering the component you are working with, or search the spec
files directly.

## Top Level

- [`spec/README.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/README.md)
  -- index of the three layers below, and the drift policy that ties spec
  to implementation.

## XQVM

Normative for the virtual machine: the instruction set, the wire format,
and the bytecode verifier.

- [`spec/xqvm/README.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/README.md)
  -- document index for this layer.
- [`spec/xqvm/SPEC.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/SPEC.md)
  -- normative for the machine overview, the three-program architecture,
  the state model, determinism, the type system, and runtime limits.
- [`spec/xqvm/ISA.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/ISA.md)
  -- normative for the instruction set: notation, per-category opcode
  tables, semantic notes, and reserved opcodes.
- [`spec/xqvm/HLF.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/HLF.md)
  -- normative for the high-level constraint expansion formulas: the QUBO
  penalty terms `ONEHOTR`, `ONEHOTC`, `EXCLUDE`, `IMPLIES`, `EQUALITY`,
  `ATLEAST`, `ATLEASTW`, and `REDUCE` inject.
- [`spec/xqvm/ENCODING.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/ENCODING.md)
  -- normative for both file formats: `.xqasm` assembly syntax and the
  `.xqb` binary encoding, including the XQBC header layout.
- [`spec/xqvm/VERIFIER.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/VERIFIER.md)
  -- normative for the bytecode verifier: its phase pipeline and every
  error it can raise.
- [`spec/xqvm/METERING.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqvm/METERING.md)
  -- normative for step metering: what a step is, the cost constants and
  how they were measured, the per-opcode charge table, the three formulas,
  and the conformance rules for step counts and operand validation order.

## XQCP

Normative for the constraint-programming DSL that compiles a `Problem`
into the three XQVM programs.

- [`spec/xqcp/README.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqcp/README.md)
  -- document index for this layer.
- [`spec/xqcp/SPEC.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqcp/SPEC.md)
  -- normative for the DSL overview, the three-program architecture as
  `xqcp` implements it, the problem lifecycle, and the compilation
  contract.
- [`spec/xqcp/TYPES.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqcp/TYPES.md)
  -- normative for the symbolic value types, the expression tree, the
  operator algebra, and the free functions built on top of it.
- [`spec/xqcp/CONSTRAINTS.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqcp/CONSTRAINTS.md)
  -- normative for the constraint taxonomy, each constraint method's
  signature, and its cross-reference into the matching HLF expansion.
- [`spec/xqcp/COMPILER.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqcp/COMPILER.md)
  -- normative for the compilation pipeline: encoder/verifier/decoder
  generation, register allocation, and action recording.

## XQSA

Normative for the solver-adapter interface between a model and a backend.

- [`spec/xqsa/README.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqsa/README.md)
  -- document index for this layer.
- [`spec/xqsa/SPEC.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqsa/SPEC.md)
  -- normative for the architecture overview, where solving sits in the
  pipeline, and the plugin model new solvers implement.
- [`spec/xqsa/INTERFACE.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqsa/INTERFACE.md)
  -- normative for the `Solver` abstract class, the `solve()` contract,
  the `SolverResult` type, and parameter-passing conventions.
- [`spec/xqsa/ENERGY.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqsa/ENERGY.md)
  -- normative for the energy computation formula, its precision contract,
  and the sparse representation solvers exchange it in.
- [`spec/xqsa/DOMAINS.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqsa/DOMAINS.md)
  -- normative for the domain support matrix, sample encoding, grid
  metadata, and capability negotiation between a model and a solver.
- [`spec/xqsa/SOLVERS.md`](https://gitlab.com/quip.network/xquad/-/blob/main/spec/xqsa/SOLVERS.md)
  -- normative for the solver registry, its naming convention, algorithm
  families, and the dependency model behind each optional extra.

## Reading a Spec File Like a Reference, Not a Tutorial

Every file above assumes the concepts this book explains from scratch --
what a quadratic model is, why a problem becomes three programs, what a
penalty weight does. Arrive from [Concepts](../concepts/) or the
relevant chapter first; the spec files are where to go once you need the
exact rule the book page summarised, not where to start.

---

### Guide Development Workflow (`docs/guide/development-workflow.md`)

# xquad development workflow

This document describes how changes move from authoring to `main` and
`dev` in the xquad repository. The central idea -- carried over from the
xq-rs <-> xq-py merge (QUI-412) -- is that two implementations of the
same VM must stay in lockstep. Everything else in the workflow is in
service of that invariant.

## Who maintains what

One repository, two implementations, one spec:

| Component | Owner(s) | Role |
|-----------|----------|------|
| [`spec/xqvm/SPEC.md`](../../spec/xqvm/SPEC.md) | all | Normative description of VM behaviour. Every conformance vector derives from here. |
| [`xqvm/`](../../xqvm/) | Rust track | Production interpreter. `no_std + alloc`. Used by the Substrate pallet, the `xquad` CLI, and the `xqffi` pyo3 extension. |
| [`xqvm_py/`](../../xqvm_py/) | Python track | Reference interpreter. Pure Python. The conformance oracle. |
| [`conformance/`](../../conformance/) | shared | Cross-impl parity harness. Every committed vector runs on both VMs in CI; disagreement fails the build. |
| [`xqffi/`](../../xqffi/) | Rust track | PyO3 FFI layer. Rust crate compiled via maturin to a Python wheel -- exposes `xqvm` and `xqasm` to the Python side. Not a pure-Python package. |
| [`xqcp/`](../../xqcp/), [`xqsa/`](../../xqsa/), [`xquad/`](../../xquad/) | Python track | Python surface: DSL, solver adapters, and umbrella. Consume the VMs (via `xqffi`) rather than define their semantics. |

## Branches

Two long-lived branches. Before 1.0, `main` is the non-breaking line
and `dev` is the breaking one; which a change targets is decided by
whether it breaks, not by whether it is a fix.
[`gitflow-protocol.md`](gitflow-protocol.md) is the normative
description -- routing, releases, betas and the back-merge -- and what
follows is only the naming.

- **`main`** -- the non-breaking line, at the next patch's `-dev`
  version. Protected: everything lands by merge request except the
  version bump that reopens it after a release, which the code owners push
  directly and CI holds to a version-only change. A merge lands as a
  real merge commit with a GitLab-generated `merge: branch '<source>'
  into 'main'` subject. Merges are not squashed.
- **`dev`** -- the breaking line, at the next minor's `-dev` version.
  Protected; Maintainers may push to it, for back-merges and version
  bumps only. It contains `main` at all times, and CI marks `dev` red
  while a back-merge is outstanding.
- **`feature/qui-<id>[-tag]`** -- short-lived branches for individual
  tickets, cut from and merged into whichever line the change belongs
  on. One branch may carry several tickets when they touch the same
  files. The MR body lists each ticket it consumes (QUI-*), so Linear
  status moves in lockstep with git state.
- **`release/v<version>`** -- prepares a release: cut from `main` for a
  patch or from `dev` for a minor, carries the version bump, and merges
  into `main` unsquashed. `release:auto-tag` cuts the tag from the
  merge.
- **`chore/<tag>`** -- repository maintenance belonging to no ticket.

## The atomic spec-MR rule

**Any MR that changes VM semantics must touch all four of the
following in the same MR:**

1. `spec/xqvm/*.md` -- any normative spec file: the change documented.
   `SPEC.md`, `ISA.md`, `HLF.md`, `ENCODING.md` and `VERIFIER.md` all
   satisfy this layer; the guard matches the directory, not one file.
2. `xqvm/src/**/*.rs` -- the Rust production impl updated.
3. `xqvm_py/{executor,opcodes,xqmx,state,vector,tracer,errors}.py` --
   the Python reference impl updated.
4. `conformance/vectors/**` or `conformance/opcodes.yaml` -- a new
   or modified conformance vector that exercises the change.

### Why

- **No drift-tracking middle ground.** Before the merge, drift
  between `xq-rs` and `xq-py` was tracked as a running list. Ten
  drift points accumulated before we stopped accepting it and merged
  the repos (QUI-412). The atomic rule makes divergence impossible by
  construction: you can't commit a spec change without updating both
  impls in the same diff.
- **Reviewable as one story.** A reviewer sees the spec delta next
  to the two impl deltas next to the test that proves they agree.
  The mental model is contained in one MR.
- **CI coverage that scales.** The conformance harness runs every
  vector against both runtimes. Adding a vector at the same time as
  the semantics change means the harness is as up-to-date as the
  spec on the day of the merge.

### Enforcement

A CI guard -- [`scripts/check-atomic-spec-mr.sh`](../../scripts/check-atomic-spec-mr.sh)
-- runs as part of `verify:policy` on every merge request. It classifies
changed files into the four layers and fails the pipeline if an MR
touches **1-3 layers but not all four**. Touching **0 layers** (pure
docs / CI / tooling MRs) or **all 4** passes.

Run locally the same way CI does:

```sh
scripts/check-atomic-spec-mr.sh origin/main HEAD
```

### Exemptions

Some legitimate changes only touch one or two layers -- the guard
would flag them as drift even though they're alignment fixes. The
exempt cases:

- **One-sided alignment fix.** One impl already matches the spec;
  the other impl is brought in line without changing the canonical
  behaviour. Example: QUI-453 changed only `xqvm_py/xqmx.py` to pre-
  populate a spin sample with `-1` per position, matching what Rust
  had always done -- no Rust change, no opcode table change.
- **Spec clarification.** The spec text gains precision without
  changing the normative rules. No impl updates needed.
- **Conformance-only coverage addition.** A new vector exercises
  existing semantics. No spec / impl changes.
- **Cross-implementation alignment with no normative change.** Both
  impls move, and sometimes the harness with them, to converge on
  behaviour the spec already states -- so the spec layer has nothing to
  add and the guard's four-layer test cannot be satisfied honestly.
  This is the case the v0.4.0 series used most: QUI-1147's step-budget
  export touched `xqvm/src/` and `xqvm_py/` together, and QUI-1032's
  opcode-signature check touched those two plus `conformance/`, neither
  of them changing what the spec says.

**To take an exemption, add a commit-message trailer of the form
`Atomic-Spec-Exempt: QUI-<id> <reason>`.** It goes in the message's last
paragraph, at column 0, beside the sign-off:

```
Align Python SSMX default to Rust's [-1, -1, ...] initialisation.

Python was shipping empty-dict samples where Rust used vec![-1; size];
this aligns the Python side. No Rust change needed.

Fixes QUI-453
Atomic-Spec-Exempt: QUI-453 one-sided Python fix, no semantics change
Signed-off-by: You <you@example.com>
```

The layout is not stylistic. git reads trailers out of the **last
paragraph only**, and a paragraph break above the trailer hides it from
`git log --format='%(trailers)'`, from GitLab's commit view, and from
everything else built on git's trailer parsing. Every exempt trailer
written before QUI-1030 sat in a paragraph of its own above the ticket
footer, so not one of them parses -- which is how eleven exemptions were
taken without any of them being findable.

Two more rules follow from how git treats that paragraph:

- **The whole reason goes on the trailer line.** A continuation line is
  not a trailer, and git either drops it -- silently truncating the
  reason to its first line -- or, with no sign-off in the paragraph,
  stops reading the paragraph altogether.
- **A `Fixes QUI-NNN` footer may share the paragraph, but not the line
  directly below the trailer.** It has no colon, so it is not a trailer
  either; the mandatory sign-off is what makes git tolerate it. Directly
  below the trailer it is indistinguishable from a wrapped reason, so the
  guard rejects that position rather than guess. Put it above the
  trailer, or below the sign-off.

A trailer that breaks any of this fails the guard rather than being
ignored by it, because an exemption nobody can find is worse than no
exemption at all. Column 0 is git's rule too, which is why an indented
example in a commit body -- like the ones in this repository's own
documentation commits -- is not mistaken for a real exemption.

The guard scans every commit message in the MR range and bypasses when
it finds at least one well-formed trailer. Reviewers should see the
exemption and confirm the rationale holds; there is no approval process
beyond review.

## What does NOT trigger the rule

- **Consumer-side surface work** -- the PyO3 bindings under
  `xqffi/src/*.rs` (Rust, but consumer glue -- they expose the VM to
  Python, they don't define its semantics), plus `xquad/program.py`
  and the `xquad` / `xqcp` / `xqsa` Python packages. Changes here
  don't need spec or conformance updates.
- **Build glue and tooling** -- `xqvm/build.rs`, `xqvm/Cargo.toml`,
  `xqvm_py/pyproject.toml`, `Makefile`, CI config, scripts. Not
  semantic.
- **Docs** -- everything under `docs/`, READMEs, CHANGELOG. Not
  semantic.
- **Tests** -- `xqvm/src/**/tests.rs`, `xqvm_py/tests/**`,
  `conformance/tests/**`. Tests exercise semantics but don't define
  them; the conformance *vectors* (under `conformance/vectors/`) are
  the authoritative cross-impl check and that's what the guard
  watches.

## The opcode-addition gate

**Adding a row to the `opcodes!` table in
`xqvm/src/bytecode/types/table.rs` is a change to VM semantics, and the
merge request that adds it argues in its description that the new opcode
clears all six clauses of the on-chain admissibility bar:**

1. **No floating point.** Integer arithmetic only, with every operation
   range-checked so that it faults rather than wrapping.
2. **No host I/O, wall-clock, or ambient state.** An instruction's
   result is a function of the program, the calldata and the VM's own
   state. Stated normatively in `spec/xqvm/SPEC.md` under Determinism.
3. **No nondeterministic iteration order.** Anything that walks a
   collection walks it in an order the spec fixes.
4. **Bounded allocation.** Every allocation is charged against a budget
   before it happens, and the budget is not escapable by a value the
   submitting account controls.
5. **Bounded per-instruction work.** The work one instruction performs
   is charged against the step budget before it happens, at a rate that
   scales with the data the program controls, so that a step is a unit
   of cost rather than a unit of dispatch.
6. **Specified behaviour, pinned by a vector.** The result and every
   fault the opcode can raise are specified normatively in
   `spec/xqvm/`, and a conformance vector covers the behaviour, failure
   paths included.

An opcode that cannot clear all six does not ship. The operation belongs
in `xqcp`, `xqsa`, the `xquad` API or a helper library, where it is
ordinary code rather than something every embedder has to trust.

### Why

The bar is a property of the instruction set rather than a decision each
embedder makes, and that is only true while every row on the table has
been held to it. The denied set is empty by construction, so an embedder
has nothing to gate -- but "by construction" names a construction
somebody has to carry out. Each of the six clauses was argued for every
opcode shipped so far, and none of those arguments was recorded where
the next proposal's reviewer would find it. An unwritten rule holds
nothing.

Clause 2 is the one most easily lost, because no test can fail it. An
opcode that read a clock would pass every conformance vector on the
machine that ran them, and split two hosts replaying the same program.
Clauses 4 and 5 are the ones most often half-satisfied: a charge levied
after the work rather than before it, or one that scales with something
other than the data the caller controls, reads as bounded until somebody
submits the worst case.

### Enforcement

There is none, deliberately. Every comparable process rule here ships a
`scripts/check-*.sh` guard that `verify:policy` runs -- the atomic
spec-MR rule, the commit-message grammar, the merge-request title, the
release notes. This one does not, and the absence is a decision rather
than an oversight.

The reason is what the gate asks for. A guard can prove that a trailer
exists, or that a file was touched. It cannot prove that the argument
the trailer claims was made is sound, and soundness is the whole of what
a reviewer is weighing here. Wiring in a grep would convert a
correctness argument into a compliance ritual, and the ritual is the
half that can be satisfied without doing the work.

So the gate lives in review. A reviewer who cannot find the six-clause
argument in the merge request description asks for it before approving,
the same way they would ask for a test.

## Commit and review conventions

See [`CONTRIBUTING.md`](../../CONTRIBUTING.md) for:

- Sign-off (DCO) requirements.
- Commit message format.
- Review turnaround expectations.
- AGPL license header requirements on new files.

---

### Guide Gitflow Protocol (`docs/guide/gitflow-protocol.md`)

# xquad git protocol

Two long-lived branches, `main` and `dev`. Every change lands on one of them
according to a single routing question, and every release is cut on a branch
that merges into `main`.

What the two branches mean depends on whether the project has reached 1.0. The
topology does not change at 1.0: two lines, release branches that merge to
`main`, one back-merge direction, one invariant. Only the routing question
changes.

## The invariant

`main` is never synchronised with `dev`. It is contained in it:

```sh
git merge-base --is-ancestor origin/main origin/dev
```

That command must exit 0, and CI checks it rather than leaving it to
convention, because the failure it catches is silent: two long-lived branches
drift apart commit by commit, and nothing reports it until a release is cut
from the branch that fell behind.

[`scripts/check-branch-containment.sh`](../../scripts/check-branch-containment.sh)
runs in `verify:policy` and judges two kinds of branch:

- **`dev`, as a signal.** Every push to `dev` runs it, against the highest
  release tag on `main` rather than `main`'s tip. The protocol back-merges
  after every tag, not after every change to `main`, so `dev` goes red when a
  release ships and stays red until its back-merge lands. Non-breaking work
  merging into `main` between releases does not turn it red. Merge requests
  into `dev` are not judged, so work continues in parallel.
- **`release/*`, as a gate.** A merge request from a release branch is judged
  on the branch's own tip, so a candidate that does not contain `main` cannot
  merge -- which is the case that would ship a release missing everything
  `main` had fixed.

`main` is never judged. It is the ref being checked against.

## Before 1.0

Cargo treats the leftmost non-zero field as the major, so `0.4.0` to `0.5.0` is
breaking and `0.4.0` to `0.4.1` is compatible. The minor is the breaking bump
and the patch carries everything else, fixes and features alike.

The routing question at merge-request time is **does this break?** Conventional
commits already force an answer to it with `!`, and the default merge request
template's Compatibility block asks it.

| Branch | Receives | Version |
| --- | --- | --- |
| `main` | every non-breaking change | `0.4.1-dev` |
| `dev` | every breaking change | `0.5.0-dev` |
| `release/v0.4.1` | cut from `main` | `0.4.1` |
| `release/v0.5.0` | cut from `dev` | `0.5.0` |

### Rules

- **No hotfix branch exists.** A bug fix is a non-breaking change landing on
  `main` like any other, and it ships in the next patch release.
- **`main` is a working branch** carrying a `-dev` version between releases.
  What shipped is read from the tag range, never from `main`'s log.
- **`main` takes merge requests, plus the reopening bump.** The one direct
  push `main` accepts is the version bump that reopens it after a release. CI
  fails any other commit that lands on `main` without a merge request; see
  [Protected branches](#protected-branches).
- **`dev` takes direct pushes for back-merges and version bumps, and nothing
  else.** Anything authored goes through a merge request. A direct push skips
  the guards scoped to a merge request -- the title check and the atomic
  spec-MR rule -- and breaking work is the work most likely to change VM
  semantics.
- **Both release branches merge to `main`**, so `release:auto-tag`, which
  looks for merge requests targeting `main`, needs no change.
- **Each release freezes its own source.** A patch freezes `main`, a minor
  freezes `dev`. The other line keeps moving.
- **Betas can come off either line.**

### Edge cases

- **At the cutover, bump `main` first, then branch `dev` from it.** Branch them
  as siblings off the tag and `main`'s bump is a commit `dev` cannot reach, so
  containment fails before any work lands.
- **When the minor ships, `main` becomes the new line.** It reopens at
  `x.y.1-dev` and `dev` takes `x.(y+1).0-dev` at its back-merge.
- **A fix landing while a release is open goes to the release branch directly.**
  Cascading it through `dev` reaches the release too, but drags every
  out-of-scope change on `dev` into the candidate with it.
- **Documentation publishes from tags, not from `main`.** `main` is unreleased,
  so `docs:publish` runs on a release tag and nothing else. A reader on an older
  line builds that tag's book locally; see the book's Stability appendix.
- **The old line's end of life is a downstream pin, not a date.** `0.4.x` stays
  patchable until every consumer that pins it has moved on.

## After 1.0

Breaking changes take the major, and the axis returns to the usual one: the
minor is features and the patch is fixes. The routing question becomes **is this
a fix for what shipped?**

| Branch | Receives | Version |
| --- | --- | --- |
| `main` | release and hotfix merges only | `1.0.0` |
| `dev` | every feature merge | `1.1.0-dev` |
| `release/v1.1.0` | cut from `dev` | `1.1.0` |
| `hotfix/v1.0.1` | cut from `main` | `1.0.1` |

### Rules

- **`main` carries exactly the last tag** and takes only release and hotfix
  merges. Features never target it.
- **Hotfixes squash.** The branch is cut from `main` and holds a few commits,
  and the squash subject becomes that patch's only changelog line. Write it as
  the fix, not as `fix: 1.0.1`.
- **Releases do not squash.** See below.
- **Feature branches merge to `dev` unsquashed**, so every commit reaches the
  changelog.

### Edge cases

- **"`main` is sparse" is a `--first-parent` property.** `git log main` still
  reaches every commit `dev` ever had. Only the first-parent view is one commit
  per release.
- **A missed back-merge reverts nothing.** A true merge keeps changes `main`
  made that the branch never touched. A missed back-merge costs divergence,
  which is slower and quieter than a revert.
- **`release:auto-tag` reads only `release/vX.Y.Z`.** A `hotfix/*` branch is not
  tagged automatically; widen the job or tag by hand when the first one ships.
- **A second release candidate cannot be tagged automatically** on a long-lived
  release branch either, because the version comes out of the branch name. Tag
  by hand, or give each candidate its own branch.
- **A hotfix during a release window needs two back-merges**, one into the open
  candidate and one into `dev`. CI checks both: the candidate's merge request is
  gated, and `dev` goes red.

## Merge and squash policy

| Merge | Squash? |
| --- | --- |
| feature or fix into `main` or `dev` | no |
| feature or fix into a `release/*` or `hotfix/*` branch | no |
| `release/*` into `main` | **no** |
| `hotfix/*` into `main` | yes |

The project setting is "Allow, off by default", which is what makes both rows
possible: the checkbox starts clear and a maintainer ticks it for the one merge
that squashes.

A release branch carries the whole history of the line it was cut from. Squash
it into `main` and none of those commits are ancestors of `main`, so the range
between two tags holds only the merge commit and one `release:` subject. A
changelog generator drops merge commits, and a `release: vX.Y.Z` entry on the
vX.Y.Z page is a tautology worth dropping too. The release notes come out empty,
and nothing reports it.

A true merge costs nothing here. `main`'s first-parent view stays exactly as
sparse, one commit per release, with the history behind it intact.

A hotfix branch is the opposite case. It is cut from `main`, so squashing it
discards nothing `main` did not already have, and one clean subject is the right
granularity for a patch.

### Merge commits

A merge commit is made in three places and nowhere else: a merge request
merging into `main` or `dev`, a release branch merging into `main`, and the
back-merge of `main` into `dev`. A feature branch never contains one. Every
merge commit a branch carries lands on the line behind the merge's second
parent, so a merge made inside a branch becomes permanent history on `dev` or
`main`.

- **Update a branch by rebasing it, never by merging its target in.**
  `git rebase origin/dev`, not `git merge origin/dev`, and the merge request's
  **Rebase** button rather than a merge.
- **Stack branches on each other's tips.** A branch stacked on another is
  rebased onto that branch's current tip, never synced by merging it in.
  `git rebase --update-refs` rebases a whole stack in one pass and moves every
  branch in it.
- **Merge a stack from the bottom up.** Merge the lowest merge request into its
  line first, with "Delete source branch" ticked, and GitLab retargets the next
  one onto the line. Never merge a stacked merge request into the branch below
  it: that merge commit lands on the lower branch and reaches the line with it.

Release branches are the exception. A minor's release branch carries every
merge `dev` made, and a fix merge request onto a release branch adds its own,
and both reach `main` through the release merge.

`verify:policy` enforces this on merge request pipelines:
`scripts/check-commit-messages.sh` fails a merge request whose branch carries a
merge commit, unless the branch is a `release/*` branch.

## Cutting a release

Cut the release branch when the milestone's scope is done, not when someone
wants something testable. A branch cut early is not a release candidate; it is a
snapshot that keeps moving, and the freeze it is supposed to impose never
happens. Pre-freeze snapshots are what betas are for.

1. Cut `release/vX.Y.Z` from its line -- `main` for a patch, `dev` for a minor.
   The source line freezes.
2. Set the version and regenerate the lockfiles:

   ```sh
   make set-version VERSION=X.Y.Z
   cargo check && uv lock
   cargo update -p xqvm --manifest-path fixtures/pallet-xqvm/Cargo.toml
   git commit -sam "chore: bump workspace to X.Y.Z"
   ```

3. Cut release candidates as work stabilises, the way a beta is cut: a
   throwaway branch off the release branch, the version set to `X.Y.Z-rcN`,
   the tag, and the branch discarded. The release branch stays at `X.Y.Z`.
   Only fixes land on it; a feature arriving here is the rule being broken.
4. Open the merge request into `main` with the `release` template and merge it
   without squashing. `release:auto-tag` pushes `vX.Y.Z` and the tag pipeline
   publishes.
5. Reopen `main` at the next patch `-dev` version, by pushing the bump
   straight to `main`:

   ```sh
   git switch -c reopen origin/main                 # local only, never pushed
   make set-version VERSION=X.Y.(Z+1)-dev
   cargo check && uv lock
   cargo update -p xqvm --manifest-path fixtures/pallet-xqvm/Cargo.toml
   git commit -sam "chore: reopen main at X.Y.(Z+1)-dev"
   git push origin HEAD:main
   git switch - && git branch -D reopen
   ```

   Until it lands, `release:validate` fails on `main`'s pipelines.
6. Back-merge `main` into `dev`, below. Until it lands, `dev` carries a
   standing red.

[`RELEASING.md`](../../RELEASING.md) has the full checklist.

## Pre-release snapshots

Use `-betaN` for anything published before the freeze.

Not `-devN`: that suffix already means unreleased and never published, and
reusing it makes the same string mean two opposite things.

Not `-previewN` or `-preN`: PEP 440 normalises both to `rc`. A Rust toolchain
keeps `0.5.0-preview1` and `0.5.0-rc1` apart, a Python one renders both as
`0.5.0rc1`. Publish a preview and then a candidate and the second upload is
rejected as a duplicate, after the first has already published elsewhere and
cannot be withdrawn.

A beta is cut on a throwaway branch, tagged, and never merged:

```sh
git switch -c beta/v0.5.0-beta1 origin/dev     # or origin/main, for a 0.4.x beta
make set-version VERSION=0.5.0-beta1
cargo check && uv lock
cargo update -p xqvm --manifest-path fixtures/pallet-xqvm/Cargo.toml
git commit -sam "chore: cut 0.5.0-beta1"
git tag v0.5.0-beta1 && git push origin v0.5.0-beta1
git switch - && git branch -D beta/v0.5.0-beta1
```

The published version comes from the tree, not from the tag, so the tagged
commit has to carry the beta version -- which is the whole reason a commit
exists at all here. The lockfiles have to agree with it too: `release:crates`
publishes with `--locked`. The line itself never moves, so there is no second
bump to remember and nothing to undo, and the procedure is identical off either
line.

The trade is that the beta's commit is reachable only through its tag. A `v*`
tag is protected and only a Maintainer can delete one, and git does not collect
a tagged commit, so that is enough for something ephemeral by construction.

A beta publishes to crates.io and PyPI and gets no release page and no
documentation, exactly like a release candidate: `release:notes` and
`docs:publish` match `cliff.toml`'s tag pattern, which is three numeric fields
and nothing else.

## Back-merging

Back-merge `main` into `dev` after every tag on `main`.

The version sites always conflict, because the two branches hold different
versions by design and both have moved since the last back-merge. A plain
`git merge` stops with every manifest carrying conflict markers, and
`make set-version` cannot repair that: it reads every site before writing any,
and a manifest with conflict markers does not parse.

So take the version out of the merge before it starts. Carry `dev`'s version
onto a scratch branch off `main`, and merge that instead. Both sides then made
the same change to every version line, git has nothing to reconcile there, and
whatever still conflicts is a real conflict:

```sh
git fetch origin
git switch -c backmerge/vX.Y.Z origin/main       # local only, never pushed
make set-version VERSION=0.5.0-dev               # dev's version, not main's
cargo check && uv lock
cargo update -p xqvm --manifest-path fixtures/pallet-xqvm/Cargo.toml
git commit -sam "chore: carry dev's version into the vX.Y.Z back-merge"

git switch dev && git pull --ff-only
git merge --no-ff backmerge/vX.Y.Z               # only real conflicts remain
git push origin dev
git branch -d backmerge/vX.Y.Z
```

Everything else `main` changed in those manifests -- a new dependency, a
changed constraint -- merges in normally, because only the version lines were
taken out of play. If `Cargo.lock` or `uv.lock` still conflicts, both sides
changed dependencies: resolve the manifests, take either lockfile, and
regenerate it with `cargo check` and `uv lock`.

Before 1.0 expect real conflicts. `main` is a working line carrying every
non-breaking feature and fix, not a line that only receives releases, so the
two branches diverge in code between tags. A back-merge here is an ordinary
merge with a mechanical prologue, not a rubber stamp.

This stays manual on purpose. The auto-resolvable case is the one where
*nothing but* the version sites conflict, which is the post-1.0 shape, and
before 1.0 it is the exception. Machinery whose fast path rarely fires is wrong
more often than it is right, and a back-merge that silently resolves a real code
conflict is the worst outcome available here.

Avoid a `.gitattributes` `merge=ours` driver too. It needs per-clone
configuration and fails open when someone has not set it up, silently taking the
wrong side.

## Protected branches

Protection and push access are separate settings, and the distinction is what
makes this work. *Protected* is what makes `test:wasm`, `test:substrate` and the
`hardware:*` tier run unconditionally rather than path-gated. *Push access* is
who may write without a merge request.

| Branch | Protected | Push | Merge | Why |
| --- | --- | --- | --- | --- |
| `main` | yes | Code owners | Maintainers | The line consumers pin. Pushes are open to the code owners for the reopening bump only, and CI enforces that. |
| `dev` | yes | Maintainers | Maintainers | The back-merge is a push. Closing it would tax the one routine operation. |
| `release/*` | yes | Maintainers | Maintainers | Protection alone is the point: a release candidate runs the full CI tier. |

Force push is off on all three, and code owner approval is off on all three.
`v*` tags are protected separately, Maintainers only.

Push access cannot say "version bumps only", so a CI check says it for `main`.
[`scripts/check-main-direct-push.sh`](../../scripts/check-main-direct-push.sh)
runs in `verify:policy` on every push to `main` and judges each commit the push
put on `main`'s first-parent line. A commit passes if it is a merge request's
merge commit, or if every file it touches is identical before and after once
the old and new versions are masked out. Anything else turns `main` red. The
check detects and does not prevent: the commit is already on `main` when it
runs, and the fix is a revert through a merge request.

*Code owners* means the users listed in
[`.gitlab/CODEOWNERS`](../../.gitlab/CODEOWNERS), named one by one in the
branch's push setting. GitLab does not link the two, so a change to either one
is made by hand in the other.

Opening pushes to the code owners was a trade. A release costs one merge
request fewer, and in exchange a stray `git push origin main` from a code
owner's clone now lands, where it used to be rejected. The check turns that into a red
pipeline rather than a silent change.

---

### Guide Solverquip Testing (`docs/guide/solverquip-testing.md`)

# Testing SolverQuip against a Quip network

`SolverQuip` (the [`xqsa`](../xqsa/) `"quip"` backend, [`xqsa/quip/`](../xqsa/quip/))
submits Ising jobs to a live Quip Network chain, waits for a solver fleet to
return a solution, decodes the best one, and runs an energy canary against the
chain's recorded result. The gated live suite
([`xqsa/tests/quip/test_live.py`](../xqsa/tests/quip/test_live.py), pytest marker
`quip`) exercises that whole path end to end against a real chain.

This guide covers running that suite in the two environments you can point it
at:

- a local **DevNet** -- `make localdev` in the sibling `nodes.quip.network`
  repo stands up a self-contained single-validator chain plus a faucet and one
  CPU miner. Fast, offline, and you control the whole stack.
- the public **aglais** network -- a real multi-node deployment with a
  third-party GPU solver fleet and a rolling runtime. This is the target for
  validation the DevNet cannot fully stand in for (real solvers, real timing,
  real runtime upgrades). Its faucet is healthy, so the funded tier runs there
  too.

The older public **TestNet** is retired and is no longer a target. It ran the
H3 suite (sr25519 + ML-DSA-44) that this signing layer no longer speaks, and
its faucet stopped resolving before it was shut down. Nothing in this guide
points at it.

Both are opt-in and gated on `QUIP_RPC_URL`, so they never run in the default
`make test` or in CI. The [MR template](../.gitlab/merge_request_templates/default.md)
lists `make test-quip` as an Optional Check for any change to SolverQuip.

## The make targets

The three quip targets live in the [`Makefile`](../Makefile):

| Target | Runs | Needs a chain? |
| --- | --- | --- |
| `make test-quip-sign` | signing tests ([`test_signing.py`](../xqsa/tests/quip/test_signing.py)) -- pure-Python SCALE / keystore / extrinsic | no |
| `make test-quip-e2e` | live suite ([`test_live.py`](../xqsa/tests/quip/test_live.py)) against a chain | yes |
| `make test-quip` | both of the above (the MR-template Optional Check) | yes (for the e2e leaf) |

`make test-quip-sign` is also what the CI `test:quip` job runs; it needs no
chain and always executes. `make test-quip-e2e` hard-errors when `QUIP_RPC_URL`
is unset (rather than reporting a hollow all-skipped pass), so the aggregate
`make test-quip` requires the devnet env vars below for its e2e leaf. All three
install the `[quip]` extra themselves: `uv sync --extra quip`, which also
rebuilds `xqffi` when its Rust sources changed, then pytest through
`uv run --no-sync`.

### Environment variables

| Variable | When | Meaning |
| --- | --- | --- |
| `QUIP_RPC_URL` | required for e2e | chain RPC, `ws://` (DevNet) or `wss://` (aglais). `make test-quip` / `make test-quip-e2e` pass it through explicitly. |
| `QUIP_FAUCET_URL` | optional | faucet **base** URL; the funded fixture POSTs to `<QUIP_FAUCET_URL>/request`. `SolverQuip` reads it too, as the faucet `autofund` draws from, but only when its RPC URL also comes from `QUIP_RPC_URL`; the fixtures pass `url=` and fund directly. Without it only the read-only connectivity tests run. |
| `QUIP_TOPOLOGY` | optional | registered topology hash for `SolverQuip` to target instead of the chain default. Unlike the rest of this table it configures the solver, not the harness, so it also applies outside the tests. Setting it un-skips the override tier. |
| `QUIP_MINER_PROBE_TIMEOUT` | optional (default 60) | how long the `solving_miner` probe waits before skipping the end-to-end tier. Raise it on a slow or remote fleet. |

Any variable exported in your shell -- or passed as `make VAR=val` -- reaches
pytest, so you can set `QUIP_MINER_PROBE_TIMEOUT` alongside the two the
recipe forwards. On macOS, whose python.org and uv Pythons ship no CA
bundle, `xqsa.quip.metadata.connect` and the suite's faucet requests verify
TLS against certifi's bundle unless `SSL_CERT_FILE` or `SSL_CERT_DIR` is
set, or `WEBSOCKET_CLIENT_CA_BUNDLE` names an existing file or directory, so
aglais needs no CA export.

### The test tiers

The live suite runs in three tiers, by what a test needs from the chain:

- **read-only** (`TestConnectivity`): any reachable chain. Without
  `QUIP_FAUCET_URL`, these are the only tests that run.
- **funded**: `QUIP_FAUCET_URL` set. The `funded_keystore` fixture generates a
  fresh keystore and draws one faucet drip into it; `second_funded_keystore`
  draws a second for another account and skips its tests if the funding has not
  landed within 60 s. A full session makes up to three faucet requests.
- **solved**: an answer to the order. Some tests wait for the solver fleet
  through the `solving_miner` fixture, which proposes a throwaway order and
  skips the tier cleanly when nothing answers within `QUIP_MINER_PROBE_TIMEOUT`.
  Others answer their own orders (self-solve, below) and need no fleet.

| Class | Covers | Needs |
| --- | --- | --- |
| `TestConnectivity` | chain constants, the topology decode, the V14 metadata shim, the signed-extension list, the topology override | a reachable chain; a few tests fund an account; the override test needs `QUIP_TOPOLOGY` |
| `TestSubmitPath` | the balance pre-check, a proposal that times out and is recovered, auto-reclaim of an expired unanswered order | funding; patches `MIN_DEADLINE_BLOCKS` to 1 so an order can expire inside the test |
| `TestJobOrder` | `create_order`, `set()`, `quote()` and `submit()`, the stored order read back, `set()` refused after submit | funding |
| `TestReceipt` | an unanswered order reclaimed through its receipt (`Reward  reclaimed`), `list_orders()`, a repeated reclaim refused | funding; patches `MIN_DEADLINE_BLOCKS` to 1 |
| `TestEndToEnd` | spin, binary and native round trips; a receipt rebuilt in a fresh session (reclaims and skips if unanswered); the deprecated `query()` and `status()` | the fleet (`solving_miner`) |
| `TestLeader` | a tied order follows our lead (skips without a tie); a four-vector answer comes back whole (skips if `MaxSolutions` is below 4) | funding and self-solve; the tie test also needs the fleet |
| `TestPayoutRules` | a `TopNEqual(2)` split matches the claim; `TopNWeighted` refused below runtime 119 (skips on 119 and later) | `second_funded_keystore` and self-solve |
| `TestAccess` | an allowlisted account answers; a type allowlist admits by type; an unlisted solver is refused (`NotEligibleSolver`) | self-solve; two tests use `second_funded_keystore` |
| `TestQualityFloors` | the ceiling drops the vector above it (self-answered); an order with floors comes back whole (reclaims and skips if no solver meets them) | funding; the second test needs the fleet |

Self-solve tests answer their own orders. A helper registers the test account as
a `Cpu` solver if it is not one, then proposes the order and submits its answer
in one `Utility.batch_all`, predicting the order id from `NextOrderId` and
retrying up to three times when another proposal takes it; payout tests then
send `claim_reward`. Registration is permanent: every account a run registers
stays on chain as a `Cpu` solver. The fixtures use throwaway keystores for that
reason, so a run adds up to two solver accounts to the network.

A fresh order's first answer took 1 to 25 blocks on aglais, measured across
twelve answered orders on 2026-09-18 (median 1 to 2). The suite therefore runs
with `deadline_blocks=100` and `block_wait=10` (`SOLVER_KWARGS` in
`test_live.py`), so a slow answer still lands before the deadline.

## Common prerequisites

- `uv sync --extra quip` in this repo (installs `substrate-interface` + the
  `quip-signer` extension). The make targets do it for you. `uv run` keeps the
  extra, but a plain `uv sync`, including the one in `make deps-py`,
  `make test-py` and `make repl`, removes it again; re-run
  `uv sync --extra quip` after any of them, or `SolverQuip` fails with
  `substrate-interface is not installed`.
- **Apple Silicon:** `quip-signer` 0.3.0 publishes no macOS wheel, so the first
  sync after the pin moved builds it from the sdist and needs a local Rust
  toolchain. Expect minutes, not seconds. Slow here is expected; a failure is
  not, and almost always means no `cargo` on `PATH`.
- A hybrid keystore (sr25519 + FN-DSA-512). The funded test fixture generates
  one on demand; for a manual solve, `load_or_generate_keystore(path)` creates a
  `0600` seed file. A keystore written under the old H3 suite is worthless: the
  format check rejects it, and H4 derives a different account id from the same
  master seed, so the balance does not carry over either. Generate a fresh one
  and fund that.

## Option A -- local DevNet

The DevNet is the fast, offline way to exercise the full propose -> discover ->
solve -> submit path without a public chain. It runs the same node image the
public stack runs, so a spec-matched DevNet is a good pre-flight before an
aglais run.

There are no pinned `:v0.2` image tags any more. `make localdev` resolves the
newest published tag per image at run time (`scripts/newest-tags.py` writes
`data/localdev.tags.env`), because CI does not move `latest` for rc builds and
tracking `latest` would silently hold localdev on an old build. The public
stack selects images by `CHANNEL` instead, defaulting to `beta`; `stable` and
`beta` both run aglais and differ only in how far ahead of the release line
they sit. A `QUIP_*_TAG` pinned in `.env` passes through either path untouched.

Sibling repos under `~/code/gitlab.com/quip.network/`:

- `nodes.quip.network` -- DevNet orchestration (compose, Makefile, coordinator
  invocation); this is where you run `make localdev`.
- `quip-miner` -- the mining stack: the `quip-coordinator`, `quip-miner-exec`
  and `quip-mock-miner` crates. Only needed to read miner internals, such as
  whether a miner registers itself as a mempool solver. Renamed from
  `quip-protocol`, and no longer a Python image source; it is a Rust workspace
  as of the v0.3 line.
- `quip-validator` -- the substrate node (runtime, pallets) + the `quip_signer`
  binding. Renamed from `quip-protocol-rs`.

### Bring-up

Prerequisites: Docker Desktop running; `nodes.quip.network` on the latest
`main`. The `quip.network` GitLab container registry is public, so the
`make localdev` image pulls resolve anonymously: `docker login` is NOT
required (logging in only raises registry rate limits). If you do want to
log in, use `docker login registry.gitlab.com` with a `read_registry` PAT.

From `nodes.quip.network`:

    cp env.example .env          # then set PUID=0 / PGID=0 (see macOS notes)
    make localdev                # wipe + pull + seed topology + bring up the stack

`make localdev` is idempotent and destructive by design: it tears down, wipes
the chain, resolves and pulls the newest published image tags, brings up the
validator + faucet, seeds the `advantage2_system1` topology + difficulty via
`//Alice` sudo, then starts the miner. Seeding now lives inside the coordinator
binary (`quip-coordinator seed-chain`); the old
`scripts/seed-advantage2-topology.py` is gone. Success prints
`localdev stack up` with the dashboard / RPC / faucet URLs. The miner
self-bootstraps: it faucet-funds its account, registers as a PoW miner, and
auto-registers as a mempool solver (`mempool solver guard: registered (cpu)`).
Bootstrap completes ~60-90s after the miner container starts.

**macOS (Apple Silicon):** set `PUID=0` and `PGID=0` in `.env`. Do NOT use
`PGID=$(id -g)` -- on macOS that is `20` (`staff`), which collides with the
miner image's GID `20` (`dialout`) and crash-loops the entrypoint's group
remap. The miner and faucet images are amd64-only and run under emulation, so
the first pull is slow and CPU is higher than native -- expected, not a hang.

### Verify the stack

- Blocks + runtime version: read `state_getRuntimeVersion` over the RPC and
  record what it says. Nothing is pinned here on purpose -- localdev tracks the
  newest published image, so its runtime moves whenever one is published, and a
  figure written down in this guide would be stale within the week. Read
  aglais the same way; a localdev stack brought up from current images should
  be at or ahead of it.
- Containers: `docker ps` -- all `*-localdev` Up, `quip-cpu-localdev` not
  Restarting.
- Miner healthy: `docker logs quip-cpu-localdev | grep -iE "solver guard: registered|topology .from chain.|mempool=on"`
  -- expect the solver registered, the DevNet `DefaultTopology` hash, and
  mempool on. `make localdev` seeds the coordinator's built-in
  `advantage2-system1` preset (`allowed_h = [-1000, 0, 1000]`, hashing to
  `0xfb91813b...`), which is a different topology from the `allowed_h = [0]`
  one aglais runs. `SolverQuip` reads whichever the chain reports, so nothing
  in xquad needs to know which you are on -- but never carry a hash across.

### Host access

The stack exposes everything through Caddy on `:20049`:

- RPC: `ws://localhost:20049/rpc` (use `http://localhost:20049/rpc` for HTTP).
- Faucet: base `http://localhost:20049/api/faucet` (the suite POSTs
  `.../request`).

If you prefer raw ports, add a `ports:` mapping for `quip-validator`
(`9944:9944`) and `quip-faucet` (`8087:8087`) -- the localdev stack does not
publish them by default. Note that `docker-compose.override.yml` is no longer
the file to put that in: the localdev overrides moved to the explicit
`docker-compose.localdev.yml`, which plain `docker compose` does not auto-load,
so use a differently-named override and pass it with `-f`.

### Run the suite

    make test-quip \
        QUIP_RPC_URL=ws://localhost:20049/rpc \
        QUIP_FAUCET_URL=http://localhost:20049/api/faucet

A fresh order solves in ~2-4 blocks at localdev block time (~14-21s propose to
on-chain solution under emulation).

### Teardown

From `nodes.quip.network`: `make down` (stop), `make clean-chain` (wipe chain
only), `make clean` (full reset). Re-run `make localdev` after any wipe -- state
is non-persistent and order ids reset, so never hard-code one.

### Troubleshooting

- **Miner crash-loops "at least one validator URL is required":** the persisted
  `data/config.toml` has an explicit empty `validators = []` (old-schema
  leftover) that defeats the built-in fallback. On latest `main`,
  `make localdev` overwrites the config; if you hand-edited it, delete the
  `validators` line or set `validators = ["ws://quip-validator:9944"]`.
- **Stale checkout of `nodes.quip.network`:** a checkout still on the `v0.2`
  line does not merely lag, it fails. The miner images moved to a separate
  `v0.3` repository line, `data/config.toml` changed schema (`make updateconfig`
  migrates it), and chain seeding moved into the coordinator. Pull `main` before
  blaming the stack.
- **0 solutions with a healthy-looking miner:** confirm the node's runtime with
  `state_getRuntimeVersion` and that the miner logs the `mempool_producer` /
  `work_scheduler` stack. `make localdev` resolves the newest published tags on
  every run, so a stale image is far less likely than it used to be; a stale
  `.env` pinning `QUIP_*_TAG` is the remaining way to get one.
- **macOS miner crash-loop before any app logs:** `PGID=20` group collision --
  set `PUID=0` / `PGID=0`.
- **Faucet:** on a fresh bring-up the miner's first requests hit
  `Connection refused` for ~30s until the faucet is up, then fund on retry
  (expected). After that the faucet's two gates apply, as described
  for Option B below: a repeat request inside the ~5s window is a 429
  that `fund_from_faucet` retries once, and an account already holding
  more than one drip gets a 403 that is not retried. Start from an
  empty keystore, or spend the account below one drip.

## Option B -- the public aglais network

aglais is a real, multi-node deployment with a registered solver fleet. You do
not control the solvers or the topology -- you propose jobs and a third-party
fleet solves them. It runs the H4 hybrid suite (sr25519 + FN-DSA-512), which is
what the signing layer speaks.

### Coordinates (verify before a run -- these move)

- **RPC:** the `aglais` preset in `xqsa/quip/networks.py` is the source of
  truth; it points at bootnode-1's validator RPC (bootnode-2 and bootnode-3
  serve the same chain). The commands below spell out its current RPC and
  faucet values; if they disagree with the preset, the preset wins. Any
  node can fall behind the chain tip and serve a stale, frozen view -- balances read as 0 and freshly submitted extrinsics look
  like they never land, even though the chain is live. Confirm the node is
  caught up before trusting reads (see the liveness check below), and point
  `QUIP_RPC_URL` at a different RPC if the one you are on is not syncing.
- **Faucet:** the `aglais` preset's faucet coordinate in
  `xqsa/quip/networks.py` is the source of truth, healthy as of 2026-09-15
  (`/health` returns `{"status":"ok"}`; the bare root returns 404, which is not a
  fault). Set `QUIP_FAUCET_URL` to it and the funded tier runs. The faucet
  [gates requests two ways](https://gitlab.com/quip.network/faucet/-/blob/10ca67676777da0031be5bf8dd2f9ed6967d269a/README.md?plain=1#L54-62). A `429` with
  `retry_after_seconds` is the per-destination rate limit, a window of
  about 5s. A `429` without `retry_after_seconds` is a
  [duplicate request still in flight](https://gitlab.com/quip.network/faucet/-/blob/10ca67676777da0031be5bf8dd2f9ed6967d269a/src/handlers.rs#L87-90) for the
  same destination. Both are retryable, and `fund_from_faucet` retries
  a `429` once, waiting 5s when no `retry_after_seconds` is given; a
  `429` asking for more than 30 s raises `QuipFaucetError` instead of
  waiting (`MAX_RETRY_AFTER_SECONDS`). A `403` `destination already funded` with
  `free_balance_plancks` is the balance ceiling: the faucet refuses any
  account holding more than one drip (10 AGLS), and it is not retryable.
  Spending below the ceiling makes the account eligible again, so there is
  no lifetime quota; a fresh keystore works because it starts empty.
- **Block time:** ~6s.
- **Runtime:** advances over time; read it with `state_getRuntimeVersion`
  rather than assuming a pinned value. The signer reads `transactionVersion`
  from the chain, so a bump to that number alone does not break submission.
  The signed-extension list and its order are fixed client-side
  (`SIGNED_EXTENSIONS` in `xqsa/quip/signing.py`), so a runtime that adds,
  removes, reorders or reshapes an extension breaks signing. After an upgrade,
  run `test_live.py::TestConnectivity`, which checks that list against the
  chain.
- **Metadata:** the runtime serves Metadata V16, which `scalecodec` cannot
  decode. Build clients through `xqsa.quip.metadata.connect`, which pulls V14
  from the versioned runtime API; a stock `SubstrateInterface` fails with
  `Index '16' not present in Enum type mapping` (see the gotcha below).
- **Spec id:** `DEFAULT_ISING_SPEC_ID = 0x8f46f3a3...` (== chain
  `DefaultIsingSpecId`); `MinReward` = 1e12 planck (1 UNIT).

Quick version + liveness check. A synced node reports `isSyncing == false` and
`currentBlock == highestBlock`; if it is behind, its state reads are stale:

    uv run python -c "from xqsa.quip.metadata import connect; \
        from xqsa.quip.networks import NETWORKS; \
        s=connect(NETWORKS['aglais'].rpc); \
        print('runtime', s.rpc_request('state_getRuntimeVersion',[])['result']); \
        print('sync   ', s.rpc_request('system_syncState',[])['result']); \
        print('health ', s.rpc_request('system_health',[])['result'])"

### Topology

The same `advantage2_system1` graph hashes differently per deployment (each
network's allowed-value specs fold into the hash), so `SolverQuip` resolves the
topology at construction from `topology=`, then `QUIP_TOPOLOGY`, then chain
`QuantumPow.DefaultTopology`. There is no pinned fallback in the codebase, and a
topology that resolves from none of the three raises rather than selecting a
hash no chain would accept. On aglais `DefaultTopology` is
`0xcbec1eb4...` over 4577 nodes / 41514 edges, read live on 2026-09-15. Never
assume the DevNet hash on aglais or vice versa.

`solve()` does not consult `MineableTopologies`. That set is the chain's active
mining set: it gates `submit_proof`, and so block production, not the compute
mempool. Nothing binds an order to a topology at `propose_job` -- an order
carries its nodes, edges and coefficients inline and no topology hash at all --
so the chain cannot perceive which topology an order was built against, and
mineability cannot affect whether an order is admitted or answered.

To exercise that, aglais carries a permanently registered non-mineable topology:
a complete graph on 16 nodes (K16), hash
`0x830abc16c0b28b26f119f3a1279d07811bdeb9c0234d7f442ae46d6698d9a797`, registered
at block 232673. It is registered, not mineable, and not the default. Point
`QUIP_TOPOLOGY` at it to run the whole suite against it:

    QUIP_MINER_PROBE_TIMEOUT=180 \
      QUIP_TOPOLOGY=0x830abc16c0b28b26f119f3a1279d07811bdeb9c0234d7f442ae46d6698d9a797 \
      make test-quip \
        QUIP_RPC_URL=wss://bootnode-1.aglais.quip.network:20049/rpc \
        QUIP_FAUCET_URL=https://faucet.aglais.quip.network

Reuse K16; do not register another. There is no `unregister_topology`, so every
registration is permanent.

### Run the suite

    QUIP_MINER_PROBE_TIMEOUT=180 \
      make test-quip \
        QUIP_RPC_URL=wss://bootnode-1.aglais.quip.network:20049/rpc \
        QUIP_FAUCET_URL=https://faucet.aglais.quip.network

The submit-path tests always run; the `TestEndToEnd` cases are gated behind the
`solving_miner` probe. If the probe skips (no solution within the window), retry
once when the fleet is active.

### Learnings / gotchas

- **Discovery gap (important):** the fleet discovers jobs ONLY from live
  `JobProposed` events -- there is NO storage backfill of pre-existing open
  orders. An order proposed BEFORE a solver subscribed is invisible to it
  forever. Always test with a freshly proposed order; a fresh one usually gets
  its first answer within 1 to 2 blocks while the fleet is live (the slowest
  measured took 25), but historical orders that predate the current fleet stay
  unsolved, which looks like "nothing solves" if you only inspect old orders. (Known miner-side follow-up, not a client bug.)
- **Runtime can advance under you:** re-read `state_getRuntimeVersion` each
  session; do not hard-code pinned facts. The signer adapts `transactionVersion`
  from the chain, but the signed-extension list is fixed client-side, so an
  upgrade that changes it breaks submission. Run
  `test_live.py::TestConnectivity` after every known upgrade.
- **Metadata V16 blocks a stock client:** `substrate-interface` decodes through
  `scalecodec`, whose `MetadataAll` enum ends at V14, so
  `SubstrateInterface.init_runtime()` raises `ValueError: Index '16' not present
  in Enum type mapping` against any runtime from `specVersion 116` on. No
  release of either library decodes V16. `xqsa.quip.metadata` fetches V14
  through `state_call("Metadata_metadata_at_version", 14)` instead, and
  `SolverQuip` builds its client that way; ad-hoc scripts must do the same. The
  shim falls back to `state_getMetadata` for older DevNet images, and raises
  `QuipMetadataError` naming the served version when neither path decodes.
- **Watch for a lagging RPC node:** a node that has fallen behind the chain tip
  serves a frozen snapshot, and every read then looks wrong -- balances at 0,
  funded accounts "missing", submitted extrinsics that never land -- while the
  chain is actually fine. A faucet request returning `200 + extrinsic_hash` is NOT
  proof of funding. Check `isSyncing == false` and `currentBlock == highestBlock`;
  if a node is not syncing, point `QUIP_RPC_URL` at a different RPC URL.
- **Lifecycle:** an order's effective expiry is `created_at + deadline_blocks`,
  tightened to `first_solution_at + block_wait` once a first solution lands. An
  order that expires with zero solutions can be auto-reclaimed (SolverQuip does
  this and raises a job-failed error, releasing the reserved reward).
- **Fleet answers can be valid but suboptimal:** the aglais fleet runs
  heuristic GPU solvers and may return a correct, chain-consistent assignment
  that is not the global optimum -- observed: a 3-var binary model whose true
  optimum is -2 came back as -1, while the controlled DevNet CPU fleet returns
  -2 deterministically. Do not assert global optimality against a live fleet.
  `TestEndToEnd` asserts only pipeline correctness (`energy_matches_chain`, a
  returned solution, domain feasibility); encoding and optimality correctness
  are guarded deterministically offline by
  `test_client.py::test_encoded_problem_argmin_decodes_to_optimum`.
- **Direct `pytest` invocation:** export `QUIP_RPC_URL` / `QUIP_FAUCET_URL` as
  environment variables before the command. Passing them as trailing `KEY=VAL`
  args is a make-ism (`make VAR=val`); pytest reads trailing `KEY=VAL` tokens
  as file paths and errors with "file or directory not found".

## Manual smoke solve (optional)

Beyond the pytest suite, a single small model round-trips the propose + decode +
energy-canary path. Set `QUIP_KEYSTORE` and fund the keystore, then:

    export QUIP_KEYSTORE=/tmp/quip-ks.json
    uv run python - <<'PY'
    from xqsa.quip import SolverQuip
    from xqvm_py.xqmx import XQMX
    m = XQMX.spin_model(2)
    m.set_linear(0, 1); m.set_quadratic(0, 1, -1)   # brute-force optimum = -2
    r = SolverQuip.for_network("aglais").solve(m)   # or "devnet"
    print("energy:", r.energy, "| matches chain:", r.metadata["energy_matches_chain"])
    PY

Expect `energy_matches_chain: True`; energy is typically `-2.0`, though a
heuristic fleet may return a valid but suboptimal value. Get the account id to
fund via
`load_or_generate_keystore('/tmp/quip-ks.json').account_id.hex()` and POST
`{"dest":"0x<accountid>","amount":10000000000000}` to the faucet's `/request`
endpoint.

## Inspecting chain state

Independent confirmation lives in `QuantumComputeMempool` storage: `JobOrders`
(status / solution_count / first_solution_at), `OrderSolutions`,
`OrderFrontRunner` (winning solver + `energy_milli`, the natural energy x1000),
`OrderResults` (populated at finalization), and `Solvers` (each with
`solutions_submitted`). Topology lives under
`QuantumPow.{DefaultTopology,MineableTopologies,RegisteredTopologies}`. Strip
the large `ising_params` arrays when dumping orders.

## Which to use when

- **DevNet** for fast, deterministic, offline iteration and as a spec-matched
  pre-flight before an aglais run. You control the fleet, so a fresh order
  always solves quickly.
- **aglais** for the real-solver, real-timing, real-runtime validation the
  DevNet cannot reproduce -- run it before landing a SolverQuip change that
  touches submission, decoding, or the lifecycle. It is the only public target;
  the older TestNet is retired.

---

### Book Src SUMMARY (`docs/book/src/SUMMARY.md`)

# Summary

[Introduction](README.md)

- [Getting Started](start/README.md)
  - [First Problem](start/first-problem.md)
  - [What Happened](start/what-happened.md)

- [Concepts](concepts/README.md)
  - [Ways to Use XQuad](concepts/ways-to-use.md)
  - [Quadratic Models](concepts/quadratic-models.md)
  - [Three Programs](concepts/three-programs.md)
  - [Backends](concepts/backends.md)

- [Modelling and Solving](modelling/README.md)
  - [Inputs and Model Shape](modelling/inputs-and-model.md)
  - [Expressions](modelling/expressions.md)
  - [Objectives](modelling/objectives.md)
  - [Constraints](modelling/constraints.md)
  - [Control Flow](modelling/control-flow.md)
  - [Outputs and Decoding](modelling/outputs-and-decoding.md)
  - [Compiling](modelling/compiling.md)
  - [Running Programs](running/README.md)
  - [Verification](running/verification.md)
  - [Solving Overview](solving/README.md)
  - [Local Solvers](solving/local.md)
  - [D-Wave QPU](solving/dwave-qpu.md)
  - [Quip Network](solving/quip-network.md)
    - [Your First Quip Job](solving/quip-first-job.md)
    - [Job Orders](solving/quip-orders.md)
  - [Energy and Precision](solving/energy-and-precision.md)

- [Examples and Recipes](examples/README.md)
  - [Using the Examples](examples/using-examples.md)
  - [Max-Cut](examples/maxcut.md)
  - [Graph Coloring](examples/graph_coloring.md)
  - [Maximum Independent Set](examples/max_independent_set.md)
  - [Vertex Cover](examples/vertex_cover.md)
  - [Travelling Salesman Problem](examples/tsp.md)
  - [Knapsack](examples/knapsack.md)
  - [Bin Packing](examples/bin_packing.md)
  - [Set Cover](examples/set_cover.md)
  - [Weighted Set Cover](examples/weighted_set_cover.md)
  - [Number Partition](examples/number_partition.md)
  - [Portfolio Optimization](examples/portfolio_opt.md)
  - [Portfolio Rebalance](examples/portfolio_rebalance.md)
  - [Max-3-SAT](examples/max3sat.md)
  - [Cubic Optimization](examples/cubic_opt.md)
  - [Quartic Optimization](examples/quartic_opt.md)
  - [Cookbook](cookbook/README.md)
    - [Permutations](cookbook/permutations.md)
    - [Assignment](cookbook/assignment.md)
    - [Selection Under Budget](cookbook/selection-under-budget.md)
    - [Mutual Exclusion](cookbook/mutual-exclusion.md)
    - [Soft vs. Hard Constraints](cookbook/soft-vs-hard-constraints.md)
    - [Integer Scaling](cookbook/integer-scaling.md)

- [XQVM Reference](xqvm/README.md)
  - [VM Architecture](xqvm/machine-model.md)
  - [Loop Stack](xqvm/loops.md)
  - [Calldata and Outputs](xqvm/io.md)
  - [Execution](xqvm/execution.md)
  - [Assembly](xqvm/assembly.md)
  - [Assembly Examples](xqvm/assembly-examples.md)
  - [Instructions](xqvm/instructions/README.md)
    - [Control Flow](xqvm/instructions/control-flow.md)
    - [Register I/O](xqvm/instructions/register-io.md)
    - [Stack Manipulation](xqvm/instructions/stack-manipulation.md)
    - [Arithmetic](xqvm/instructions/arithmetic.md)
    - [Comparison](xqvm/instructions/comparison.md)
    - [Logical Boolean](xqvm/instructions/logical.md)
    - [Bitwise](xqvm/instructions/bitwise.md)
    - [Allocators](xqvm/instructions/allocators.md)
    - [Vector Operations](xqvm/instructions/vector-ops.md)
    - [Index Math](xqvm/instructions/index-math.md)
    - [Coefficient Access](xqvm/instructions/coefficient-access.md)
    - [Grid Operations](xqvm/instructions/grid.md)
    - [High-Level Constraints](xqvm/instructions/constraints.md)
    - [Energy Evaluation](xqvm/instructions/energy.md)
  - [Opcode Reference](xqvm/opcodes.md)
  - [Bytecode Format](xqvm/bytecode-format.md)
  - [Verifier](xqvm/verifier.md)
  - [CLI](xqvm/cli/README.md)
    - [Assemble](xqvm/cli/asm.md)
    - [Disassemble](xqvm/cli/dism.md)
    - [Run](xqvm/cli/run.md)
    - [Verify](xqvm/cli/verify.md)
  - [Limits and Errors](xqvm/limits-and-errors.md)

- [Embedding and Internals](embedding/README.md)
  - [Builder API](embedding/builder-api.md)
  - [Pallet Fixture](embedding/pallet.md)
  - [On-Chain Admissibility](embedding/admissibility.md)
  - [Conformance](embedding/conformance.md)

---

[Glossary](appendix/glossary.md)
[Stability](appendix/stability.md)
[Spec Index](appendix/spec-index.md)
[Contributing to These Docs](appendix/contributing.md)

---

### Design Plans 2026 03 24 Xqvm Spec Migration (`docs/design/plans/2026-03-24-xqvm-spec-migration.md`)

> Editor's note: the domain this plan calls "discrete" was renamed to "integer" in QUI-1345. The text below is left as written on 2026-03-24.

# XQVM Spec Migration Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Align the Rust XQVM implementation (68 -> 76 opcodes) with `XQVM_SPEC.md` via a layer-by-layer migration that keeps `make lint && make test` green after each layer.

**Architecture:** Four independent crates updated in dependency order: `bytecode` (opcode table + builder) -> `disasm` (recompile-only) -> `asm` (assembler sugar updates + TSP examples) -> `vm` (new instruction handlers + stack depth enforcement). JUMP/JUMPI keep `i16` relative byte offsets; loop semantics stay lazy.

**Tech Stack:** Rust 2024 edition, `pastey` (paste! macro), `pest` (grammar -- no changes), `thiserror`, `miette`, `cargo nextest`.

**Spec:** `docs/design/specs/2026-03-24-xqvm-spec-migration-design.md`

---

## File Map

| File | Change |
|---|---|
| `crates/bytecode/src/types/table.rs` | Rewrite: all recodings + removal + 9 new opcodes |
| `crates/bytecode/src/builder.rs` | Update `minimal_pushc()` to use new variant names; update doc comments referencing `PUSHC_N`; auto-generated methods (`copy`, `one_hot_r`, `drop`, `sclr`, etc.) come for free from the macro |
| `crates/bytecode/src/codec.rs` | Update doc examples and tests that hard-code old variant names (`PushC0`, `PushC1`, etc.) or opcode bytes (`0x10`, `0x0F`) |
| `crates/bytecode/src/stream.rs` | Update doc examples and unit tests referencing old variant names or opcode bytes |
| `crates/bytecode/src/lib.rs` | Update doc examples referencing `Instruction::PushC0`, `Opcode::PushC0`, byte `0x10` |
| `crates/disasm/` | No code changes -- recompile from updated table; update any snapshot tests |
| `crates/asm/src/assembler.rs` | Update `PUSH`/`PUSHC` special-case to also accept `PUSH1..PUSH8`; update doc comment opcode references |
| `crates/vm/src/error.rs` | Add `StackOverflow` variant |
| `crates/vm/src/vm.rs` | Introduce module-level `STACK_LIMIT` const; introduce `push_stack` helper (replaces `push_val`); rename exec handlers; add 9 new `exec_*` handlers |
| `crates/vm/examples/tsp/encoder.xqasm` | `DUPL`->`COPY`, `ONEHOT`->`ONEHOTR` |
| `crates/vm/examples/tsp/decoder.xqasm` | Inspect and update if affected |
| `crates/vm/examples/tsp/verifier.xqasm` | Inspect and update if affected |

---

## Layer 1: `crates/bytecode`

### Task 1: Rewrite the opcode table

**Files:**
- Modify: `crates/bytecode/src/types/table.rs`

Apply all recodings, removal, and new opcodes in a **single edit** to avoid transient code collisions.

- [ ] **Step 1: Write a failing compile check**

Run `cargo check -p aglais-xqvm-bytecode` to confirm it currently passes.

```sh
cargo check -p aglais-xqvm-bytecode
```

Expected: exits 0 (green baseline).

- [ ] **Step 2: Replace the full opcode table**

Replace the contents of `crates/bytecode/src/types/table.rs` with the version below. The diff is: (1) update the doc comment count from 76 to 76 post-migration, (2) apply all opcode code/variant/mnemonic changes, (3) remove `PushC0`, (4) add 9 new entries.

Complete new table:

```rust
// Copyright (C) 2026 Postquant Labs Incorporated
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

/// Invoke `$mac!` with the complete XQVM opcode table.
///
/// The callback macro receives the full comma-separated list of 76 opcode
/// entries. Each entry has the form:
///
/// ```text
/// (code, Variant, "MNEMONIC", "doc", {field_name: FieldType, ...})
/// ```
///
/// | Position | Type | Description |
/// |---|---|---|
/// | `code` | `u8` literal | Wire-encoding byte |
/// | `Variant` | `ident` | `PascalCase` Rust enum variant name |
/// | `"MNEMONIC"` | `str` literal | Uppercase assembly mnemonic |
/// | `"doc"` | `str` literal | Single-sentence description |
/// | `{field: Type, ...}` | named-field list | Zero or more named operand fields |
///
/// # Examples
///
/// ```rust
/// macro_rules! collect_mnemonics {
///     ( $( ($code:literal, $var:ident, $mnem:literal, $doc:literal, {$($f:tt)*}) ),* $(,)? ) => {
///         &[ $( $mnem ),* ]
///     }
/// }
///
/// let mnemonics: &[&str] = aglais_xqvm_bytecode::opcodes!(collect_mnemonics);
/// assert!(mnemonics.contains(&"HALT"));
/// assert!(mnemonics.contains(&"ENERGY"));
/// ```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Run compile check**

```sh
cargo check -p aglais-xqvm-bytecode 2>&1 | head -40
```

Expected: compilation errors in crates that depend on `bytecode` (the downstream crates reference old variant names). The `bytecode` crate itself should compile. If `bytecode` fails, fix it before proceeding.

---

### Task 2: Update `builder.rs`

**Files:**
- Modify: `crates/bytecode/src/builder.rs`

The `impl_builder_methods!` macro auto-generates methods by converting `PascalCase` variant names to `snake_case` (via `pastey::paste!`). The new variants produce these auto-generated names:
- `Drop` -> `fn drop(&mut self, reg: Register)` -- `drop` is not a Rust keyword; it is safe as a method name
- `Sclr` -> `fn sclr(&mut self)`
- `Sqr` -> `fn sqr(&mut self)`
- `Abs` -> `fn abs(&mut self)`
- `Min` -> `fn min(&mut self)`
- `Max` -> `fn max(&mut self)`
- `Inc` -> `fn inc(&mut self)`
- `Dec` -> `fn dec(&mut self)`
- `OneHotC` -> `fn one_hot_c(&mut self, reg: Register)`
- `OneHotR` -> `fn one_hot_r(&mut self, reg: Register)` (renamed from `one_hot`)
- `Copy` -> `fn copy(&mut self)` (renamed from `dupl`)

> **Grammar note:** The pest grammar rule `mnemonic = @{ ASCII_ALPHA ~ (ASCII_ALPHA | ASCII_DIGIT)* }` already matches `PUSH1..PUSH8` and all other new/renamed mnemonics (they are all alphanumeric). No changes to `grammar.pest` are needed.

The macro handles all of the above automatically. The only manual changes needed in `builder.rs` are:

1. **Update the `push()` method** -- it currently references `PushC1..PushC8`; update to `Push1..Push8`.
2. **Update the `impl_builder_methods!` skip arm for push variants** -- currently matches `{val: ...}`; remains correct since the field name is still `val`.
3. **Update doc comments** -- find and replace `PUSHC_N` references in doc strings with `PUSH_N`.
4. **Remove or update any method that was hand-written for old variants** -- search for `fn dupl`, `fn onehot`, `fn push_c` in the file and update.

- [ ] **Step 1: Find all hand-written references to old variant names in builder.rs**

```sh
grep -n "dupl\|push_c\|one_hot\b\|PushC\|PUSHC" crates/bytecode/src/builder.rs
```

- [ ] **Step 2: Update `minimal_pushc` and doc comments in `builder.rs`**

The `push()` method delegates to `minimal_pushc(val: i64) -> Instruction` (a private helper near the bottom of the file). Update `minimal_pushc` to use the new variant names. The logic (sign-extension range checks) is unchanged; only the `Instruction::PushCN` names change to `Instruction::PushN`:

```rust
fn minimal_pushc(val: i64) -> Instruction {
    let be = val.to_be_bytes();
    for n in 1usize..=7 {
        let bits = (n * 8) as u32;
        let shift = 64 - bits;
        if (val << shift) >> shift == val {
            return match n {
                1 => Instruction::Push1 { val: [be[7]] },
                2 => Instruction::Push2 { val: [be[6], be[7]] },
                3 => Instruction::Push3 { val: [be[5], be[6], be[7]] },
                4 => Instruction::Push4 { val: [be[4], be[5], be[6], be[7]] },
                5 => Instruction::Push5 { val: [be[3], be[4], be[5], be[6], be[7]] },
                6 => Instruction::Push6 { val: [be[2], be[3], be[4], be[5], be[6], be[7]] },
                7 => Instruction::Push7 {
                    val: [be[1], be[2], be[3], be[4], be[5], be[6], be[7]],
                },
                _ => unreachable!(),
            };
        }
    }
    Instruction::Push8 { val: be }
}
```

Note: the old function had a special case `if val == 0 { return Instruction::PushC0 {} }` at the top. Remove that branch -- `val == 0` now encodes as `Push1 { val: [0x00] }` (the loop handles it since 0 fits in 1 byte).

Also update all doc comments in `builder.rs` that reference `PUSHC_N`:
- Struct-level doc comment: "Use push() to emit the smallest `PUSHC_N` instruction" -> "Use push() to emit the smallest `PUSH1..PUSH8` instruction"
- Any `/// PUSHC_0`, `/// PUSHC_1..PUSHC_8` references in examples

Search with: `grep -n "PUSHC\|PushC" crates/bytecode/src/builder.rs`

- [ ] **Step 3: Update codec.rs, stream.rs, and lib.rs doc examples and tests**

These files contain doc examples and unit tests that hard-code old variant names and opcode bytes. Find them all:

```sh
grep -rn "PushC\|0x10\b\|0x0F\b\|0x14\b\|0x15\b\|0x16\b\|0x17\b" \
  crates/bytecode/src/codec.rs \
  crates/bytecode/src/stream.rs \
  crates/bytecode/src/lib.rs
```

For each hit:
- `Instruction::PushC0 {}` -> `Instruction::Push1 { val: [0x00] }`
- `Instruction::PushCN { val: [...] }` -> `Instruction::PushN { val: [...] }` (same bytes, new name)
- `Opcode::PushC0` -> `Opcode::Push1`
- `0x10 // PUSHC_0` -> `0x11 // PUSH1`
- `0x0F // HALT` -> `0x09 // HALT`
- `0x14 // LOAD` -> `0x0A // LOAD`
- etc.

- [ ] **Step 4: Verify the builder compiles and tests pass**

```sh
cargo test -p aglais-xqvm-bytecode 2>&1 | tail -20
```

Expected: all pass. If tests fail on hard-coded byte values, fix them as described in Step 3.

- [ ] **Step 5: Run full lint on bytecode crate**

```sh
cargo clippy -p aglais-xqvm-bytecode --all-targets --all-features -- -D warnings 2>&1 | head -40
RUSTDOCFLAGS="-D warnings" cargo doc -p aglais-xqvm-bytecode 2>&1 | head -20
```

Expected: zero warnings from both clippy and rustdoc.

---

### Task 3: Commit Layer 1

- [ ] **Step 1: Run full bytecode test suite**

```sh
make test-unit 2>&1 | tail -30
```

- [ ] **Step 2: Commit**

```sh
git add crates/bytecode/
git commit -s -m "Recode and extend XQVM opcode table to match spec (68 -> 76 opcodes)"
```

---

## Layer 2: `crates/disasm`

### Task 4: Verify disasm

The disasm crate is X-macro driven and should recompile cleanly from the updated table. No source changes are expected.

**Files:**
- Verify: `crates/disasm/src/` (no edits expected)

- [ ] **Step 1: Build disasm**

```sh
cargo build -p aglais-xqvm-disasm 2>&1 | head -20
```

Expected: compiles cleanly. If there are errors, they are due to hard-coded opcode references -- fix them.

- [ ] **Step 2: Run disasm tests**

```sh
cargo test -p aglais-xqvm-disasm 2>&1 | tail -20
```

If snapshot tests exist, update them: `INSTA_UPDATE=new cargo test -p aglais-xqvm-disasm` or equivalent.

- [ ] **Step 3: Spot-check output format**

First check the actual disassembler binary name in `crates/disasm/Cargo.toml`:
```sh
grep 'name' crates/disasm/Cargo.toml | head -5
```

Then run a round-trip:
```sh
printf 'PUSH 0\nHALT\n' | cargo run --bin xqasm -- /dev/stdin \
  | cargo run --bin <disasm-bin-name> -- /dev/stdin 2>/dev/null || true
```

Verify rendered mnemonics use `PUSH1`, not `PUSHC_1`, and `HALT` appears.

- [ ] **Step 4: Commit**

```sh
git add crates/disasm/
git commit -s -m "Verify disasm recompiles cleanly against updated opcode table"
```

---

## Layer 3: `crates/asm`

### Task 5: Update assembler special-case handling

**Files:**
- Modify: `crates/asm/src/assembler.rs`

The `build_instr` x-macro match already handles all renamed opcodes automatically (it matches on `$mnem` string literals from the table). The only manual changes needed are:

1. The `"PUSH" | "PUSHC"` special case -- update the match arm to also accept explicit `PUSH1..PUSH8`.
2. Update the doc comment in `assemble()` that references `PUSHC_0`.
3. Update the doc-test assertion `program.code()[0] == 0x10 // PUSHC_0` to reflect the new encoding (`PUSH1` = `0x11`).

- [ ] **Step 1: Locate and update the push special case**

Find the arm:
```rust
"PUSH" | "PUSHC" => {
    assemble_push(instr, &mut b, src)?;
}
```

Replace with:
```rust
"PUSH" | "PUSHC" | "PUSH1" | "PUSH2" | "PUSH3" | "PUSH4"
| "PUSH5" | "PUSH6" | "PUSH7" | "PUSH8" => {
    assemble_push(instr, &mut b, src)?;
}
```

This makes `PUSH1 42`, `PUSH2 256`, etc. valid in assembly. All of these accept a **single integer operand** and route through `assemble_push`, which calls `b.push(value)` and lets the builder pick the minimal encoding. The explicit width suffix is a readability hint only -- `PUSH2 42` emits `PUSH1` because 42 fits in 1 byte.

> **Not supported:** The multi-byte literal form `PUSH2 0x01 0x00` (two separate hex operands) is not valid in this assembler. `check_operand_count(instr, 1, src)?` will reject it with `WrongOperandCount`. Document this limitation in the function's doc comment.

- [ ] **Step 2: Update doc comments**

In `assembler.rs`, find all references to `PUSHC_0`, `PUSHC_N`, `0x10 // PUSHC_0`, `0x0F // HALT` and update:
- `PUSHC_0` -> `PUSH1` (sugar for value 0 uses `PUSH1 0x00`)
- `0x10 // PUSHC_0` -> `0x11 // PUSH1` (PUSH1 opcode = 0x11)
- `0x0F // HALT` -> `0x09 // HALT`

- [ ] **Step 3: Build and test asm crate**

```sh
cargo test -p aglais-xqvm-asm 2>&1 | tail -30
```

Expected: all pass.

- [ ] **Step 4: Lint**

```sh
cargo clippy -p aglais-xqvm-asm --all-targets --all-features -- -D warnings 2>&1 | head -20
```

---

### Task 6: Update TSP example `.xqasm` files

**Files:**
- Modify: `crates/vm/examples/tsp/encoder.xqasm`
- Check: `crates/vm/examples/tsp/decoder.xqasm`
- Check: `crates/vm/examples/tsp/verifier.xqasm`

`grep` found two usages in `encoder.xqasm`: `DUPL` (line 29) and `ONEHOT r4` (line 106).

- [ ] **Step 1: Update `encoder.xqasm`**

```sh
sed -i 's/\bDUPL\b/COPY/g; s/\bONEHOT\b/ONEHOTR/g' crates/vm/examples/tsp/encoder.xqasm
```

Verify with:
```sh
grep -n "DUPL\|ONEHOT\b\|PUSHC" crates/vm/examples/tsp/encoder.xqasm
```

Expected: no matches.

- [ ] **Step 2: Check decoder and verifier**

```sh
grep -n "DUPL\|ONEHOT\b\|PUSHC" crates/vm/examples/tsp/decoder.xqasm crates/vm/examples/tsp/verifier.xqasm
```

Apply same replacements if any matches are found.

- [ ] **Step 3: Commit Layer 3**

```sh
git add crates/asm/ crates/vm/examples/tsp/
git commit -s -m "Update assembler sugar and TSP examples for renamed opcodes"
```

---

## Layer 4: `crates/vm`

### Task 7: Add `StackOverflow` error and `push_stack` helper

**Files:**
- Modify: `crates/vm/src/error.rs`
- Modify: `crates/vm/src/vm.rs`

- [ ] **Step 1: Write a failing test for stack overflow**

Add to `crates/vm/src/vm.rs` in the `#[cfg(test)]` block:

```rust
#[test]
fn stack_overflow_is_reported() {
    // RANGE loop with count=8193: pushes 1 value per iteration without
    // popping. After 8192 successful pushes (stack len = 8192), the 8193rd
    // push triggers `stack.len() >= STACK_LIMIT` (8192 >= 8192) -> overflow.
    let mut b = InstructionBuilder::new();
    b.push(0).push(8193).range();
    b.push(1);
    b.next();
    b.halt();
    let program = b.build().unwrap();

    let mut vm = Vm::new();
    let err = vm.run(&program).unwrap_err();
    assert!(
        matches!(err, Error::StackOverflow { .. }),
        "expected StackOverflow, got {err:?}"
    );
}
```

Run to confirm it fails (StackOverflow variant does not exist yet):
```sh
cargo test -p aglais-xqvm-vm stack_overflow 2>&1 | tail -10
```

- [ ] **Step 2: Add `StackOverflow` to `error.rs`**

In `crates/vm/src/error.rs`, add after the `StepLimitExceeded` variant:

```rust
/// The value stack exceeded the maximum allowed depth of 8192.
#[error("stack overflow at byte {pos:#06x}: depth exceeded 8192")]
StackOverflow { pos: usize },
```

Also add `pos` to the `byte_pos()` match arm:
```rust
Self::StackOverflow { pos } => Some(*pos),
```

- [ ] **Step 3: Introduce `STACK_LIMIT` const and `push_stack` helper in `vm.rs`**

Add a module-level const immediately after `DEFAULT_STEP_LIMIT` (line ~111):

```rust
/// Maximum stack depth enforced by [`Vm`].
const STACK_LIMIT: usize = 8192;
```

Replace the existing `push_val` helper:

```rust
fn push_val(&mut self, v: i64) {
    self.stack.push(v);
}
```

With (note: module-level const, not `Self::STACK_LIMIT`):

```rust
fn push_stack(&mut self, v: i64, pos: usize) -> Result<(), Error> {
    if self.stack.len() >= STACK_LIMIT {
        return Err(Error::StackOverflow { pos });
    }
    self.stack.push(v);
    Ok(())
}
```

- [ ] **Step 4: Update all call sites of `push_val` -> `push_stack`**

Every `self.push_val(v)` call must become `self.push_stack(v, pos)?`. Use:

```sh
grep -n "push_val" crates/vm/src/vm.rs
```

Important: some handlers declare their `pos` parameter with a leading underscore (`_pos: usize`) to suppress the unused-variable lint. When these handlers are updated to call `push_stack(v, pos)`, the parameter must be renamed from `_pos` to `pos` -- otherwise `-D warnings` will fire for `unused_variables`. Check every handler that pushes. For example, `exec_push_c1` currently has `_pos: usize`; after renaming to `exec_push1` and calling `push_stack`, change the signature to `pos: usize`.

- [ ] **Step 5: Run the overflow test**

```sh
cargo test -p aglais-xqvm-vm stack_overflow 2>&1 | tail -10
```

Expected: PASS.

---

### Task 8: Rename changed VM match arms

**Files:**
- Modify: `crates/vm/src/vm.rs`

The `impl_dispatch!` macro generates calls to `self.exec_<variant_snake>()`. Renamed variants automatically call the new function name. We must rename the handler functions to match.

- [ ] **Step 1: Rename push handler functions**

Find and rename:
- `exec_push_c0` (delete -- `PushC0` variant is gone)
- `exec_push_c1` -> `exec_push1`
- `exec_push_c2` -> `exec_push2`
- ... through `exec_push_c8` -> `exec_push8`

Each handler body is identical (calls `sign_extend_be` and `push_stack`). Use a bulk rename:

```sh
sed -i \
  's/fn exec_push_c0/fn exec_push_c0_DELETED/g;
   s/fn exec_push_c1/fn exec_push1/g;
   s/fn exec_push_c2/fn exec_push2/g;
   s/fn exec_push_c3/fn exec_push3/g;
   s/fn exec_push_c4/fn exec_push4/g;
   s/fn exec_push_c5/fn exec_push5/g;
   s/fn exec_push_c6/fn exec_push6/g;
   s/fn exec_push_c7/fn exec_push7/g;
   s/fn exec_push_c8/fn exec_push8/g' \
  crates/vm/src/vm.rs
```

Then **immediately** remove the `exec_push_c0_DELETED` function body from the file before proceeding to Step 4 (the push_val -> push_stack migration). The deleted function calls `self.push_val(0)`; if it is still present when push_val is renamed, the file will fail to compile.

- [ ] **Step 2: Rename `exec_dupl` -> `exec_copy`**

```sh
sed -i 's/fn exec_dupl/fn exec_copy/g' crates/vm/src/vm.rs
```

- [ ] **Step 3: Rename `exec_one_hot` -> `exec_one_hot_r`**

```sh
sed -i 's/fn exec_one_hot\b/fn exec_one_hot_r/g' crates/vm/src/vm.rs
```

- [ ] **Step 4: Compile check**

```sh
cargo check -p aglais-xqvm-vm 2>&1 | head -30
```

Expected: errors only for missing `exec_drop`, `exec_sclr`, `exec_sqr`, `exec_abs`, `exec_min`, `exec_max`, `exec_inc`, `exec_dec`, `exec_one_hot_c` (not yet written). Fix any other errors first.

---

### Task 9: Add new instruction handlers

**Files:**
- Modify: `crates/vm/src/vm.rs`
- Modify: `crates/vm/src/model.rs`

- [ ] **Step 1: Add arithmetic handlers**

Add after `exec_neg`:

```rust
fn exec_sqr(&mut self, pos: usize) -> Result<StepResult, Error> {
    let a = self.pop(pos)?;
    self.push_stack(a.wrapping_mul(a), pos)?;
    Ok(StepResult::Continue)
}

fn exec_abs(&mut self, pos: usize) -> Result<StepResult, Error> {
    let a = self.pop(pos)?;
    self.push_stack(a.wrapping_abs(), pos)?;
    Ok(StepResult::Continue)
}

fn exec_min(&mut self, pos: usize) -> Result<StepResult, Error> {
    let b = self.pop(pos)?;
    let a = self.pop(pos)?;
    self.push_stack(a.min(b), pos)?;
    Ok(StepResult::Continue)
}

fn exec_max(&mut self, pos: usize) -> Result<StepResult, Error> {
    let b = self.pop(pos)?;
    let a = self.pop(pos)?;
    self.push_stack(a.max(b), pos)?;
    Ok(StepResult::Continue)
}

fn exec_inc(&mut self, pos: usize) -> Result<StepResult, Error> {
    let a = self.pop(pos)?;
    self.push_stack(a.wrapping_add(1), pos)?;
    Ok(StepResult::Continue)
}

fn exec_dec(&mut self, pos: usize) -> Result<StepResult, Error> {
    let a = self.pop(pos)?;
    self.push_stack(a.wrapping_sub(1), pos)?;
    Ok(StepResult::Continue)
}
```

- [ ] **Step 2: Add stack/register handlers**

```rust
fn exec_drop(&mut self, _pos: usize, reg: Register) -> Result<StepResult, Error> {
    *self.reg_mut(reg) = RegVal::Int(0);
    Ok(StepResult::Continue)
}

fn exec_sclr(&mut self, _pos: usize) -> Result<StepResult, Error> {
    self.stack.clear();
    Ok(StepResult::Continue)
}
```

- [ ] **Step 3: Add `exec_one_hot_c` handler**

The column one-hot mirrors `exec_one_hot_r` with row/col axes swapped. Add after `exec_one_hot_r`:

```rust
fn exec_one_hot_c(&mut self, pos: usize, reg: Register) -> Result<StepResult, Error> {
    let penalty = self.pop(pos)?;
    let col = self.pop(pos)?;
    let m = self
        .reg_mut(reg)
        .as_model_mut()
        .map_err(|got| Error::RegisterType {
            reg: reg.slot(),
            expected: "model",
            got,
        })?;
    let col_usize = col as usize;
    // H = penalty * (sum(x_i for i in col) - 1)^2
    // Linear: -penalty per variable in column
    // Quadratic: 2*penalty per pair in column
    for r in 0..m.rows {
        m.add_linear(r * m.cols + col_usize, -penalty);
    }
    for ri in 0..m.rows {
        for rj in (ri + 1)..m.rows {
            m.add_quad(ri * m.cols + col_usize, rj * m.cols + col_usize, 2 * penalty);
        }
    }
    Ok(StepResult::Continue)
}
```

- [ ] **Step 4: Write unit tests for new instructions**

Add to the `#[cfg(test)]` block:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 5: Run new tests**

```sh
cargo test -p aglais-xqvm-vm sqr_abs_min_max drop_clears sclr_empties 2>&1 | tail -15
```

Expected: all PASS.

---

### Task 10: Final validation and commit

- [ ] **Step 1: Run full workspace lint and test**

```sh
make lint && make test
```

Expected: zero warnings, all tests pass. If the TSP example is a test target, it must pass end-to-end.

- [ ] **Step 2: Commit Layer 4**

```sh
git add crates/vm/
git commit -s -m "Add 9 new VM instruction handlers and enforce 8192-element stack limit"
```

- [ ] **Step 3: Final integration check**

```sh
make all
```

Expected: exits 0.

---

### Design Plans 2026 03 27 Tracing Vm (`docs/design/plans/2026-03-27-tracing-vm.md`)

# Tracing VM Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add zero-cost execution tracing to the XQVM interpreter with text and JSON output formats.

**Architecture:** Generic `Tracer` trait injected via type parameter into `Vm::run_trace`. Monomorphization with `NoopTracer` produces identical code to the current `run`. Concrete `TextTracer` / `JsonTracer` formatters live behind `#[cfg(feature = "std")]`. CLI gets `--trace`, `--trace-format`, `--trace-file` flags.

**Tech Stack:** Rust, existing workspace (`aglais-xqvm-bytecode`, `aglais-xqvm-vm`), clap, no new dependencies.

**Spec:** `docs/design/specs/2026-03-27-tracing-vm-design.md`

---

## File Structure

### New files

| File | Responsibility |
|---|---|
| `crates/bytecode/src/types/register_effect.rs` | `RegisterEffect` type + `Instruction::read_registers` / `written_registers` methods |
| `crates/vm/src/tracer/mod.rs` | `Tracer` trait, `StepState`, `NoopTracer`, re-exports |
| `crates/vm/src/tracer/text.rs` | `TextTracer<W>` (std-only) |
| `crates/vm/src/tracer/json.rs` | `JsonTracer<W>` (std-only) |

### Modified files

| File | Change |
|---|---|
| `crates/bytecode/src/types/mod.rs` | Add `mod register_effect;` + re-export `RegisterEffect` |
| `crates/bytecode/src/types/instruction.rs` | Add `Display` impl, `read_registers`, `written_registers` |
| `crates/bytecode/src/lib.rs` | Re-export `RegisterEffect` |
| `crates/vm/src/lib.rs` | Add `mod tracer;` + re-exports |
| `crates/vm/src/error.rs` | Add `TraceFailed` variant + update `byte_pos()` |
| `crates/vm/src/vm.rs` | Add `run_trace`, refactor `run` to delegate |
| `crates/vm/src/bin/xqvm.rs` | Add `--trace`, `--trace-format`, `--trace-file` CLI flags |

---

## Task 1: Register Effect Type (bytecode crate)

**Files:**
- Create: `crates/bytecode/src/types/register_effect.rs`
- Modify: `crates/bytecode/src/types/mod.rs`
- Modify: `crates/bytecode/src/lib.rs`

- [ ] **Step 1: Write the failing test**

Add to the bottom of `crates/bytecode/src/types/register_effect.rs` (we create the file with the test first):

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Wire up the module**

In `crates/bytecode/src/types/mod.rs`, add after `mod operand;`:
```rust
mod register_effect;
```
And update the re-export block:
```rust
pub use self::{
    instruction::Instruction,
    opcode::{DecodeError, Opcode},
    operand::Register,
    register_effect::RegisterEffect,
};
```

In `crates/bytecode/src/lib.rs`, add to the re-export line:
```rust
pub use types::{Instruction, Opcode, Register, RegisterEffect};
```

- [ ] **Step 3: Run tests to verify they pass**

Run: `cargo nextest run -p aglais-xqvm-bytecode --lib -- register_effect`
Expected: 3 PASS

- [ ] **Step 4: Commit**

```
Add RegisterEffect type for register introspection

Implements QUI-314
```

---

## Task 2: Register Introspection Methods (bytecode crate)

**Files:**
- Modify: `crates/bytecode/src/types/instruction.rs`

- [ ] **Step 1: Write the failing tests**

Add tests to the existing `#[cfg(test)] mod tests` block in `crates/bytecode/src/types/instruction.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cargo nextest run -p aglais-xqvm-bytecode --lib -- read_registers`
Expected: FAIL — method `read_registers` not found

- [ ] **Step 3: Implement `read_registers` and `written_registers`**

Add a new `impl Instruction` block in `crates/bytecode/src/types/instruction.rs`, before `#[cfg(test)]`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `cargo nextest run -p aglais-xqvm-bytecode --lib`
Expected: all PASS (existing + new)

- [ ] **Step 5: Commit**

```
Add register introspection to Instruction

Implements QUI-314
```

---

## Task 3: Display Impl for Instruction (bytecode crate)

**Files:**
- Modify: `crates/bytecode/src/types/instruction.rs`

- [ ] **Step 1: Write the failing test**

Add to the `#[cfg(test)] mod tests` in `crates/bytecode/src/types/instruction.rs`:

```rust
    use alloc::format;

    #[test]
    fn display_no_operands() {
        assert_eq!(format!("{}", Instruction::Add {}), "ADD");
        assert_eq!(format!("{}", Instruction::Halt {}), "HALT");
        assert_eq!(format!("{}", Instruction::Nop {}), "NOP");
    }

    #[test]
    fn display_register_operand() {
        assert_eq!(
            format!("{}", Instruction::Load { reg: Register(5) }),
            "LOAD r5",
        );
        assert_eq!(
            format!("{}", Instruction::Stow { reg: Register(0) }),
            "STOW r0",
        );
    }

    #[test]
    fn display_push() {
        assert_eq!(
            format!("{}", Instruction::Push8 { val: 42i64.to_be_bytes() }),
            "PUSH8 42",
        );
        assert_eq!(
            format!("{}", Instruction::Push1 { val: [0xFF] }),
            "PUSH1 -1",
        );
    }

    #[test]
    fn display_jump() {
        assert_eq!(
            format!("{}", Instruction::Jump { label: 3 }),
            "JUMP .3",
        );
    }

    #[test]
    fn display_energy() {
        assert_eq!(
            format!(
                "{}",
                Instruction::Energy { model: Register(0), sample: Register(1) }
            ),
            "ENERGY r0 r1",
        );
    }
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cargo nextest run -p aglais-xqvm-bytecode --lib -- display`
Expected: FAIL — `Instruction doesn't implement fmt::Display`

- [ ] **Step 3: Implement Display**

Add to `crates/bytecode/src/types/instruction.rs`, before the `#[cfg(test)]` block. Use the existing `sign_extend_be`-style helper from `vm.rs` — but since this is the bytecode crate, define a local helper:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Note: the final `_` catch-all covers `Nop`, `Target`, `Next`, `Range`, `Halt`, `Pop`, `Sclr`, `Swap`, `Copy`, and all arithmetic/comparison/logic/bitwise/index instructions (all zero-operand).

- [ ] **Step 4: Run tests**

Run: `cargo nextest run -p aglais-xqvm-bytecode --lib`
Expected: all PASS

- [ ] **Step 5: Commit**

```
Add Display impl for Instruction

Implements QUI-314
```

---

## Task 4: Tracer Trait and NoopTracer (VM crate)

**Files:**
- Create: `crates/vm/src/tracer/mod.rs`
- Modify: `crates/vm/src/lib.rs`
- Modify: `crates/vm/src/error.rs`

- [ ] **Step 1: Create the tracer module**

Create `crates/vm/src/tracer/mod.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Add `TraceFailed` error variant**

In `crates/vm/src/error.rs`, add at the top of the file after the existing `#[cfg(feature = "std")]` imports:

```rust
#[cfg(not(feature = "std"))]
use alloc::string::String;
```

Add new variant to the `Error` enum, after `InvalidGridDimensions`:

```rust
    /// A tracer callback returned an error (e.g. I/O write failure).
    #[error("trace failed at byte {pos:#06x}: {message}")]
    TraceFailed {
        pos: usize,
        message: String,
    },
```

Update `byte_pos()` — add `Self::TraceFailed { pos, .. }` to the `Some(*pos)` arm:

```rust
            | Self::TraceFailed { pos, .. } => Some(*pos),
```

- [ ] **Step 3: Wire up in `lib.rs`**

In `crates/vm/src/lib.rs`, add `mod tracer;` after `mod vm;`:

```rust
mod vm;
pub mod tracer;
```

Add re-exports after the existing re-export block:

```rust
pub use tracer::{NoopTracer, StepState, Tracer};
#[cfg(feature = "std")]
pub use tracer::{JsonTracer, TextTracer};
```

- [ ] **Step 4: Create placeholder files for TextTracer and JsonTracer**

Create `crates/vm/src/tracer/text.rs` with the license header and a placeholder struct that compiles:

```rust
// [license header]

//! Human-readable text tracer.

use std::io::Write;

use crate::tracer::{StepState, Tracer};

/// Writes human-readable trace output with aligned columns.
#[derive(Debug)]
pub struct TextTracer<W: Write> {
    out: W,
}

impl<W: Write> Tracer for TextTracer<W> {
    type Error = std::io::Error;

    fn on_step(&mut self, _state: &StepState<'_>) -> Result<(), Self::Error> {
        todo!("implemented in Task 6")
    }
}
```

Create `crates/vm/src/tracer/json.rs` with same pattern:

```rust
// [license header]

//! JSONL tracer.

use std::io::Write;

use crate::tracer::{StepState, Tracer};

/// Writes one JSON object per line (JSONL format).
#[derive(Debug)]
pub struct JsonTracer<W: Write> {
    out: W,
}

impl<W: Write> Tracer for JsonTracer<W> {
    type Error = std::io::Error;

    fn on_step(&mut self, _state: &StepState<'_>) -> Result<(), Self::Error> {
        todo!("implemented in Task 7")
    }
}
```

- [ ] **Step 5: Verify compilation**

Run: `cargo check -p aglais-xqvm-vm --all-features`
Expected: compiles cleanly

Run: `cargo check -p aglais-xqvm-vm --no-default-features`
Expected: compiles cleanly (no_std)

- [ ] **Step 6: Commit**

```
Add Tracer trait, StepState, and NoopTracer

Implements QUI-314
```

---

## Task 5: VM Integration — `run_trace` Method

**Files:**
- Modify: `crates/vm/src/vm.rs`

- [ ] **Step 1: Write the failing test for RecordingTracer**

Add at the very bottom of `crates/vm/src/vm.rs`, a test module:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cargo nextest run -p aglais-xqvm-vm --lib -- vm::tests`
Expected: FAIL — method `run_trace` not found

- [ ] **Step 3: Implement `run_trace`**

In `crates/vm/src/vm.rs`, add imports at the top (after existing imports):

```rust
#[cfg(not(feature = "std"))]
use alloc::{format, vec::Vec};

use crate::tracer::{NoopTracer, StepState, Tracer};
```

Replace the existing `run` method (lines 268-304) with:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Run tests**

Run: `cargo nextest run -p aglais-xqvm-vm --lib`
Expected: all PASS

Run: `cargo nextest run -p aglais-xqvm-vm --test '*'`
Expected: all existing integration tests PASS (run delegates to run_trace)

- [ ] **Step 5: Commit**

```
Add run_trace method with register snapshotting

Implements QUI-314
```

---

## Task 6: TextTracer (VM crate, std-only)

**Files:**
- Modify: `crates/vm/src/tracer/text.rs`

This is a design and formatting task where your choices shape the output. See the spec for the desired column layout. The `RegVal` display formatting (compact representation for complex types) is a key decision.

- [ ] **Step 1: Write the integration test**

Create `crates/vm/tests/trace_text.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Implement TextTracer**

Replace the placeholder in `crates/vm/src/tracer/text.rs` with the full implementation. Key decisions:

- Fixed column widths: step(6), offset(8), instruction(22), stack(26), read-regs(18), written-regs(18).
- Write a header line first, then one row per step.
- `RegVal` compact display:
  - `Int(v)` → `"{v}"`
  - `VecInt(v)` → `"vec<int>(len={v.len()})"`
  - `VecXqmx(v)` → `"vec<model>(len={v.len()})"`
  - `Model(m)` → `"model({m.rows}x{m.cols})"`
  - `Sample(s)` → `"sample(len={s.values.len()})"`
- Stack display: show all if ≤8 elements, otherwise `...N more, top8`

The compact `RegVal` formatting, stack formatting, and column layout are design choices — implement them per the spec's example output.

- [ ] **Step 3: Run tests**

Run: `cargo nextest run -p aglais-xqvm-vm --test trace_text`
Expected: PASS

- [ ] **Step 4: Commit**

```
Add TextTracer for human-readable trace output

Implements QUI-314
```

---

## Task 7: JsonTracer (VM crate, std-only)

**Files:**
- Modify: `crates/vm/src/tracer/json.rs`

- [ ] **Step 1: Write the integration test**

Create `crates/vm/tests/trace_json.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Implement JsonTracer**

Replace the placeholder in `crates/vm/src/tracer/json.rs`. Hand-write JSON serialization using `write!` calls — no serde dependency. One JSON object per line.

Format:
```json
{"step":1,"pos":0,"instruction":"PUSH1 3","stack":[3],"read_regs":{},"written_regs":{}}
```

Register objects use the register index as key:
```json
{"step":4,"pos":5,"instruction":"STOW r0","stack":[],"read_regs":{},"written_regs":{"0":{"type":"int","value":7}}}
```

Complex types:
```json
{"0":{"type":"model","rows":8,"cols":8}}
{"0":{"type":"vec<int>","len":12}}
{"0":{"type":"sample","len":5}}
```

- [ ] **Step 3: Run tests**

Run: `cargo nextest run -p aglais-xqvm-vm --test trace_json`
Expected: PASS

- [ ] **Step 4: Commit**

```
Add JsonTracer for JSONL trace output

Implements QUI-314
```

---

## Task 8: CLI Integration

**Files:**
- Modify: `crates/vm/src/bin/xqvm.rs`

- [ ] **Step 1: Add CLI flags**

In `crates/vm/src/bin/xqvm.rs`, add the new imports and types:

```rust
use std::fs::File;
use std::io::{BufWriter, Write};

use aglais_xqvm_vm::{TextTracer, JsonTracer};
```

Add the `TraceFormat` enum:

```rust
/// Trace output format.
#[derive(Clone, Debug, clap::ValueEnum)]
enum TraceFormat {
    /// Human-readable aligned columns.
    Text,
    /// One JSON object per line (JSONL).
    Json,
}
```

Add new fields to `Args`:

```rust
    /// Enable execution tracing.
    #[arg(long)]
    trace: bool,

    /// Trace output format.
    #[arg(long, default_value = "text", requires = "trace")]
    trace_format: TraceFormat,

    /// Write trace output to a file instead of stderr.
    #[arg(long, requires = "trace")]
    trace_file: Option<PathBuf>,
```

- [ ] **Step 2: Wire up trace dispatch**

Replace the `vm.run(&program)` call in `main()` (line 94-95) with:

```rust
    if args.trace {
        let writer: Box<dyn Write> = match &args.trace_file {
            Some(path) => {
                let file = File::create(path)
                    .into_diagnostic()
                    .wrap_err_with(|| format!("failed to create '{}'", path.display()))?;
                Box::new(BufWriter::new(file))
            }
            None => Box::new(BufWriter::new(std::io::stderr())),
        };
        match args.trace_format {
            TraceFormat::Text => {
                let mut tracer = TextTracer::new(writer);
                vm.run_trace(&mut tracer, &program)
                    .map_err(|e| e.into_diagnostic(&program, &args.file.to_string_lossy()))?;
            }
            TraceFormat::Json => {
                let mut tracer = JsonTracer::new(writer);
                vm.run_trace(&mut tracer, &program)
                    .map_err(|e| e.into_diagnostic(&program, &args.file.to_string_lossy()))?;
            }
        }
    } else {
        vm.run(&program)
            .map_err(|e| e.into_diagnostic(&program, &args.file.to_string_lossy()))?;
    };
```

- [ ] **Step 3: Verify compilation and help output**

Run: `cargo build -p aglais-xqvm-vm --bin xqvm`
Expected: compiles

Run: `cargo run -p aglais-xqvm-vm --bin xqvm -- --help`
Expected: output includes `--trace`, `--trace-format`, `--trace-file`

- [ ] **Step 4: Commit**

```
Add --trace, --trace-format, --trace-file CLI flags

Implements QUI-314
```

---

## Task 9: Final Verification

- [ ] **Step 1: Run full lints**

Run: `make lint`
Expected: 0 warnings, 0 errors

- [ ] **Step 2: Run all tests**

Run: `make test`
Expected: all PASS

- [ ] **Step 3: Run doc tests**

Run: `make lint-doc`
Expected: 0 warnings

- [ ] **Step 4: Fix any issues found above, then commit fixes**

- [ ] **Step 5: Mark QUI-314 as Done in Linear**

---

### Design Plans 2026 06 18 Quantum Gate Isa (`docs/design/plans/2026-06-18-quantum-gate-isa.md`)

# Quantum Gate ISA Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a gate-based quantum ISA as a separate `.xqg` binary format with CPU, IBM Quantum, and IonQ backends, enabling XQuad to target real quantum hardware alongside the existing QUBO optimization path.

**Architecture:** A new Rust crate `xqgbc` defines the gate opcode table (X-macro), `.xqg` binary format (15-byte header + instruction stream), codec, and static verifier. A new Python package `xqgb` provides a pure-Python decoder for `.xqg` files, a `GateSolver` ABC mirroring `xqsa`'s `Solver`, a NumPy statevector CPU simulator, and backend adapters for IBM Quantum (Qiskit) and IonQ (ionq SDK), all tied together by a registry factory.

**Tech Stack:** Rust 2024 / `no_std+alloc` (core types), `crc32fast` (CRC), `thiserror`; Python 3.13+ / `numpy>=2.0`, optional `qiskit>=1.0` + `qiskit-aer>=0.14` + `qiskit-ibm-runtime>=0.20`, optional `ionq>=0.3`

## Consolidated work items (W1–W7)

The Linear epic (QUI-724) groups these tasks into seven work items. The granular tasks below are the implementation sub-steps; this table maps each to its W-item.

| Work item | Composed of | Linear ticket |
|---|---|---|
| **W1** -- `xqgbc` Rust crate | W1 Parts 1–3 (former Tasks 1–3) | QUI-725 |
| **W2** -- `xqgb` Python core | W2 Parts 1–2 (former Tasks 4–5) | QUI-728 |
| **W3** -- `xqgb` hardware backends | W3 Parts 1–2 (former Tasks 6–7) | QUI-730 |
| **W4** -- Conformance harness | former Task 8 | QUI-732 |
| **W5** -- Gate primitives | former Task 10 | QUI-739 |
| **W6** -- Classical control flow + register file | W6 Parts 1–3 (former Tasks 9, 11, 13) | QUI-738 |
| **W7** -- Parameterized circuits / header v2 | former Task 12 | QUI-741 |

> Section order in this document predates the consolidation: W6's three parts appear below as the former Task 9 (Part 1), Task 11 (Part 2), and Task 13 (Part 3), interleaved with W5 and W7. Follow the W-numbering, not the document order.

> W4 should assert that the Rust opcode set, the Python opcode set, and `gate_opcodes.yaml` are all equal, rather than checking a hardcoded opcode count -- this lets W5, W6, and W7 proceed in parallel once W4 lands.

## Global Constraints

- `xqgbc` must be `no_std + alloc` with a `std` feature gate, matching `xqvm`'s portability posture
- All Rust code must pass `cargo clippy --all-features -- -D warnings` with the workspace `clippy.toml`
- Python must pass `ruff check` and `ruff format --check` at the repo root
- Google-style docstrings on all public Python APIs
- Gate opcode bytes live in 0x80–0xE8 (W1 defines 0x80–0xC2; W5–W6 extend it up to 0xE8), inside the range the XQVM spec reserves as illegal for `.xqb`
- Gate angles are stored as `u32` binary-angular units (full turn = 2^32); conversion to radians happens only at the simulator/backend boundary, never on-chain -- this eliminates NaN/−0 hazards and halves angle storage cost vs f64
- No modifications to `xqvm`, `xqasm`, or `xqvm_py` -- gate ISA is purely additive in new crates/packages
- Add `xqgbc` to root `Cargo.toml` `members`; add `xqgb` to root `pyproject.toml` `[tool.uv.workspace] members`

---

## File Map

### New: `xqgbc/` (Rust crate)

| File | Purpose |
|---|---|
| `xqgbc/Cargo.toml` | Crate manifest with `crc32fast`, `thiserror`, `pastey` deps |
| `xqgbc/src/lib.rs` | Public facade re-exporting all public types |
| `xqgbc/src/table.rs` | `gate_opcodes!` X-macro (25 gate instructions) |
| `xqgbc/src/types/mod.rs` | Re-exports from sub-modules |
| `xqgbc/src/types/operand.rs` | `Qubit(u8)`, `Cbit(u8)`, `GateAngle(u32)` newtypes (full turn = 2^32 units) |
| `xqgbc/src/types/opcode.rs` | `GateOpcode` `#[repr(u8)]` enum derived from X-macro |
| `xqgbc/src/types/instruction.rs` | `GateInstruction` enum derived from X-macro |
| `xqgbc/src/codec.rs` | `EncodeOperand`/`DecodeOperand` traits + `impl_gate_codec!` macro |
| `xqgbc/src/program.rs` | `GateCircuit` struct + `.xqg` encode/decode with 15-byte header |
| `xqgbc/src/verifier.rs` | `static_verify()` single-pass qubit bounds + MEAS presence check |
| `xqgbc/tests/roundtrip.rs` | Integration: encode → decode roundtrips for every opcode |

### New: `xqgb/` (Python package)

| File | Purpose |
|---|---|
| `xqgb/pyproject.toml` | Package manifest; extras `[ibm]`, `[ionq]` |
| `xqgb/__init__.py` | Re-exports public API |
| `xqgb/decoder.py` | Pure-Python `.xqg` header + instruction decoder |
| `xqgb/solver.py` | `GateSolverResult`, `GateSolver` ABC |
| `xqgb/transpiler.py` | `GateCircuitTranspiler[T]` ABC; `QiskitTranspiler`; `IonQTranspiler` |
| `xqgb/cpu_sim.py` | `StatevectorSimulator` + `SolverCPUSim(GateSolver)` |
| `xqgb/ibm_quantum.py` | `SolverIBMQuantum(GateSolver)` |
| `xqgb/ionq.py` | `SolverIonQ(GateSolver)` |
| `xqgb/registry.py` | `GATE_SOLVERS` dict + `build_gate_solver(name, **kwargs)` |
| `xqgb/tests/test_decoder.py` | Decoder unit tests (no hardware) |
| `xqgb/tests/test_cpu_sim.py` | Statevector sim: Bell state, GHZ, Pauli correctness |
| `xqgb/tests/test_registry.py` | Registry smoke tests |

### New: conformance additions

| File | Purpose |
|---|---|
| `conformance/gate_opcodes.yaml` | Canonical source of truth for all 25 gate opcodes: code, mnemonic, operand types and wire widths |
| `conformance/vectors/gate/<name>/circuit.xqg` | Pre-encoded `.xqg` binary blobs (one per operand shape); committed ground truth |
| `conformance/vectors/gate/<name>/expected_parse.json` | Decoded instruction list each blob must parse to |
| `conformance/vectors/gate/bell_state/circuit.xqg` | 2-qubit Bell circuit; simulation conformance vector |
| `conformance/vectors/gate/bell_state/expected_counts.json` | Measurement distribution bounds (min fraction per outcome) |
| `scripts/check-gate-opcode-parity.py` | CI script: validates Rust `gate_opcodes!` table and Python `_OPCODE_TABLE` both match `gate_opcodes.yaml` |
| `scripts/gen-gate-vectors.py` | Generator: reads `gate_opcodes.yaml`, emits codec vectors under `conformance/vectors/gate/` |

---

## W1 · Part 1 -- `xqgbc` Rust Crate: Scaffold + Opcode Table

**Files:**
- Create: `xqgbc/Cargo.toml`
- Create: `xqgbc/src/lib.rs`
- Create: `xqgbc/src/table.rs`
- Create: `xqgbc/src/types/mod.rs`
- Create: `xqgbc/src/types/operand.rs`
- Modify: `Cargo.toml` (root) -- add `xqgbc` to `members`

**Interfaces:**
- Produces: `gate_opcodes!` X-macro usable by later tasks to generate `GateOpcode`, `GateInstruction`, and codec

- [ ] **Step 1: Add `xqgbc` to root workspace**

In `Cargo.toml` (root), add `"xqgbc"` to the `members` array:
```toml
members = [
    "xqvm",
    "xqasm",
    "xqcli",
    "conformance",
    "xqffi",
    "xqgbc",             # <-- add this
    "fixtures/xqvm-wasm",
]
```

- [ ] **Step 2: Write `xqgbc/Cargo.toml`**

```toml
[package]
name        = "xqgbc"
version     = "0.1.0"
edition     = "2021"
description = "Gate-based quantum bytecode (XQG) format, codec, and verifier."
repository.workspace = true
license.workspace    = true

[features]
default = ["std"]
std     = ["crc32fast/std", "thiserror/std"]

[dependencies]
crc32fast = { version = "1.5", default-features = false }
thiserror  = { version = "2", default-features = false }
pastey     = { workspace = true }

[dev-dependencies]
std::f64::consts = []   # accessed in tests directly; no extra dep needed
```

- [ ] **Step 3: Write `xqgbc/src/types/operand.rs`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Write `xqgbc/src/table.rs`** (the X-macro -- single source of truth)

```rust
// Copyright (C) 2026 Postquant Labs Incorporated
// SPDX-License-Identifier: AGPL-3.0-or-later

/// Invoke `$mac!` with the complete XQG gate opcode table.
///
/// Each entry is an enum variant with a `#[gate(...)]` attribute and an
/// optional named-field body, mirroring Rust enum variant syntax:
///
/// ```text
/// /// Doc comment.
/// #[gate(code, "MNEMONIC", qubit_arity)]
/// VariantName { field: Type, ... },
/// ```
///
/// | Attribute position | Type | Description |
/// |---|---|---|
/// | `code` | `u8` literal | Wire-encoding byte (0x80–0xE8) |
/// | `"MNEMONIC"` | `str` literal | Uppercase assembly mnemonic |
/// | `qubit_arity` | `u8` literal | Number of qubit operands; used by the static verifier |
///
/// `GateAngle` fields are not counted toward `qubit_arity`.
/// All opcodes live in 0x80–0xE8, inside the range `.xqb` reserves as illegal.
///
/// Consumer macros match the pattern:
/// ```rust,ignore
/// macro_rules! my_consumer {
///     (
///         $(
///             $(#[doc = $doc:literal])*
///             #[gate($code:literal, $mnem:literal, $arity:literal)]
///             $var:ident $({ $($fields:tt)* })?
///         ),* $(,)?
///     ) => { /* ... */ }
/// }
/// ```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 5: Write `xqgbc/src/types/mod.rs`** (placeholder for W1 Part 2 types)

```rust
pub mod operand;
pub use operand::{Cbit, GateAngle, Qubit};
// opcode and instruction modules added in W1 Part 2
```

- [ ] **Step 6: Write `xqgbc/src/lib.rs`**

```rust
// Copyright (C) 2026 Postquant Labs Incorporated
// SPDX-License-Identifier: AGPL-3.0-or-later

#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;

#[macro_use]
mod table;

mod types;
pub use types::{Cbit, GateAngle, Qubit};
pub use gate_opcodes;
```

- [ ] **Step 7: Verify it compiles**

```bash
cargo check -p xqgbc
```

Expected: 0 errors, 0 warnings.

- [ ] **Step 8: Commit**

```bash
git add xqgbc/ Cargo.toml Cargo.lock
git commit -m "feat(xqgbc): scaffold crate with gate_opcodes! X-macro and operand types"
```

---

## W1 · Part 2 -- `xqgbc`: Opcode Enum, Instruction Enum, and Codec

**Files:**
- Create: `xqgbc/src/types/opcode.rs`
- Create: `xqgbc/src/types/instruction.rs`
- Create: `xqgbc/src/codec.rs`
- Modify: `xqgbc/src/types/mod.rs`
- Modify: `xqgbc/src/lib.rs`

**Interfaces:**
- Consumes: `gate_opcodes!` from W1 Part 1; `Qubit`, `Cbit`, `GateAngle` from W1 Part 1
- Produces: `GateOpcode`, `GateInstruction`, `codec::encode(&GateInstruction) -> Vec<u8>`, `codec::decode(&[u8]) -> Result<(GateInstruction, usize), DecodeError>`

- [ ] **Step 1: Write failing roundtrip tests** in `xqgbc/tests/roundtrip.rs`

```rust
use xqgbc::{GateInstruction, Qubit, Cbit, GateAngle};
use xqgbc::codec;

#[test]
fn roundtrip_h() {
    let instr = GateInstruction::H { q: Qubit(0) };
    let bytes = codec::encode(&instr);
    assert_eq!(bytes, [0x90, 0x00]);
    let (decoded, consumed) = codec::decode(&bytes).unwrap();
    assert_eq!(decoded, instr);
    assert_eq!(consumed, 2);
}

#[test]
fn roundtrip_cnot() {
    let instr = GateInstruction::Cnot { ctrl: Qubit(0), tgt: Qubit(1) };
    let bytes = codec::encode(&instr);
    assert_eq!(bytes, [0xB0, 0x00, 0x01]);
    let (decoded, consumed) = codec::decode(&bytes).unwrap();
    assert_eq!(decoded, instr);
    assert_eq!(consumed, 3);
}

#[test]
fn roundtrip_rx() {
    use std::f64::consts::PI;
    let instr = GateInstruction::Rx { q: Qubit(2), theta: GateAngle::from_radians(PI / 2.0) };
    let bytes = codec::encode(&instr);
    assert_eq!(bytes.len(), 6); // 1 opcode + 1 qubit + 4 angle (u32, not f64)
    let (decoded, consumed) = codec::decode(&bytes).unwrap();
    assert_eq!(decoded, instr);
    assert_eq!(consumed, 6);
}

#[test]
fn decode_unknown_opcode() {
    let err = codec::decode(&[0xDE]).unwrap_err();
    assert!(matches!(err, xqgbc::codec::DecodeError::UnknownOpcode { byte: 0xDE }));
}
```

Run: `cargo test -p xqgbc` → expected: fails to compile (types not yet defined).

- [ ] **Step 2: Write `xqgbc/src/types/opcode.rs`**

Use the `pastey` crate (already in workspace) for paste-based identifier construction:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Note: `GateOpcode` is written by hand rather than macro-generated to avoid needing `pastey` for a simple enum. The X-macro is the source of truth for `GateInstruction` and the codec.

- [ ] **Step 3: Write `xqgbc/src/types/instruction.rs`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Write `xqgbc/src/codec.rs`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 5: Update `xqgbc/src/types/mod.rs` and `lib.rs`**

`types/mod.rs`:
```rust
pub mod operand;
pub mod opcode;
pub mod instruction;
pub use operand::{Cbit, GateAngle, Qubit};
pub use opcode::GateOpcode;
pub use instruction::GateInstruction;
```

`lib.rs`:
```rust
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;

#[macro_use]
mod table;

mod types;
pub use types::{Cbit, GateAngle, GateInstruction, GateOpcode, Qubit};
pub use gate_opcodes;

pub mod codec;
```

- [ ] **Step 6: Run the roundtrip tests**

```bash
cargo test -p xqgbc
```

Expected: all tests pass.

- [ ] **Step 7: Commit**

```bash
git add xqgbc/
git commit -m "feat(xqgbc): gate instruction enum, opcode enum, and binary codec"
```

---

## W1 · Part 3 -- `xqgbc`: `.xqg` Program Format and Static Verifier

**Files:**
- Create: `xqgbc/src/program.rs`
- Create: `xqgbc/src/verifier.rs`
- Modify: `xqgbc/src/lib.rs`

**Interfaces:**
- Consumes: `GateInstruction`, `codec::encode`/`decode` from W1 Part 2
- Produces: `GateCircuit::encode() -> Vec<u8>`, `GateCircuit::decode(&[u8]) -> Result<GateCircuit, _>`, `static_verify(&GateCircuit) -> Result<(), GateVerifyError>`

### `.xqg` Binary Header (15 bytes)

The gate header is structurally identical to the QUBO (`.xqb`) header -- same size, same
field positions, same CRC scope. The format is distinguished by the magic bytes.

**XQ bytecode family discriminant:** Both formats share the two-byte prefix `XQ`. Bytes
2–3 identify the specific format: `BC` → QUBO circuit; `GB` → gate circuit. A
generic XQ reader can dispatch on bytes `[2:4]` without parsing the rest of the header.

```
Offset  Width  Field             (.xqb / QUBO)        (.xqg / Gate)
------  -----  -----             ------------         -------------
 0..4     4    Magic             b"XQBC"              b"XQGB"
    4     1    version: u8       = 1                  = 1
    5     1    domain_a: u8      input_slots           n_qubits
    6     1    domain_b: u8      output_slots          n_cbits
 7..11    4    domain_c: u32 BE  code_len              n_shots
11..15    4    crc32: u32 BE     CRC-32 over stream   CRC-32 over stream
15+       *    instruction stream
```

`domain_a`/`domain_b`/`domain_c` are format-specific interpretations of the same
structural slots. CRC is stream-only in both formats: header fields are independently
validated by the verifier (qubit/cbit bounds checks; length match), so a corrupt header
byte that disagrees with the stream fails structurally rather than silently.

- [ ] **Step 1: Write failing tests** in `xqgbc/tests/roundtrip.rs` (append):

```rust
use xqgbc::program::GateCircuit;
use xqgbc::verifier::static_verify;

#[test]
fn encode_decode_bell_circuit() {
    use xqgbc::GateInstruction::{H, Cnot, Meas, QHalt};
    let instructions = vec![
        H { q: Qubit(0) },
        Cnot { ctrl: Qubit(0), tgt: Qubit(1) },
        Meas { q: Qubit(0), c: Cbit(0) },
        Meas { q: Qubit(1), c: Cbit(1) },
        QHalt,
    ];
    let circuit = GateCircuit::new(2, 2, 1024, instructions);
    let bytes = circuit.encode();
    let decoded = GateCircuit::decode(&bytes).unwrap();
    assert_eq!(decoded.n_qubits(), 2);
    assert_eq!(decoded.n_cbits(), 2);
    assert_eq!(decoded.n_shots(), 1024);
    assert_eq!(decoded.instructions(), circuit.instructions());
    static_verify(&decoded).unwrap();
}

#[test]
fn verify_rejects_missing_meas() {
    use xqgbc::GateInstruction::{H, QHalt};
    let circuit = GateCircuit::new(1, 0, 100, vec![H { q: Qubit(0) }, QHalt]);
    assert!(static_verify(&circuit).is_err());
}

#[test]
fn verify_rejects_qubit_out_of_bounds() {
    use xqgbc::GateInstruction::{Meas, QHalt};
    // Circuit declares 1 qubit but instruction references qubit 5
    let circuit = GateCircuit::new(1, 1, 100, vec![
        Meas { q: Qubit(5), c: Cbit(0) },
        QHalt,
    ]);
    assert!(static_verify(&circuit).is_err());
}
```

Run: `cargo test -p xqgbc` → expected: compile error (module not found).

- [ ] **Step 2: Write `xqgbc/src/program.rs`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Write `xqgbc/src/verifier.rs`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Update `xqgbc/src/lib.rs`**

```rust
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;

#[macro_use]
mod table;

mod types;
pub use types::{Cbit, GateAngle, GateInstruction, GateOpcode, Qubit};
pub use gate_opcodes;

pub mod codec;
pub mod program;
pub mod verifier;
pub use program::{GateCircuit, GateCircuitDecodeError};
pub use verifier::{static_verify, GateVerifyError};
```

- [ ] **Step 5: Run all tests**

```bash
cargo test -p xqgbc
```

Expected: all pass including the 3 new program/verifier tests.

- [ ] **Step 6: Commit**

```bash
git add xqgbc/
git commit -m "feat(xqgbc): .xqg binary program format and static verifier"
```

---

## W2 · Part 1 -- `xqgb` Python Package: Decoder and `GateSolver` Base

**Files:**
- Create: `xqgb/pyproject.toml`
- Create: `xqgb/__init__.py`
- Create: `xqgb/decoder.py`
- Create: `xqgb/solver.py`
- Create: `xqgb/tests/__init__.py`
- Create: `xqgb/tests/test_decoder.py`
- Modify: root `pyproject.toml` -- add `xqgb` to `[tool.uv.workspace] members`

**Interfaces:**
- Produces: `GateCircuit` (Python dataclass), `decode_xqg(data: bytes) -> GateCircuit`, `GateSolverResult`, `GateSolver` ABC

- [ ] **Step 1: Write `xqgb/tests/test_decoder.py`** (failing tests first)

```
/* Detailed source-code truncated for AI context efficiency. */
```

Run: `cd /home/konrad/Quip/xquad && uv run pytest xqgb/tests/test_decoder.py` → expected: import error.

- [ ] **Step 2: Write `xqgb/pyproject.toml`**

```toml
[build-system]
requires      = ["hatchling"]
build-backend = "hatchling.build"

[project]
name            = "xqgb"
version         = "0.1.0"
description     = "Gate-circuit backends for the XQuad toolchain (CPU sim, IBM Quantum, IonQ)."
requires-python = ">=3.13"
license         = { text = "AGPL-3.0-or-later" }
dependencies    = ["numpy>=2.0"]

[project.optional-dependencies]
ibm  = ["qiskit>=1.0", "qiskit-aer>=0.14", "qiskit-ibm-runtime>=0.20"]
ionq = ["ionq>=0.3"]

[tool.hatch.build.targets.wheel]
packages = ["."]
exclude  = ["tests/**", "pyproject.toml"]
```

- [ ] **Step 3: Add `xqgb` to root `pyproject.toml` workspace**

Locate `[tool.uv.workspace]` in root `pyproject.toml` and add `"xqgb"`:

```toml
[tool.uv.workspace]
members = ["xqffi", "xqvm_py", "xqcp", "xqsa", "xquad", "xqgb"]
```

Also add `"xqgb/tests"` to `[tool.pytest.ini_options] testpaths` if that list exists.

- [ ] **Step 4: Write `xqgb/decoder.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 5: Write `xqgb/solver.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 6: Write `xqgb/__init__.py`**

```python
# Copyright (C) 2026 Postquant Labs Incorporated
# SPDX-License-Identifier: AGPL-3.0-or-later

"""XQuad gate-circuit backend package."""

from xqgb.decoder import GateCircuit, XqgHeader, RawGateInstruction, decode_xqg
from xqgb.solver import GateSolver, GateSolverResult

__all__ = [
    "GateCircuit",
    "XqgHeader",
    "RawGateInstruction",
    "decode_xqg",
    "GateSolver",
    "GateSolverResult",
]
```

- [ ] **Step 7: Run decoder tests**

```bash
cd /home/konrad/Quip/xquad && uv run pytest xqgb/tests/test_decoder.py -v
```

Expected: all 4 tests pass.

- [ ] **Step 8: Commit**

```bash
git add xqgb/ pyproject.toml
git commit -m "feat(xqgb): Python package scaffold with .xqg decoder and GateSolver ABC"
```

---

## W2 · Part 2 -- `xqgb`: NumPy Statevector CPU Simulator

**Files:**
- Create: `xqgb/cpu_sim.py`
- Create: `xqgb/tests/test_cpu_sim.py`
- Modify: `xqgb/__init__.py`

**Interfaces:**
- Consumes: `GateCircuit`, `GateSolver`, `GateSolverResult` from W2 Part 1
- Produces: `SolverCPUSim(GateSolver)` with `run(circuit) -> GateSolverResult`

- [ ] **Step 1: Write failing tests** in `xqgb/tests/test_cpu_sim.py`

```
/* Detailed source-code truncated for AI context efficiency. */
```

Run: `uv run pytest xqgb/tests/test_cpu_sim.py` → expected: import error.

- [ ] **Step 2: Write `xqgb/cpu_sim.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Run the CPU sim tests**

```bash
uv run pytest xqgb/tests/test_cpu_sim.py -v
```

Expected: all 5 tests pass.

- [ ] **Step 4: Commit**

```bash
git add xqgb/cpu_sim.py xqgb/tests/test_cpu_sim.py
git commit -m "feat(xqgb): NumPy statevector CPU simulator with full gate set"
```

---

## W3 · Part 1 -- `xqgb`: Transpilers and IBM Quantum Backend

**Files:**
- Create: `xqgb/transpiler.py`
- Create: `xqgb/ibm_quantum.py`

**Interfaces:**
- Consumes: `GateCircuit`, `GateSolver`, `GateSolverResult` from W2 Part 1
- Produces: `QiskitTranspiler`, `IonQTranspiler`, `SolverIBMQuantum(GateSolver)`

- [ ] **Step 1: Write `xqgb/transpiler.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Write `xqgb/ibm_quantum.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Lint check**

```bash
uv run ruff check xqgb/transpiler.py xqgb/ibm_quantum.py
```

Expected: 0 issues.

- [ ] **Step 4: Commit**

```bash
git add xqgb/transpiler.py xqgb/ibm_quantum.py
git commit -m "feat(xqgb): Qiskit/IonQ transpilers and IBM Quantum backend"
```

---

## W3 · Part 2 -- `xqgb`: IonQ Backend and Registry

**Files:**
- Create: `xqgb/ionq.py`
- Create: `xqgb/registry.py`
- Create: `xqgb/tests/test_registry.py`
- Modify: `xqgb/__init__.py`

**Interfaces:**
- Consumes: all solver classes from W2 Part 2 and W3 Part 1, `IonQTranspiler` from W3 Part 1
- Produces: `SolverIonQ(GateSolver)`, `GATE_SOLVERS` dict, `build_gate_solver(name, **kwargs) -> GateSolver`

- [ ] **Step 1: Write failing tests** in `xqgb/tests/test_registry.py`

```python
"""Registry unit tests -- no cloud credentials needed."""

import pytest

from xqgb.registry import GATE_SOLVERS, DEFAULT_GATE_SOLVER, build_gate_solver
from xqgb.cpu_sim import SolverCPUSim


def test_all_solvers_registered():
    expected = {"cpu-sim", "ibm-aer", "ibm-quantum", "ionq-sim", "ionq"}
    assert expected == set(GATE_SOLVERS.keys())


def test_default_solver_is_cpu_sim():
    assert DEFAULT_GATE_SOLVER == "cpu-sim"


def test_build_cpu_sim():
    solver = build_gate_solver("cpu-sim", seed=42)
    assert isinstance(solver, SolverCPUSim)


def test_build_unknown_raises():
    with pytest.raises(ValueError, match="Unknown gate solver"):
        build_gate_solver("not-a-real-solver")
```

Run: `uv run pytest xqgb/tests/test_registry.py` → expected: import error.

- [ ] **Step 2: Write `xqgb/ionq.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Write `xqgb/registry.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Update `xqgb/__init__.py`** to expose registry

```python
# Copyright (C) 2026 Postquant Labs Incorporated
# SPDX-License-Identifier: AGPL-3.0-or-later

"""XQuad gate-circuit backend package."""

from xqgb.decoder import GateCircuit, XqgHeader, RawGateInstruction, decode_xqg
from xqgb.solver import GateSolver, GateSolverResult
from xqgb.registry import GATE_SOLVERS, DEFAULT_GATE_SOLVER, build_gate_solver

__all__ = [
    "GateCircuit",
    "XqgHeader",
    "RawGateInstruction",
    "decode_xqg",
    "GateSolver",
    "GateSolverResult",
    "GATE_SOLVERS",
    "DEFAULT_GATE_SOLVER",
    "build_gate_solver",
]
```

- [ ] **Step 5: Run all tests**

```bash
uv run pytest xqgb/ -v
```

Expected: all 12 tests pass (4 decoder + 5 CPU sim + 3 registry).

- [ ] **Step 6: Run Rust tests**

```bash
cargo test -p xqgbc
```

Expected: all pass.

- [ ] **Step 7: Lint**

```bash
uv run ruff check xqgb/
cargo clippy -p xqgbc -- -D warnings
```

Expected: 0 issues each.

- [ ] **Step 8: Commit**

```bash
git add xqgb/ionq.py xqgb/registry.py xqgb/tests/test_registry.py xqgb/__init__.py
git commit -m "feat(xqgb): IonQ backend and gate solver registry"
```

---

## W4 -- Conformance Hardening: Gate ISA Parity

**Files:**
- Create: `conformance/gate_opcodes.yaml`
- Create: `scripts/check-gate-opcode-parity.py`
- Create: `scripts/gen-gate-vectors.py`
- Create: `conformance/vectors/gate/` (generated then committed)
- Modify: `conformance/build.rs` -- add `gate` vector category runner
- Modify: `conformance/Cargo.toml` -- add `xqgbc` dependency
- Modify: `conformance/src/lib.rs` -- add `run_rust_gate` and `run_python_gate`

**Interfaces:**
- Consumes: `xqgbc::codec::{encode, decode}` from W1 Part 2; `xqgb.decoder.decode_xqg` from W2 Part 1
- Produces: CI checks that fail if Rust and Python gate ISA tables or wire codecs diverge

- [ ] **Step 1: Write `conformance/gate_opcodes.yaml`**

This is the canonical source of truth. Every gate opcode appears once; both the Rust parity check and Python parity check are validated against it.

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run the parity check (will fail -- files not yet matched)**

```bash
uv run python scripts/check-gate-opcode-parity.py
```

Expected: `ERROR` -- Python `_OPCODE_TABLE` and/or Rust `gate_opcodes!` not yet validated against the YAML. This confirms the script works.

- [ ] **Step 3: Write `scripts/check-gate-opcode-parity.py`**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Run the parity check against live code**

```bash
uv run python scripts/check-gate-opcode-parity.py
```

Expected: `OK -- 25 gate opcodes match across YAML, Rust, and Python.`

Fix any differences reported before proceeding.

- [ ] **Step 5: Write `scripts/gen-gate-vectors.py`**

This generates one codec vector per distinct operand shape (not per opcode -- that would be 25 near-identical files). Each vector is a minimal valid `.xqg` with sentinel operand values, encoded by Python to guarantee the expected bytes are independent of the Rust encoder.

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 6: Generate and commit the vectors**

```bash
uv run python scripts/gen-gate-vectors.py
```

Expected output lists each generated directory. Verify the files look sane:

```bash
ls conformance/vectors/gate/
xxd conformance/vectors/gate/codec_rx/circuit.xqg
# Should show: 58 51 47 42 (XQGB magic), then header bytes, then A0 02 40 00 00 00 81
cat conformance/vectors/gate/codec_rx/expected_parse.json
```

Commit:

```bash
git add conformance/gate_opcodes.yaml conformance/vectors/gate/ scripts/
git commit -m "feat(conformance): gate_opcodes.yaml + codec/simulation conformance vectors"
```

- [ ] **Step 7: Extend `conformance/Cargo.toml` with `xqgbc` dep**

```toml
[dependencies]
xqvm.workspace   = true
xqasm.workspace  = true
xqgbc.workspace  = true          # add this line
serde            = { version = "1", features = ["derive"] }
serde_json       = "1"
tempfile         = "3"
```

- [ ] **Step 8: Add gate vector runners to `conformance/src/lib.rs`**

Add these two functions at the bottom of `conformance/src/lib.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 9: Add `xqgb decode` CLI entry point to `xqgb/__main__.py`**

The Python gate runner above calls `python -m xqgb decode <path>`. Add this module:

```python
# Copyright (C) 2026 Postquant Labs Incorporated
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Minimal CLI for conformance harness: `python -m xqgb decode <path.xqg>`."""

from __future__ import annotations

import json
import sys
from pathlib import Path

from xqgb.decoder import decode_xqg


def main() -> None:
    if len(sys.argv) < 3 or sys.argv[1] != "decode":
        print(
            "Usage: python -m xqgb decode <path.xqg>",
            file=sys.stderr,
        )
        sys.exit(2)

    data = Path(sys.argv[2]).read_bytes()
    circuit = decode_xqg(data)
    result = [
        {"mnemonic": instr.mnemonic, "operands": list(instr.operands)}
        for instr in circuit.instructions
    ]
    print(json.dumps(result))


if __name__ == "__main__":
    main()
```

- [ ] **Step 10: Extend `conformance/build.rs` to generate gate vector tests**

In `build.rs`, the `discover_vectors` function already scans `vectors/<category>/<name>/`. Gate codec vectors are discovered automatically because they live under `vectors/gate/`. Add a secondary discovery pass for the `expected_parse.json` sentinel:

```rust
// In discover_vectors(), change the required-files check to accept either
// the classic triple OR a gate codec vector:
if (dir.join("program.xqasm").exists()
    && dir.join("inputs.json").exists()
    && dir.join("expected.json").exists())
    || (dir.join("circuit.xqg").exists()
        && dir.join("expected_parse.json").exists())
{
    out.push((category.clone(), name));
}
```

And in `write_test_file`, dispatch on the vector kind:

```rust
// Replace the single call to xquad_conformance::check_vector with:
writeln!(
    body,
    "#[test]\nfn {fn_name}() {{\n    xquad_conformance::check_any_vector(\"{category}\", \"{name}\", xquad_conformance::Impl::{imp});\n}}\n"
)
```

Add `check_any_vector` to `lib.rs`:

```rust
/// Dispatch to the correct check function based on which files exist.
/// Classic vectors (program.xqasm + expected.json) use check_vector.
/// Gate codec vectors (circuit.xqg + expected_parse.json) use check_gate_codec_vector.
#[expect(clippy::panic, reason = "test-harness entry point")]
pub fn check_any_vector(category: &str, name: &str, imp: Impl) {
    let dir = conformance_root().join("vectors").join(category).join(name);
    if dir.join("circuit.xqg").exists() {
        check_gate_codec_vector(category, name, imp);
    } else {
        check_vector(category, name, imp);
    }
}
```

- [ ] **Step 11: Run the full conformance suite**

```bash
cargo test -p xquad-conformance
```

Expected: all prior vectors still pass, plus new `vector_gate_codec_*` tests appear and pass for both Rust and Python.

- [ ] **Step 12: Add parity check to CI**

In `.github/workflows/` (whichever workflow runs Python checks), add:

```yaml
- name: Gate opcode parity
  run: uv run python scripts/check-gate-opcode-parity.py
```

- [ ] **Step 13: Commit**

```bash
git add \
  conformance/Cargo.toml \
  conformance/src/lib.rs \
  conformance/build.rs \
  xqgb/__main__.py \
  scripts/check-gate-opcode-parity.py \
  scripts/gen-gate-vectors.py
git commit -m "feat(conformance): gate ISA parity check + codec conformance harness"
```

---

## Verification

### End-to-end (CPU sim -- no cloud credentials needed)

```python
import math, struct, zlib
from xqgb import decode_xqg, build_gate_solver

# Build a 2-qubit Bell circuit .xqg in memory
# Note: rotation gates (RX, RY, RZ, P, U, CP, RZZ, CRX) encode angles as
# u32 binary-angular units (full turn = 2^32). Helper for those:
#   def encode_angle(r): return struct.pack(">I", round(r % (2*math.pi) / (2*math.pi) * 4_294_967_296) & 0xFFFFFFFF)
# Bell circuit has no rotation gates, so no angle encoding needed here.

stream = (
    b"\x90\x00"          # H q=0
    b"\xB0\x00\x01"      # CNOT ctrl=0 tgt=1
    b"\x83\x00\x00"      # MEAS q=0 c=0
    b"\x83\x01\x01"      # MEAS q=1 c=1
    b"\x81"              # QHALT
)
crc = zlib.crc32(stream) & 0xFFFFFFFF
header = b"XQGB" + bytes([1, 2, 2]) + struct.pack(">I", 1024) + struct.pack(">I", crc)
data = header + stream

circuit = decode_xqg(data)
solver = build_gate_solver("cpu-sim", seed=42)
result = solver.run(circuit)

print(result.counts)          # e.g. {"00": 510, "11": 514}
assert set(result.counts) <= {"00", "11"}
assert result.total_shots == 1024
print("Bell state verification passed.")
```

### Rust roundtrip

```bash
cargo test -p xqgbc -- --nocapture
```

### IBM Aer (requires `pip install xqgb[ibm]`)

```python
from xqgb import decode_xqg, build_gate_solver
# same data bytes as above
circuit = decode_xqg(data)
solver = build_gate_solver("ibm-aer")
result = solver.run(circuit)
print(result.counts)
assert set(result.counts) <= {"00", "11"}
```

### IonQ sim (requires `pip install xqgb[ionq]` and `IONQ_API_KEY`)

```python
import os
os.environ["IONQ_API_KEY"] = "your-key-here"
solver = build_gate_solver("ionq-sim")
result = solver.run(circuit)
print(result.counts)
```

---

## W5 -- Missing Gate Primitives: `RESET`, `CCX`, `CSWAP`

**Background:** Three standard OpenQASM 3 primitives are absent from the v1 gate table:
`reset` (qubit mid-circuit reset to |0⟩), `ccx` (Toffoli / doubly-controlled-X), and
`cswap` (Fredkin / controlled-SWAP). Both IBM and IonQ support all three natively.
Without `RESET`, qubit reuse in error correction is impossible. Without `CCX`/`CSWAP`,
classical-reversible sub-circuits require decomposition into 1- and 2-qubit gates,
inflating circuit depth unnecessarily.

Partial `BARRIER` with a qubit list is deferred -- it requires variable-length operands,
which would break the codec invariant that the opcode byte alone determines operand width.
The global `BARRIER` (0x82) remains sufficient.

**Opcode placement:**

| Byte | Mnemonic | Operands | Group |
|---|---|---|---|
| `0x88` | `RESET` | `q: Qubit` | Circuit control (gap after CF opcodes) |
| `0xB4` | `CCX` | `ctrl0: Qubit, ctrl1: Qubit, tgt: Qubit` | Three-qubit fixed gates |
| `0xB5` | `CSWAP` | `ctrl: Qubit, q0: Qubit, q1: Qubit` | Three-qubit fixed gates |

`0xB4–0xBF` extends the existing two-qubit fixed-gate group (`0xB0–0xB3`).

**Files:**
- Modify: `conformance/gate_opcodes.yaml` -- 3 new entries
- Modify: `xqgbc/src/table.rs` -- 3 entries in `gate_opcodes!`
- Modify: `xqgbc/src/types/opcode.rs` -- 3 variants
- Modify: `xqgbc/src/types/instruction.rs` -- 3 variants + `opcode()`/`mnemonic()` arms
- Modify: `xqgbc/src/codec.rs` -- encode/decode arms
- Modify: `xqgbc/src/verifier.rs` -- extend `check_qubits` for 3-qubit instructions
- Modify: `xqgb/decoder.py` -- 3 entries in `_OPCODE_TABLE` (`"qqq"` / `"qq"` formats)
- Modify: `xqgb/cpu_sim.py` -- `RESET` collapses statevector to |0⟩ on qubit q; `CCX`
  and `CSWAP` applied via `_apply_three()` using 8×8 unitaries
- Modify: `xqgb/transpiler.py` -- add `QiskitTranspiler` and `IonQTranspiler` mappings

**Interfaces:**
- Consumes: W1 (xqgbc), W2–W3 (xqgb), W4 (conformance)
- Produces: `RESET`/`CCX`/`CSWAP` in Rust codec and Python decoder; CPU sim `_apply_three()`
  helper for 8×8 unitaries; parity check updated to 28 opcodes (32 if W6 Part 1's four CF opcodes have already landed)

---

- [ ] **Step 1: Update `conformance/gate_opcodes.yaml`**

```yaml
  - code: 0x88
    mnemonic: RESET
    operands:
      - {name: q, type: qubit, width: 1}

  - code: 0xB4
    mnemonic: CCX
    operands:
      - {name: ctrl0, type: qubit, width: 1}
      - {name: ctrl1, type: qubit, width: 1}
      - {name: tgt,   type: qubit, width: 1}

  - code: 0xB5
    mnemonic: CSWAP
    operands:
      - {name: ctrl, type: qubit, width: 1}
      - {name: q0,   type: qubit, width: 1}
      - {name: q1,   type: qubit, width: 1}
```

- [ ] **Step 2: Add entries to `xqgbc/src/table.rs`**

After `Barrier`/`Meas` group, add `RESET` in the circuit-control section; add `CCX`/`CSWAP` after `ISwap` in the two-qubit fixed group:

```rust
            /// Reset qubit to |0⟩ (mid-circuit).
            #[gate(0x88, "RESET", 1)]
            Reset { q: $crate::Qubit },

            // (in two-qubit fixed group, after ISwap)
            /// Toffoli (doubly-controlled-X) gate.
            #[gate(0xB4, "CCX", 3)]
            Ccx { ctrl0: $crate::Qubit, ctrl1: $crate::Qubit, tgt: $crate::Qubit },

            /// Fredkin (controlled-SWAP) gate.
            #[gate(0xB5, "CSWAP", 3)]
            Cswap { ctrl: $crate::Qubit, q0: $crate::Qubit, q1: $crate::Qubit },
```

- [ ] **Step 3: Extend `opcode.rs`, `instruction.rs`, `codec.rs`** following the same
pattern as existing single- and two-qubit gates. `check_qubits` in `verifier.rs` gains
two new arms returning `&[ctrl0.0, ctrl1.0, tgt.0]` and `&[ctrl.0, q0.0, q1.0]`.

- [ ] **Step 4: Add `_apply_three` helper to `xqgb/cpu_sim.py`**

```python
def _apply_three(sv, n, q0, q1, q2, u):
    """Apply 8x8 unitary to (q0, q1, q2) in an n-qubit statevector."""
    sv = sv.reshape([2] * n)
    axes = [q0, q1, q2] + [i for i in range(n) if i not in (q0, q1, q2)]
    sv = sv.transpose(axes).reshape(8, -1)
    sv = u @ sv
    sv = sv.reshape([2, 2, 2] + [2] * (n - 3))
    inv_axes = [0] * n
    for new, old in enumerate(axes):
        inv_axes[old] = new
    return sv.transpose(inv_axes).reshape(-1)
```

`RESET` collapses the statevector: project onto `q == 0`, renormalise.

```python
if m == "RESET":
    q = int(ops[0])
    sv = sv.reshape([2] * n_qubits)
    # Zero out all amplitudes where qubit q == 1
    idx = [slice(None)] * n_qubits
    idx[q] = 1
    sv[tuple(idx)] = 0.0
    sv = sv.reshape(-1)
    norm = np.sqrt((sv * sv.conj()).real.sum())
    if norm > 0:
        sv /= norm
    pos += 1
    continue
```

- [ ] **Step 5: Run tests and parity check**

```bash
cargo test -p xqgbc
uv run pytest xqgb/ -v
uv run python scripts/check-gate-opcode-parity.py
# Expected: OK -- N gate opcodes match, where N = 25 + 3 (W5) + any of W6 Part 1's 4 CF opcodes already landed
```

- [ ] **Step 6: Commit**

```bash
git commit -m "feat(xqgbc,xqgb): RESET, CCX (Toffoli), CSWAP (Fredkin) gate primitives"
```

---

## W6 · Part 2 -- Wide-Form CF Jumps

**Background:** `CF_JZ`/`CF_JNZ`/`CF_JUMP` use `u8` label IDs (max 255 distinct labels
per circuit). Circuits compiled from unrolled `for` loops or deep conditional chains can
exceed this limit. Wide-form counterparts with `u16` IDs follow the `Jump1`/`Jump2`
precedent from the QUBO ISA exactly, and require no header changes -- the 15-byte header
is kept intact, matching the QUBO format's unchanging 15-byte structure.

The header CRC remains stream-only (consistent with the QUBO format's design: header
fields are validated structurally by the verifier, not by the checksum). No feature-flags
byte is added; format evolution is handled by version bumps.

**Wide-form CF opcodes (in gap `0x8A–0x8C`):**

| Byte | Mnemonic | Operands |
|---|---|---|
| `0x8A` | `CF_JUMP2` | `id: u16` |
| `0x8B` | `CF_JZ2` | `c: Cbit, id: u16` |
| `0x8C` | `CF_JNZ2` | `c: Cbit, id: u16` |

**Files:**
- Modify: `conformance/gate_opcodes.yaml` -- 3 new CF wide-form entries (type `label16`, width 2)
- Modify: `xqgbc/src/table.rs` -- 3 new CF wide-form entries in `gate_opcodes!`
- Modify: `xqgbc/src/types/opcode.rs` -- 3 new variants
- Modify: `xqgbc/src/types/instruction.rs` -- 3 new variants
- Modify: `xqgbc/src/codec.rs` -- encode/decode arms (`u16` already has `EncodeOperand`/`DecodeOperand`)
- Modify: `xqgbc/src/verifier.rs` -- wide-form jumps participate in label-consistency check
- Modify: `xqgb/decoder.py` -- `_OPCODE_TABLE` entries; `'L'` format char for `u16` labels
- Modify: `xqgb/cpu_sim.py` -- handle `CF_JUMP2`/`CF_JZ2`/`CF_JNZ2` in dispatch loop

**Interfaces:**
- Consumes: W1, W4, W6 Part 1
- Produces: 3 wide-form CF opcodes; `'L'` format char in `_OPCODE_TABLE` decoder

---

- [ ] **Step 1: Add 3 entries to `conformance/gate_opcodes.yaml`**

```yaml
  - code: 0x8A
    mnemonic: CF_JUMP2
    operands:
      - {name: id, type: label16, width: 2}

  - code: 0x8B
    mnemonic: CF_JZ2
    operands:
      - {name: c,  type: cbit,    width: 1}
      - {name: id, type: label16, width: 2}

  - code: 0x8C
    mnemonic: CF_JNZ2
    operands:
      - {name: c,  type: cbit,    width: 1}
      - {name: id, type: label16, width: 2}
```

Also add `label16` to the type maps in `check-gate-opcode-parity.py`.

- [ ] **Step 2: Add entries to `xqgbc/src/table.rs`**

```rust
            #[gate(0x8A, "CF_JUMP2", 0)]
            CfJump2 { id: u16 },
            #[gate(0x8B, "CF_JZ2", 0)]
            CfJz2 { c: $crate::Cbit, id: u16 },
            #[gate(0x8C, "CF_JNZ2", 0)]
            CfJnz2 { c: $crate::Cbit, id: u16 },
```

- [ ] **Step 3: Extend `xqgbc/src/verifier.rs`** -- the label-consistency check already
uses pattern matching; extend all three `match` arms to also cover the `2` variants:

```rust
            let referenced = match instr {
                GateInstruction::CfJump  { id }
                | GateInstruction::CfJump2 { id }
                | GateInstruction::CfJz   { id, .. }
                | GateInstruction::CfJz2  { id, .. }
                | GateInstruction::CfJnz  { id, .. }
                | GateInstruction::CfJnz2 { id, .. } => Some(*id as u16),
                _ => None,
            };
```

Note: `CF_TARGET` label IDs remain `u8` (the target declaration), but `CF_JUMP2` can
reference any `u16`. This means a `CF_JUMP2` referencing id > 255 will always fail the
consistency check -- which is correct since no `CF_TARGET` can have id > 255. Wide-form
is useful only when you have many distinct labels, not larger individual label IDs. If
label IDs > 255 are needed, `CF_TARGET2` would be required (deferred).

- [ ] **Step 4: Update `xqgb/decoder.py`** -- add `'L'` format character for `u16`
label operands and add 3 opcode entries:

```python
elif char == "L":   # u16 label (wide-form CF)
    if pos + consumed + 2 > len(stream):
        raise ValueError(f"Stream truncated reading u16 label at offset {pos + consumed}")
    (val,) = struct.unpack_from(">H", stream, pos + consumed)
    values.append(val)
    consumed += 2
```

```python
    0x8A: ("CF_JUMP2", "L"),
    0x8B: ("CF_JZ2",   "cL"),
    0x8C: ("CF_JNZ2",  "cL"),
```

- [ ] **Step 5: Extend `xqgb/cpu_sim.py`** -- in the CF dispatch block:

```python
if m in ("CF_JUMP2", "CF_JUMP"):
    pos = _find_label(instructions, ops[0])
    continue
if m in ("CF_JZ2", "CF_JZ"):
    pos = _find_label(instructions, ops[1]) if cbits[ops[0]] == 0 else pos + 1
    continue
if m in ("CF_JNZ2", "CF_JNZ"):
    pos = _find_label(instructions, ops[1]) if cbits[ops[0]] != 0 else pos + 1
    continue
```

- [ ] **Step 6: Write tests** -- encode a circuit with 260 `CF_TARGET` + `CF_JUMP2`
labels (impossible with `u8`), round-trip through Rust codec, verify Python decoder
reconstructs all jump targets correctly.

- [ ] **Step 7: Run suite and parity check**

```bash
cargo test -p xqgbc && uv run pytest xqgb/ -v
uv run python scripts/check-gate-opcode-parity.py
# Expected: OK -- 35 gate opcodes match (32 = W1 + W5 + W6 Part 1, plus 3 wide-form CF; counts assume that landing order)
```

- [ ] **Step 8: Commit**

```bash
git commit -m "feat(xqgbc,xqgb): wide-form CF_JUMP2/JZ2/JNZ2 with u16 label IDs"
```

---

## W7 -- Parameterized Circuits

**Background:** VQE, QAOA, and similar variational algorithms execute the same circuit
structure hundreds or thousands of times with different rotation angles. Currently every
parameter sweep requires re-encoding the full `.xqg` file.

**Design:** Format version 2 adds a parameter preamble immediately after the 15-byte
header. The header itself is unchanged (same magic, same field layout as v1). The version
byte distinguishes the two:

```
v1 file (15 bytes header, then instruction stream):
  [15-byte XQGB header, version=1] [instruction stream]

v2 file (15 bytes header + parameter preamble, then instruction stream):
  [15-byte XQGB header, version=2] [n_params: u8] [u32 BE × n_params] [instruction stream]
```

The CRC in the header covers the stream bytes only (i.e. the instruction stream excluding
the parameter preamble). The parameter preamble is validated structurally: the decoder
reads `n_params` and then exactly `4 × n_params` bytes before handing the stream to the
instruction decoder.

Angle operands that reference a parameter slot use the high bit of their `u32` value as
a flag: bit 31 = 0 → literal value; bit 31 = 1 → parameter-slot reference
(bits 0–7 = slot index). In v2 files a literal angle therefore has 31 bits of precision
and a full turn is 2^31 units (v1: 2^32). Without this change every literal angle of pi
or more would set bit 31 and be misread as a slot reference. The two versions are not
interchangeable: a v1 decoder reading a v2 file misreads every angle, so decoders must
reject unknown versions rather than guess, and angle-to-radian conversion must key off
the file version.

**Files:**
- Modify: `xqgbc/src/program.rs` -- add `params: Vec<GateAngle>` to `GateCircuit`;
  encode v2 preamble when `params` is non-empty; decode v2 preamble when `version == 2`;
  add `GateAngle::is_param_ref()`, `GateAngle::param_slot()`, `GateAngle::from_param_slot()`
- Modify: `xqgbc/src/verifier.rs` -- every `is_param_ref()` angle must have slot index `< n_params`
- Modify: `xqgb/decoder.py` -- read v2 preamble when `data[4] == 2`; expose `circuit.params`;
  add `circuit.bind(values: list[float]) -> GateCircuit`
- Modify: `xqgb/solver.py` -- `GateSolver.run()` gains optional `params: list[float]` kwarg

**Interfaces:**
- Consumes: W1 (codec, program format); W6 Part 2 (no dependency -- header unchanged)
- Produces: `GateCircuit.params`, `GateCircuit.bind()`, `GateSolver.run(params=...)`

---

- [ ] **Step 1: Add `is_param_ref` and `param_slot` to `GateAngle`** in
`xqgbc/src/types/operand.rs`:

```rust
impl GateAngle {
    /// True when this value encodes a parameter-slot reference (bit 31 set).
    pub fn is_param_ref(self) -> bool { self.0 & 0x8000_0000 != 0 }

    /// The slot index when `is_param_ref()` is true.
    pub fn param_slot(self) -> u8 { (self.0 & 0xFF) as u8 }

    /// Construct a parameter-slot reference.
    pub fn from_param_slot(slot: u8) -> Self { Self(0x8000_0000 | slot as u32) }
}
```

- [ ] **Step 2: Extend `GateCircuit::encode()`** -- if `self.params` is non-empty, set
version byte = 2 and emit preamble (`n_params: u8`, then each param as `u32 BE`) before
the instruction stream. CRC is still computed over the instruction stream bytes only.

`GateCircuit::decode()` branches on `bytes[4]`: 1 → stream starts at byte 15; 2 → read
preamble starting at byte 15, stream starts at `15 + 1 + 4 * n_params`.

- [ ] **Step 3: Extend `xqgbc/src/verifier.rs`** -- after the qubit/cbit bounds check
loop, add a second pass: every angle operand with `is_param_ref()` set must have
`param_slot() < circuit.params().len()`.

- [ ] **Step 4: Implement `bind()` in `xqgb/decoder.py`**

```python
def bind(self, values: list[float]) -> "GateCircuit":
    """Return a new GateCircuit with all parameter references replaced by concrete angles."""
    _PARAM_FLAG = 0x8000_0000
    if len(values) != len(self.params):
        raise ValueError(f"expected {len(self.params)} parameter values, got {len(values)}")

    def resolve(op):
        if isinstance(op, int) and op & _PARAM_FLAG:
            slot = op & 0xFF
            # v2 literal angles are 31-bit: full turn = 2^31 units.
            return round(values[slot] % (2 * math.pi) / (2 * math.pi) * 2_147_483_648) & 0x7FFF_FFFF
        return op
    new_instrs = [
        RawGateInstruction(i.mnemonic, tuple(resolve(o) for o in i.operands))
        for i in self.instructions
    ]
    # Bound circuits carry no preamble; re-encoding emits a v2 file with n_params = 0.
    return GateCircuit(header=self.header, params=[], instructions=new_instrs)
```

- [ ] **Step 5: Write tests** -- a v2 parameterized `RX` circuit with `θ = slot 0`,
default 0. Encode, round-trip decode, bind `θ = π`, assert same distribution as literal
`RX(π)`. Also: v1 files still decode identically, a v2 literal `RX(3*pi/2)` round-trips as a literal (not a slot reference), and `bind()` with the wrong number of values raises `ValueError`.

- [ ] **Step 6: Commit**

```bash
git commit -m "feat(xqgbc,xqgb): v2 format -- parameterized circuits with preamble parameter table"
```

---

## W6 · Part 3 -- Classical Integer Registers

**Background:** The CF opcodes (`CF_JZ`/`CF_JNZ`) can only branch on a single `Cbit`
(one measurement bit). OpenQASM 3 also supports `int` classical registers that accumulate
values, support arithmetic, and can be compared in multi-bit conditions
(`if (syndrome == 3)`). Without this, multi-qubit error syndrome decoding must happen
off-chip.

**Design:** A classical register file of 8 `u16` slots (`CR0`–`CR7`). Format version 3
adds an `n_cregs: u8` preamble byte immediately after the parameter preamble (or directly
after the 15-byte header if no parameter preamble is present). The 15-byte header is
unchanged; the version byte (= 3) signals the preamble. New opcode group `0xE0–0xEF`:

| Byte | Mnemonic | Operands | Description |
|---|---|---|---|
| `0xE0` | `CSET` | `reg: u8, val: u8` | Load 8-bit immediate into classical register |
| `0xE1` | `CADD` | `dst: u8, src: u8` | `CR[dst] += CR[src]` |
| `0xE2` | `CSUB` | `dst: u8, src: u8` | `CR[dst] -= CR[src]` |
| `0xE3` | `CINC` | `reg: u8` | `CR[reg] += 1` |
| `0xE4` | `CDEC` | `reg: u8` | `CR[reg] -= 1` |
| `0xE5` | `CMBIT` | `reg: u8, c: Cbit` | Copy `Cbit` value into `CR[reg]` bit 0 |
| `0xE6` | `CJEQ` | `r0: u8, r1: u8, id: u8` | Jump to label if `CR[r0] == CR[r1]` |
| `0xE7` | `CJLT` | `r0: u8, r1: u8, id: u8` | Jump to label if `CR[r0] < CR[r1]` |
| `0xE8` | `CJGT` | `r0: u8, r1: u8, id: u8` | Jump to label if `CR[r0] > CR[r1]` |

`CF_LOOP`/`CF_NEXT` (deliberately omitted from W6 Part 1) can now be expressed as
`CSET` + `CF_TARGET` + (body) + `CDEC` + `CJGT reg 0 label`, which is explicit and
auditable.

`CMBIT` bridges the Cbit/classical-register boundary: after a MEAS, copy the result
into a register accumulator for syndrome arithmetic.

**Files:**
- Modify: `conformance/gate_opcodes.yaml` -- 9 new entries (type `creg`, width 1)
- Modify: `xqgbc/src/table.rs`, `opcode.rs`, `instruction.rs`, `codec.rs` -- 9 new variants
- Modify: `xqgbc/src/verifier.rs` -- `CJEQ`/`CJLT`/`CJGT` participate in label-consistency
  check; `CSET`/`CMBIT` register-index bounds check (`< n_cregs`)
- Modify: `xqgb/decoder.py` -- 9 entries with `'r'` format char for `creg` operands
- Modify: `xqgb/cpu_sim.py` -- add `cregs = [0] * 8` to `simulate()`; dispatch all `C*`
  opcodes; `CJEQ`/`CJLT`/`CJGT` call `_find_label`
- No transpiler changes -- backends receive the classical registers as opaque; IBM and IonQ
  do not support this natively and `QiskitTranspiler`/`IonQTranspiler` should raise
  `ValueError` on any `0xE*` opcode

**Interfaces:**
- Consumes: W6 Part 1 (CF label infrastructure), W6 Part 2 (header v2 `flags`/`n_cregs`)
- Produces: 8-slot u16 classical register file; arithmetic/comparison opcodes; loop
  counter idiom via `CSET`+`CDEC`+`CJGT`; `CMBIT` bridge from Cbit to register

---

- [ ] **Step 1: Implement version 3 preamble** -- `GateCircuit::encode()` emits version = 3
  when `n_cregs > 0`. The preamble layout after the 15-byte header: optional param block
  (if `n_params > 0`), then `n_cregs: u8`. `GateCircuit::decode()` branches on version:
  1 → stream only; 2 → param preamble + stream; 3 → param preamble + `n_cregs` byte + stream.

- [ ] **Step 2: Add 9 entries to `xqgbc/src/table.rs`** in a new `0xE0–0xE8` group.
  `u8` operands already have `EncodeOperand`/`DecodeOperand`; no new types needed.

- [ ] **Step 3: Extend `xqgbc/src/verifier.rs`**
  - `CSET`/`CMBIT`/`CINC`/`CDEC`/`CADD`/`CSUB`: check all `reg` operands `< n_cregs`
  - `CJEQ`/`CJLT`/`CJGT`: check reg operands + check `id` is in `declared_labels`

- [ ] **Step 4: Extend `xqgb/cpu_sim.py` `simulate()` loop**

```python
cregs = [0] * 8   # u16 classical registers

# In dispatch:
if m == "CSET":  cregs[ops[0]] = ops[1]; pos += 1; continue
if m == "CADD":  cregs[ops[0]] = (cregs[ops[0]] + cregs[ops[1]]) & 0xFFFF; pos += 1; continue
if m == "CSUB":  cregs[ops[0]] = (cregs[ops[0]] - cregs[ops[1]]) & 0xFFFF; pos += 1; continue
if m == "CINC":  cregs[ops[0]] = (cregs[ops[0]] + 1) & 0xFFFF; pos += 1; continue
if m == "CDEC":  cregs[ops[0]] = (cregs[ops[0]] - 1) & 0xFFFF; pos += 1; continue
if m == "CMBIT": cregs[ops[0]] = cbits[ops[1]]; pos += 1; continue
if m == "CJEQ":  pos = _find_label(instructions, ops[2]) if cregs[ops[0]] == cregs[ops[1]] else pos + 1; continue
if m == "CJLT":  pos = _find_label(instructions, ops[2]) if cregs[ops[0]] <  cregs[ops[1]] else pos + 1; continue
if m == "CJGT":  pos = _find_label(instructions, ops[2]) if cregs[ops[0]] >  cregs[ops[1]] else pos + 1; continue
```

- [ ] **Step 5: Write tests** -- a syndrome accumulation circuit: measure 3 qubits into
  cbits, use `CMBIT` to accumulate into `CR0`, branch on `CJEQ CR0 CR1` to apply a
  correction gate. Verify CPU sim produces the expected counts.

- [ ] **Step 6: Run full suite**

```bash
cargo test -p xqgbc && uv run pytest xqgb/ -v
uv run python scripts/check-gate-opcode-parity.py
# Expected: OK -- 44 gate opcodes match
```

- [ ] **Step 7: Commit**

```bash
git commit -m "feat(xqgbc,xqgb): classical integer register file (CSET/CADD/CINC/CMBIT/CJEQ…)"
```

---

## W6 · Part 1 -- Classical Control Flow: `if`/`else` via Flat Jumps

**Background:** OpenQASM 3 adds classical control flow that conditions gate application on
`bit` register values (measurement results). The only construct requiring genuine runtime
hardware support is `if`/`else` -- `for` and `while` are always unrolled by the compiler
before encoding, and `break`/`continue` resolve to unconditional jumps within the
unrolled body.

**Design:** Four opcodes in the gap `0x84–0x87`, mirroring the `TARGET`/`JUMP1`/`JUMPI1`
pattern from the QUBO ISA. `CF_TARGET` is a passive label marker -- the verifier collects
all declared IDs and checks that every jump references one, exactly as `TARGET` does in
`xqvm`. The executor and transpiler advance past `CF_TARGET` without action.

```
0x84  CF_TARGET  { id: u8 }              -- passive jump-destination label
0x85  CF_JUMP    { id: u8 }              -- unconditional jump to label
0x86  CF_JZ      { c: Cbit, id: u8 }    -- jump to label if cbit c == 0
0x87  CF_JNZ     { c: Cbit, id: u8 }    -- jump to label if cbit c == 1
```

How OpenQASM 3 constructs lower to these (compiler responsibility):

```
if (c[0] == 1) { X q[1]; }
→  CF_JZ  c=0 id=1
   X q=1
   CF_TARGET id=1

if (c[0] == 1) { X q[1]; } else { H q[1]; }
→  CF_JZ  c=0 id=1
   X q=1
   CF_JUMP id=2
   CF_TARGET id=1
   H q=1
   CF_TARGET id=2

for i in [0:N] { G; }   →  G · G · … (N copies; unrolled before encoding)
while (c) { … }         →  compile error; no hardware runtime support
break                   →  CF_JUMP <loop_exit_label>  (resolved by compiler)
continue                →  CF_JUMP <loop_top_label>   (resolved by compiler)
```

**Files:**
- Modify: `conformance/gate_opcodes.yaml` -- add 4 CF entries
- Modify: `xqgbc/src/table.rs` -- add 4 entries to `gate_opcodes!`
- Modify: `xqgbc/src/types/opcode.rs` -- add 4 variants to `GateOpcode`
- Modify: `xqgbc/src/types/instruction.rs` -- add 4 variants to `GateInstruction`
- Modify: `xqgbc/src/codec.rs` -- extend `encode`/`decode` match arms
- Modify: `xqgbc/src/verifier.rs` -- add label-consistency pass (mirrors XQVM)
- Modify: `xqgb/decoder.py` -- add 4 entries to `_OPCODE_TABLE`
- Modify: `xqgb/cpu_sim.py` -- index-based loop + cbit register + forward label scan
- Modify: `xqgb/transpiler.py` -- forward-scan body for `QiskitTranspiler`; raise for IonQ

**Interfaces:**
- Consumes: all of W1–W4
- Produces: `CF_TARGET`/`CF_JUMP`/`CF_JZ`/`CF_JNZ` in Rust codec and Python decoder;
  verifier label-consistency check; CPU sim branches on Cbit values; Qiskit transpiler
  collects body by scanning forward to the next CF_TARGET

---

- [ ] **Step 1: Update `conformance/gate_opcodes.yaml`**

Append after the `CRX` entry. Note the new operand type `label` (width 1, encodes as
`u8`); add it to `check-gate-opcode-parity.py`'s `_PY_FORMAT_WIDTHS` and
`_RUST_TYPE_WIDTH` maps as an alias for a raw `u8`:

```yaml
  # -------------------------------------------------------------------------
  # Classical control flow  (0x84–0x87; gap between circuit-control and H)
  # Mirrors TARGET/JUMP1/JUMPI1 from the QUBO ISA.
  # -------------------------------------------------------------------------
  - code: 0x84
    mnemonic: CF_TARGET
    operands:
      - {name: id, type: label, width: 1}

  - code: 0x85
    mnemonic: CF_JUMP
    operands:
      - {name: id, type: label, width: 1}

  - code: 0x86
    mnemonic: CF_JZ
    operands:
      - {name: c,  type: cbit,  width: 1}
      - {name: id, type: label, width: 1}

  - code: 0x87
    mnemonic: CF_JNZ
    operands:
      - {name: c,  type: cbit,  width: 1}
      - {name: id, type: label, width: 1}
```

---

- [ ] **Step 2: Write failing tests in `xqgbc/tests/roundtrip.rs`** (append)

```
/* Detailed source-code truncated for AI context efficiency. */
```

Run: `cargo test -p xqgbc` → expected: compile error (variants not yet defined).

---

- [ ] **Step 3: Add 4 entries to `xqgbc/src/table.rs`**

Inside `gate_opcodes!`, after the `Meas` entry (`0x83`) and before `H` (`0x90`):

```rust
            // --- Classical control flow (mirrors QUBO ISA TARGET/JUMP/JUMPI) ---
            /// Passive jump-destination label; no runtime effect.
            #[gate(0x84, "CF_TARGET", 0)]
            CfTarget { id: u8 },

            /// Unconditional jump to label.
            #[gate(0x85, "CF_JUMP", 0)]
            CfJump { id: u8 },

            /// Jump to label if classical bit c is 0.
            #[gate(0x86, "CF_JZ", 0)]
            CfJz { c: $crate::Cbit, id: u8 },

            /// Jump to label if classical bit c is 1.
            #[gate(0x87, "CF_JNZ", 0)]
            CfJnz { c: $crate::Cbit, id: u8 },
```

`qubit_arity` is `0` for all four -- `check_qubits()` already ignores them via the `_ =>
&[]` fallthrough.

---

- [ ] **Step 4: Extend `xqgbc/src/types/opcode.rs`**

Add to the `GateOpcode` enum:

```rust
    CfTarget = 0x84,
    CfJump   = 0x85,
    CfJz     = 0x86,
    CfJnz    = 0x87,
```

Add to `TryFrom<u8>`:

```rust
    0x84 => Ok(Self::CfTarget),
    0x85 => Ok(Self::CfJump),
    0x86 => Ok(Self::CfJz),
    0x87 => Ok(Self::CfJnz),
```

---

- [ ] **Step 5: Extend `xqgbc/src/types/instruction.rs`**

Add variants to `GateInstruction`:

```rust
    CfTarget { id: u8 },
    CfJump   { id: u8 },
    CfJz     { c: Cbit, id: u8 },
    CfJnz    { c: Cbit, id: u8 },
```

Extend `opcode()`:

```rust
    Self::CfTarget { .. } => GateOpcode::CfTarget,
    Self::CfJump   { .. } => GateOpcode::CfJump,
    Self::CfJz     { .. } => GateOpcode::CfJz,
    Self::CfJnz    { .. } => GateOpcode::CfJnz,
```

Extend `mnemonic()`:

```rust
    GateOpcode::CfTarget => "CF_TARGET",
    GateOpcode::CfJump   => "CF_JUMP",
    GateOpcode::CfJz     => "CF_JZ",
    GateOpcode::CfJnz    => "CF_JNZ",
```

---

- [ ] **Step 6: Extend `xqgbc/src/codec.rs`**

In `encode()`:

```rust
GateInstruction::CfTarget { id }    => { id.encode_into(&mut buf); }
GateInstruction::CfJump   { id }    => { id.encode_into(&mut buf); }
GateInstruction::CfJz  { c, id }    => { c.encode_into(&mut buf); id.encode_into(&mut buf); }
GateInstruction::CfJnz { c, id }    => { c.encode_into(&mut buf); id.encode_into(&mut buf); }
```

In `decode()`:

```rust
GateOpcode::CfTarget => GateInstruction::CfTarget { id: read!(u8) },
GateOpcode::CfJump   => GateInstruction::CfJump   { id: read!(u8) },
GateOpcode::CfJz     => GateInstruction::CfJz  { c: read!(Cbit), id: read!(u8) },
GateOpcode::CfJnz    => GateInstruction::CfJnz { c: read!(Cbit), id: read!(u8) },
```

`u8` already implements `EncodeOperand`/`DecodeOperand`.

---

- [ ] **Step 7: Extend `xqgbc/src/verifier.rs`** -- label-consistency pass

Add a second pass after the existing qubit/cbit bounds check. Structure mirrors XQVM's
verifier: collect declared labels first, then verify every jump references one.

```rust
// Collect all declared CF_TARGET label IDs.
let declared_labels: std::collections::HashSet<u8> = instrs
    .iter()
    .filter_map(|i| {
        if let GateInstruction::CfTarget { id } = i { Some(*id) } else { None }
    })
    .collect();

// Every jump must reference a declared CF_TARGET.
for instr in instrs {
    let referenced = match instr {
        GateInstruction::CfJump { id }
        | GateInstruction::CfJz  { id, .. }
        | GateInstruction::CfJnz { id, .. } => Some(*id),
        _ => None,
    };
    if let Some(id) = referenced {
        if !declared_labels.contains(&id) {
            return Err(GateVerifyError::UndeclaredLabel { id });
        }
    }
}
```

Add the new error variant to `GateVerifyError`:

```rust
#[error("jump references undeclared CF_TARGET label id {id}")]
UndeclaredLabel { id: u8 },
```

---

- [ ] **Step 8: Run Rust tests**

```bash
cargo test -p xqgbc
```

Expected: all prior tests pass plus the 6 new CF roundtrip and verifier tests.

---

- [ ] **Step 9: Update `xqgb/decoder.py` `_OPCODE_TABLE`**

Add a `'l'` format character for label/`u8` operands (distinct from `'q'` and `'c'`
semantically; decoded identically as a raw `u8`):

```python
    0x84: ("CF_TARGET", "l"),
    0x85: ("CF_JUMP",   "l"),
    0x86: ("CF_JZ",     "cl"),
    0x87: ("CF_JNZ",    "cl"),
```

Add `'l'` to `_decode_operands`:

```python
        elif char == "l":
            if pos + consumed >= len(stream):
                raise ValueError(
                    f"Truncated label operand for opcode 0x{opcode:02X} "
                    f"at offset {pos + consumed}"
                )
            values.append(stream[pos + consumed])
            consumed += 1
```

Add `'l'` to `_PY_FORMAT_WIDTHS` in `check-gate-opcode-parity.py`:

```python
_PY_FORMAT_WIDTHS = {
    "q": ("qubit", 1), "c": ("cbit", 1), "a": ("angle", 4), "l": ("label", 1)
}
```

Also add `"label"` to `_RUST_TYPE_WIDTH` and `_RUST_TYPE_NAME` (mapped from `u8`):

```python
_RUST_TYPE_WIDTH = {"Qubit": 1, "Cbit": 1, "GateAngle": 4, "u8": 1}
_RUST_TYPE_NAME  = {"Qubit": "qubit", "Cbit": "cbit", "GateAngle": "angle", "u8": "label"}
```

---

- [ ] **Step 10: Update `xqgb/cpu_sim.py`**

Replace the `for instr in instructions` loop in `simulate()` with an index-based loop
that maintains a `cbits` array and branches on label lookup. `CF_TARGET` is a no-op
(executor advances past it, same as XQVM's `TARGET`).

```python
def simulate(self, n_qubits, n_cbits, n_shots, instructions):
    sv = np.zeros(2**n_qubits, dtype=np.complex128)
    sv[0] = 1.0
    cbits = [0] * n_cbits   # updated by MEAS during execution
    meas_map: list[tuple[int, int]] = []

    pos = 0
    while pos < len(instructions):
        instr = instructions[pos]
        m = instr.mnemonic
        ops = instr.operands

        if m in ("QNOP", "BARRIER", "QHALT", "CF_TARGET"):
            pos += 1
            continue
        if m == "MEAS":
            meas_map.append((ops[0], ops[1]))
            pos += 1
            continue
        if m == "CF_JUMP":
            pos = _find_label(instructions, ops[0])
            continue
        if m == "CF_JZ":
            pos = _find_label(instructions, ops[1]) if cbits[ops[0]] == 0 else pos + 1
            continue
        if m == "CF_JNZ":
            pos = _find_label(instructions, ops[1]) if cbits[ops[0]] != 0 else pos + 1
            continue

        sv = self._apply(sv, n_qubits, instr)
        pos += 1

    probs = (sv * sv.conj()).real
    probs = np.maximum(probs, 0.0)
    probs /= probs.sum()
    sampled = self._rng.choice(2**n_qubits, size=n_shots, p=probs)
    return self._build_histogram(sampled, meas_map, n_cbits)
```

Add the label-lookup helper (O(n) scan; acceptable since CPU sim is PoC-only):

```python
def _find_label(instructions: list, label_id: int) -> int:
    """Return the index of CF_TARGET with the given id."""
    for i, instr in enumerate(instructions):
        if instr.mnemonic == "CF_TARGET" and instr.operands[0] == label_id:
            return i
    raise ValueError(f"CF_TARGET id={label_id} not found -- verifier should have caught this")
```

---

- [ ] **Step 11: Update `xqgb/transpiler.py`**

In `QiskitTranspiler.transpile()`, handle CF opcodes before the generic `_QISKIT_MAP`
lookup. `CF_TARGET` is skipped (no Qiskit equivalent needed). For `CF_JZ`/`CF_JNZ`,
collect the body by scanning forward to the matching `CF_TARGET`, then emit using
`if_test`:

```python
            if instr.mnemonic == "CF_TARGET":
                continue
            if instr.mnemonic == "CF_JUMP":
                # unconditional jump: only legal as else-skip in compiled if/else;
                # the surrounding if_test context handles this implicitly -- skip.
                continue
            if instr.mnemonic in ("CF_JZ", "CF_JNZ"):
                c_idx, label_id = instr.operands[0], instr.operands[1]
                # Collect body: instructions between here and CF_TARGET label_id.
                body, else_body = _collect_cf_body(circuit.instructions, idx, label_id)
                creg = qc.cregs[0]
                condition_val = 0 if instr.mnemonic == "CF_JZ" else 1
                with qc.if_test((creg[c_idx], condition_val)) as else_ctx:
                    for body_instr in body:
                        _emit_single(qc, body_instr)
                    if else_body:
                        with else_ctx:
                            for body_instr in else_body:
                                _emit_single(qc, body_instr)
                # Advance idx past the whole if/else block.
                idx = _find_cf_target(circuit.instructions, label_id)
                continue
```

Add helpers `_collect_cf_body` and `_find_cf_target` as module-level functions. Their
implementation is straightforward forward scans; they are not shown here to keep the plan
concise -- implement them alongside the transpiler update.

In `IonQTranspiler.transpile()`, raise on any CF opcode -- IonQ requires full unrolling:

```python
            if instr.mnemonic.startswith("CF_"):
                raise ValueError(
                    f"IonQ does not support runtime classical control. "
                    f"Unroll {instr.mnemonic!r} before encoding to .xqg."
                )
```

---

- [ ] **Step 12: Write Python tests** in `xqgb/tests/test_cf.py`

```
/* Detailed source-code truncated for AI context efficiency. */
```

Run: `uv run pytest xqgb/tests/test_cf.py -v`

Expected: all 4 tests pass.

---

- [ ] **Step 13: Run the full test suite and parity check**

```bash
cargo test -p xqgbc
uv run pytest xqgb/ -v
uv run ruff check xqgb/
cargo clippy -p xqgbc -- -D warnings
uv run python scripts/check-gate-opcode-parity.py
```

Expected: 0 failures, 0 warnings, parity check prints `OK -- 29 gate opcodes match`.

---

- [ ] **Step 14: Commit**

```bash
git add \
  conformance/gate_opcodes.yaml \
  xqgbc/src/table.rs \
  xqgbc/src/types/opcode.rs \
  xqgbc/src/types/instruction.rs \
  xqgbc/src/codec.rs \
  xqgbc/src/verifier.rs \
  xqgb/decoder.py \
  xqgb/cpu_sim.py \
  xqgb/transpiler.py \
  xqgb/tests/test_cf.py \
  scripts/check-gate-opcode-parity.py
git commit -m "feat(xqgbc,xqgb): classical control-flow opcodes CF_TARGET/JUMP/JZ/JNZ"
```

---

### Design Plans 2026 07 13 Qui 167 Scale Sweep (`docs/design/plans/2026-07-13-qui-167-scale-sweep.md`)

# QUI-167 Scale Sweep Harness Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build `scripts/scale_sweep.py`, a two-mode harness that runs the shipped MaxCut/TSP example encodings at growing sizes across VM backends and SA solvers, recording per-stage timing, memory, correctness, and parity — then run it and report findings to QUI-167.

**Architecture:** One self-contained script. Orchestrator mode builds a pruned (problem × backend × solver × n × seed) matrix and spawns itself in point mode as a child process per point (clean peak-RSS, crash isolation). Point mode re-runs the example's exact compile → encode → solve → verify → decode sequence with per-stage timers. Pure analysis/report helpers get a small pytest file.

**Tech Stack:** Python 3.13 (uv-managed venv), `xquad` workspace packages (`xquad.cp/.sa/.vm/.types`), `importlib` loading of `examples/*/runner.py`, `subprocess`, `resource`. No new dependencies.

**Spec:** `docs/design/specs/2026-07-13-qui-167-scale-sweep-design.md` (approved 2026-07-13).

## Global Constraints

- Branch `feature/qui-167` (worktree at `.claude/worktrees/feature+qui-167`); never touch `release/v0.3.0`.
- Run everything with `uv run --no-sync` (plain `uv run` reverts the maturin-built `xqffi`).
- Solver names are exactly `dwave-cpu` and `cuda-gpu` (keys of `xquad.sa.SOLVERS`).
- CPU lanes: both VM backends. GPU lanes: Rust VM only.
- Ladders — CPU: MaxCut `8,16,32,64,128,256,512`, TSP `4,6,8,12,16,24,32,48`. GPU adds MaxCut `1024`, TSP `64`.
- Seeds `42,43,44`; per-point timeout default 300 s; a lane stops escalating at the first non-ok point.
- The orchestrator never aborts the sweep on a lane failure; every outcome becomes a JSONL record.
- ≤100 lines/function, ≤5 positional params, 100-char lines, absolute imports, ruff clean (`make lint-python` equivalent: `uv run --no-sync ruff check scripts/`).
- Commit after each task (imperative, ≤72-char subject, no co-author bylines).

## File Structure

- Create `scripts/scale_sweep.py` — the harness (adapters, point mode, orchestrator, analysis, report, CLI).
- Create `scripts/tests/test_scale_sweep.py` — pytest for the pure helpers (matrix building, structural checks, analysis, report rendering). Measurement plumbing (subprocess, GPU) is verified by running the harness, not mocked.

---

### Task 1: Adapter registry and example-module loader

**Files:**
- Create: `scripts/scale_sweep.py`
- Create: `scripts/tests/test_scale_sweep.py`

**Interfaces:**
- Produces: `load_runner(example: str) -> ModuleType`; `Adapter` frozen dataclass with fields `name, ladder, gpu_ladder, runner_module, build_calldata, structurally_valid, objective, canonicalize, model_vars, approx_quad_terms`; `make_adapters() -> dict[str, Adapter]` with keys `"maxcut"`, `"tsp"`.

- [ ] **Step 1: Write the failing test**

Create `scripts/tests/test_scale_sweep.py`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run test to verify it fails**

Run: `uv run --no-sync pytest scripts/tests/test_scale_sweep.py -v`
Expected: FAIL with `ModuleNotFoundError: No module named 'scale_sweep'`

- [ ] **Step 3: Write the implementation**

Create `scripts/scale_sweep.py`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Note: `maxcut_objective` re-implements the two-line `cut_weight` instead of
loading the runner module (an import of the full xquad stack) inside a hot
helper; `tsp_objective` must delegate because `tour_distance` depends on
`triu` indexing. Both are exercised against runner goldens in Task 2's
verification.

- [ ] **Step 4: Run tests to verify they pass**

Run: `uv run --no-sync pytest scripts/tests/test_scale_sweep.py -v`
Expected: 4 passed

- [ ] **Step 5: Ruff check and commit**

```bash
uv run --no-sync ruff check scripts/scale_sweep.py scripts/tests/test_scale_sweep.py
git add scripts/scale_sweep.py scripts/tests/test_scale_sweep.py
git commit -m "feat(scripts): add scale sweep adapter registry (QUI-167)"
```

---

### Task 2: Point mode — measure one (problem, backend, solver, n, seed)

**Files:**
- Modify: `scripts/scale_sweep.py` (append; also extend imports)

**Interfaces:**
- Consumes: `Adapter`, `make_adapters()`, `load_runner()` from Task 1.
- Produces: `measure_point(adapter: Adapter, backend_name: str, solver_name: str, n: int, seed: int) -> dict` returning a record with keys `schema, problem, backend, solver, n, seed, status` and, when `status == "ok"`: `stages` (dict of 5 stage-seconds), `model` (`{"vars": int, "approx_quad_terms": int}`), `energy: int`, `valid: int`, `structural_ok: bool`, `objective: int`, `solution: list[int]`, `peak_rss_kib: int`. When a stage raises: `status="error"`, `stage`, `message`. Also `main(argv) -> int` handling `--point` CLI.

- [ ] **Step 1: Write the implementation**

Append to `scripts/scale_sweep.py` (add `import argparse, json, resource, sys, time` to the imports block):

```
/* Detailed source-code truncated for AI context efficiency. */
```

Add a temporary stub so point mode is runnable before Task 3 (replaced there):

```python
def _orchestrate(args: argparse.Namespace) -> int:
    raise NotImplementedError("orchestrator lands in Task 3")
```

- [ ] **Step 2: Verify point mode against the runner goldens**

```bash
uv run --no-sync python scripts/scale_sweep.py --point --problem maxcut --backend rust --n 8 --seed 42 | python3 -m json.tool
uv run --no-sync python examples/maxcut/runner.py --n 8 --seed 42 --interpreter rust
```
Expected: point record has `status: "ok"`, `valid: 1`, `structural_ok: true`, five positive stage timings, `peak_rss_kib > 0`, and its `energy`/`objective`/`solution` equal the runner's `energy`/`cut_weight`/`partition` for the same n and seed. Repeat for TSP:

```bash
uv run --no-sync python scripts/scale_sweep.py --point --problem tsp --backend python --n 4 --seed 42 | python3 -m json.tool
uv run --no-sync python examples/tsp/runner.py --n 4 --seed 42 --interpreter python
```
Expected: `tour`/`tour_distance` match `solution`/`objective`.

- [ ] **Step 3: Verify the error path**

```bash
uv run --no-sync python scripts/scale_sweep.py --point --problem maxcut --backend rust --solver dwave-qpu --n 8 --seed 42; echo "exit=$?"
```
Expected: JSON with `status: "error"`, `stage: "solve"`, non-empty `message`; `exit=1`.

- [ ] **Step 4: Ruff check and commit**

```bash
uv run --no-sync ruff check scripts/scale_sweep.py
git add scripts/scale_sweep.py
git commit -m "feat(scripts): add scale sweep point measurement mode (QUI-167)"
```

---

### Task 3: Orchestrator — matrix, child processes, lane escalation

**Files:**
- Modify: `scripts/scale_sweep.py` (replace the `_orchestrate` stub; extend imports with `import os, subprocess`)

**Interfaces:**
- Consumes: `make_adapters()`, `build_parser()`, point-mode CLI from Task 2; `CPU_SOLVER`, `GPU_SOLVER`, `SEEDS` from Task 1.
- Produces: `cuda_available() -> bool`; `build_matrix(adapters: dict[str, Adapter], solvers: list[str], smoke: bool) -> list[Lane]` where `Lane` is a dataclass `(problem: str, backend: str, solver: str, ladder: tuple[int, ...])`; `run_point_child(lane: Lane, n: int, seed: int, timeout: float) -> dict`; `_orchestrate(args) -> int` writing JSONL to `args.out` and a report via Task 4's `render_report(records) -> str`.

- [ ] **Step 1: Write the failing test**

Append to `scripts/tests/test_scale_sweep.py`:

```python
def test_build_matrix_cpu_only():
    adapters = scale_sweep.make_adapters()
    lanes = scale_sweep.build_matrix(adapters, solvers=["dwave-cpu"], smoke=False)
    keys = {(lane.problem, lane.backend, lane.solver) for lane in lanes}
    assert keys == {
        ("maxcut", "python", "dwave-cpu"),
        ("maxcut", "rust", "dwave-cpu"),
        ("tsp", "python", "dwave-cpu"),
        ("tsp", "rust", "dwave-cpu"),
    }
    assert all(lane.ladder == adapters[lane.problem].ladder for lane in lanes)


def test_build_matrix_gpu_lanes_rust_only_with_extended_ladder():
    adapters = scale_sweep.make_adapters()
    lanes = scale_sweep.build_matrix(adapters, solvers=["dwave-cpu", "cuda-gpu"], smoke=False)
    gpu = [lane for lane in lanes if lane.solver == "cuda-gpu"]
    assert {(lane.problem, lane.backend) for lane in gpu} == {("maxcut", "rust"), ("tsp", "rust")}
    assert all(lane.ladder == adapters[lane.problem].gpu_ladder for lane in gpu)


def test_build_matrix_smoke_truncates_ladders():
    adapters = scale_sweep.make_adapters()
    lanes = scale_sweep.build_matrix(adapters, solvers=["dwave-cpu"], smoke=True)
    assert all(len(lane.ladder) == 1 for lane in lanes)
    assert all(lane.ladder[0] == adapters[lane.problem].ladder[0] for lane in lanes)
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run --no-sync pytest scripts/tests/test_scale_sweep.py -v -k matrix`
Expected: 3 FAIL with `AttributeError: module 'scale_sweep' has no attribute 'build_matrix'`

- [ ] **Step 3: Write the implementation**

Replace the Task 2 `_orchestrate` stub with:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Task 4 defines `render_report`; until then add this stub right above `_orchestrate` (replaced in Task 4):

```python
def render_report(records: list[dict]) -> str:
    return f"# Scale sweep\n\n{len(records)} records (report lands in Task 4)\n"
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `uv run --no-sync pytest scripts/tests/test_scale_sweep.py -v`
Expected: 7 passed

- [ ] **Step 5: Verify end-to-end with the smoke sweep**

Run: `uv run --no-sync python scripts/scale_sweep.py --smoke --out scratch/scale_sweep/smoke.jsonl`
Expected: 6 lanes print (4 CPU + 2 GPU on this machine), every point `ok`; `scratch/scale_sweep/smoke.jsonl` holds 6 records with `status: "ok"`. Spot-check one:
`python3 -c "import json; rs=[json.loads(l) for l in open('scratch/scale_sweep/smoke.jsonl')]; print(len(rs), all(r['status']=='ok' for r in rs))"`

- [ ] **Step 6: Ruff check and commit**

```bash
uv run --no-sync ruff check scripts/
git add scripts/scale_sweep.py scripts/tests/test_scale_sweep.py
git commit -m "feat(scripts): add scale sweep orchestrator with lane escalation (QUI-167)"
```

---

### Task 4: Analysis and markdown report

**Files:**
- Modify: `scripts/scale_sweep.py` (replace `render_report` stub; add helpers)
- Modify: `scripts/tests/test_scale_sweep.py` (append tests)

**Interfaces:**
- Consumes: record dicts as produced by Tasks 2–3 (`status`, `stages`, `energy`, `solution`, `peak_rss_kib`, ...).
- Produces: `lane_ceilings(records) -> dict[tuple[str, str, str], int]` (max ok n per lane); `parity_failures(records) -> list[dict]` (CPU-solver python-vs-rust solution mismatches, keyed by problem/n/seed); `solver_deltas(records) -> list[dict]` (rust-VM cuda-vs-cpu energy deltas per problem/n/seed); `render_report(records: list[dict]) -> str` (markdown).

- [ ] **Step 1: Write the failing tests**

Append to `scripts/tests/test_scale_sweep.py`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run --no-sync pytest scripts/tests/test_scale_sweep.py -v -k "ceilings or parity or deltas or report"`
Expected: 4 FAIL with `AttributeError` (`lane_ceilings` undefined)

- [ ] **Step 3: Write the implementation**

Replace the `render_report` stub with:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Run all tests**

Run: `uv run --no-sync pytest scripts/tests/test_scale_sweep.py -v`
Expected: 11 passed

- [ ] **Step 5: Re-run the smoke sweep and read the real report**

Run: `uv run --no-sync python scripts/scale_sweep.py --smoke --out scratch/scale_sweep/smoke.jsonl`
Expected: `scratch/scale_sweep/report.md` renders all sections; parity section says OK; energy-delta table appears (GPU lane present on this machine).

- [ ] **Step 6: Ruff check and commit**

```bash
uv run --no-sync ruff check scripts/
git add scripts/scale_sweep.py scripts/tests/test_scale_sweep.py
git commit -m "feat(scripts): add scale sweep analysis and markdown report (QUI-167)"
```

---

### Task 5: Full sweep run and findings

**Files:**
- Create: `scratch/scale_sweep/results.jsonl` (run output; scratch/ is not committed)
- Create: `docs/design/specs/2026-07-13-qui-167-findings.md` (committed findings summary)

**Interfaces:**
- Consumes: the complete harness from Tasks 1–4.

- [ ] **Step 1: Sanity-check runtime bounds, then launch the full sweep**

```bash
uv run --no-sync python scripts/scale_sweep.py --timeout 300 --out scratch/scale_sweep/results.jsonl
```
Run in the background (it may take an hour+ if CPU lanes climb high). Expected: 6 lanes, each climbing until timeout/failure/ladder-end; JSONL grows incrementally, so progress is checkable mid-run.

- [ ] **Step 2: Review the report for anomalies**

Read `scratch/scale_sweep/report.md`. Check specifically: (a) parity section OK — any mismatch is a QUI-465-class bug and blocks findings; (b) verifier-invalid count 0 at small n (invalid TSP solutions at large n are expected annealer behavior, worth noting, not a harness bug); (c) ceilings are timeout-shaped, not crash-shaped — crashes need a follow-up ticket.

- [ ] **Step 3: Write the findings doc**

Create `docs/design/specs/2026-07-13-qui-167-findings.md` containing: the lane-ceilings table, the largest-common-rung stage breakdown, the parity verdict, the GPU-vs-CPU energy/throughput comparison, and a short "implications for v0.3.1+" section (candidate follow-ups: bit-packing QUI-704 relevance, encoder hot spots). Copy tables from the generated report; do not hand-edit numbers.

- [ ] **Step 4: Commit**

```bash
git add docs/design/specs/2026-07-13-qui-167-findings.md
git commit -m "docs: record QUI-167 scale sweep findings"
```

- [ ] **Step 5: Close the loop on Linear**

Post a comment on QUI-167 with the findings summary (ceilings table + parity verdict + GPU delta headline), tick the ticket's MaxCut and TSP checkboxes, and move the issue to In Review after the MR is opened (finishing-a-development-branch skill handles the MR).

---

## Self-Review

- **Spec coverage:** adapters/registry (Task 1), five instrumented stages + structural/objective/valid checks + peak RSS (Task 2), matrix pruning + child isolation + timeout/crash/error records + lane stop + JSONL (Task 3), parity + cross-solver deltas + ceilings + markdown report (Task 4), smoke flag (Tasks 3/5 via `--smoke`), findings + Linear closeout (Task 5). GPU env setup already done in-session and documented in the spec.
- **Placeholder scan:** the two intentional stubs (`_orchestrate` in Task 2, `render_report` in Task 3) are each replaced by a named later task — acceptable forward references, both explicit.
- **Type consistency:** record schema keys used by Task 4 match Task 2/3 producers (`stages`, `solution`, `energy`, `peak_rss_kib`, `status`, `stderr_tail`, `timeout_s`); `Lane` fields match `build_matrix` tests; `STAGES` tuple shared.

---

### Design Plans 2026 07 13 Qui 687 Gpu Validation (`docs/design/plans/2026-07-13-qui-687-gpu-validation.md`)

# QUI-687 GPU Medium-Model Validation Tests Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add `xqsa/tests/test_gpu_validation.py` — hardware-gated medium-model (20–100 var) validation for `SolverCudaGPU` and `SolverMetalGPU` with exact-optimum and CPU cross-check assertions.

**Architecture:** One new test module. Four module-level model builders return `(model, ground_energy)`. An ungated reference class proves `SolverDWaveCPU` hits each known optimum on every CI run. Two gated classes (cuda / metal markers, same skip-guard idiom as `test_xqsa.py`) assert the GPU solvers hit the exact optima on structured models and land within 2% of the CPU reference on a seeded spin glass.

**Tech Stack:** pytest, `xqvm_py.xqmx.XQMX`, `xqsa` solvers. No new dependencies.

**Spec:** `docs/design/specs/2026-07-13-qui-687-gpu-validation-design.md` (approved 2026-07-13).

## Global Constraints

- Work ONLY in a dedicated worktree on branch `feature/qui-687` (base 15918ed post-v0.3.0 main).
- Run Python via `uv run --no-sync ...` (plain `uv run` reverts the maturin-built xqffi).
- Development machine assumption: a CUDA GPU with cupy installed — cuda-marked tests run for real; no Metal device — metal-marked tests must be observed to skip.
- Guard idiom copied from `xqsa/tests/test_xqsa.py`: `_IN_CI = os.environ.get("CI") is not None`; probe `_has_cupy`/`_has_metal`; import solver under `if _has_X or _IN_CI:`; class decorated `@pytest.mark.cuda|metal` + `@pytest.mark.skipif(not _IN_CI and not _has_X, reason=...)`.
- Assertion policy: structured models exact on both solvers; spin glass `gpu <= cpu + 0.02 * abs(cpu)`.
- GPU calls pin `num_reads=64, num_sweeps=2000, seed=42` (never rely on defaults).
- Style: ≤100 lines/function, ≤5 positional params, 100-char lines (repo ruff allows 120 — the 100 limit still binds; check with `awk 'length > 100'`), ruff clean, full 16-line AGPL header + SPDX.
- Commits: imperative, ≤72-char subject, no co-author bylines/trailers.

## File Structure

- Create `xqsa/tests/test_gpu_validation.py` — builders, `TestMediumModelReferences` (ungated), `TestCudaGPUMediumModels` (cuda), `TestMetalGPUMediumModels` (metal). Nothing else changes.

---

### Task 1: Model builders + ungated CPU reference tests

**Files:**
- Create: `xqsa/tests/test_gpu_validation.py`

**Interfaces:**
- Produces (used verbatim by Tasks 2–3): `ferro_chain(n: int = 64) -> tuple[XQMX, int]`, `frustrated_triangles(n: int = 51) -> tuple[XQMX, int]`, `bipartite_maxcut(n: int = 20) -> tuple[XQMX, int]`, `random_spin_glass(n: int = 100, seed: int = 7) -> tuple[XQMX, None]`; module constants `_IN_CI`, `_has_cupy`, `_has_metal`; `GPU_PARAMS = {"num_reads": 64, "num_sweeps": 2000, "seed": 42}`; `cpu_reference(model) -> int` returning `SolverDWaveCPU(seed=42).solve(model).energy`.

- [ ] **Step 1: Create the file with header, guards, builders left unimplemented, and the reference tests**

Create `xqsa/tests/test_gpu_validation.py` with the full 16-line AGPL header exactly as at the top of `xqsa/tests/test_xqsa.py` (Copyright 2026 Postquant Labs Incorporated … `SPDX-License-Identifier: AGPL-3.0-or-later`), then:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Then the four builders as stubs raising `NotImplementedError` (implemented in Step 3), followed by the reference tests:

```python
def cpu_reference(model: XQMX) -> int:
    """Best energy SolverDWaveCPU finds on ``model`` (fixed seed)."""
    return SolverDWaveCPU(seed=42).solve(model).energy


class TestMediumModelReferences:
    """Ungated: builders and reference energies hold on every CI run."""

    def test_ferro_chain_cpu_hits_ground_state(self) -> None:
        model, ground = ferro_chain()
        assert ground == -63
        assert cpu_reference(model) == ground

    def test_frustrated_triangles_cpu_hits_ground_state(self) -> None:
        model, ground = frustrated_triangles()
        assert ground == -17
        assert cpu_reference(model) == ground

    def test_bipartite_maxcut_cpu_hits_ground_state(self) -> None:
        model, ground = bipartite_maxcut()
        assert ground == -100
        assert cpu_reference(model) == ground

    def test_random_spin_glass_is_deterministic(self) -> None:
        model_a, ground_a = random_spin_glass()
        model_b, _ = random_spin_glass()
        assert ground_a is None
        assert model_a.size == model_b.size == 100
        assert cpu_reference(model_a) == cpu_reference(model_b)
```

- [ ] **Step 2: Run to verify the reference tests fail on the stubs**

Run: `uv run --no-sync pytest xqsa/tests/test_gpu_validation.py -q -k References`
Expected: 4 FAIL with `NotImplementedError`

- [ ] **Step 3: Implement the builders**

Replace the stubs with:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Run to verify the reference tests pass**

Run: `uv run --no-sync pytest xqsa/tests/test_gpu_validation.py -q -k References`
Expected: 4 passed. If a structured-model reference assert fails (CPU misses the optimum at defaults), STOP and report — that invalidates the spec's reference assumption; do not loosen the assertion.

- [ ] **Step 5: Ruff, line-length check, commit**

```bash
uv run --no-sync ruff check xqsa/tests/test_gpu_validation.py
awk 'length > 100 {print FNR" ("length")"}' xqsa/tests/test_gpu_validation.py
git add xqsa/tests/test_gpu_validation.py
git commit -m "test(xqsa): add medium-model builders with CPU reference tests"
```
awk must print nothing.

---

### Task 2: CUDA gated class (runs for real on a CUDA development machine)

**Files:**
- Modify: `xqsa/tests/test_gpu_validation.py` (append)

**Interfaces:**
- Consumes: builders, `cpu_reference`, `GPU_PARAMS`, `GLASS_TOLERANCE`, `_IN_CI`, `_has_cupy`, `SolverCudaGPU` from Task 1.

- [ ] **Step 1: Append the CUDA class**

```python
@pytest.mark.cuda
@pytest.mark.skipif(
    not _IN_CI and not _has_cupy,
    reason="cupy not installed (skipped locally; runs and hard-fails in CI)",
)
class TestCudaGPUMediumModels:
    """SolverCudaGPU on 20-100 var models with known/CPU-referenced optima."""

    def test_ferro_chain_exact(self) -> None:
        model, ground = ferro_chain()
        result = SolverCudaGPU(**GPU_PARAMS).solve(model)
        assert result.energy == ground

    def test_frustrated_triangles_exact(self) -> None:
        model, ground = frustrated_triangles()
        result = SolverCudaGPU(**GPU_PARAMS).solve(model)
        assert result.energy == ground

    def test_bipartite_maxcut_exact(self) -> None:
        model, ground = bipartite_maxcut()
        result = SolverCudaGPU(**GPU_PARAMS).solve(model)
        assert result.energy == ground

    def test_random_spin_glass_within_tolerance_of_cpu(self) -> None:
        model, _ = random_spin_glass()
        cpu = cpu_reference(model)
        gpu = SolverCudaGPU(**GPU_PARAMS).solve(model).energy
        assert gpu <= cpu + GLASS_TOLERANCE * abs(cpu), (
            f"GPU best energy {gpu} more than {GLASS_TOLERANCE:.0%} above CPU reference {cpu}"
        )
```

- [ ] **Step 2: Run the CUDA class for real**

Run: `uv run --no-sync pytest xqsa/tests/test_gpu_validation.py -q -k Cuda`
Expected: 4 passed (on a machine with a CUDA GPU + cupy).

If an exact assert fails: first re-run once to distinguish flake from miss. If the GPU consistently misses a structured optimum at `num_sweeps=2000`, raise `num_sweeps` in `GPU_PARAMS` stepwise (4000 → 10000 → 20000) until all structured models pass reliably, and record the final value + observed energies in your report. If `20000` still fails, STOP and report the observed vs expected energies — that is a solver-quality finding (consistent with QUI-167's CUDA observations), not a test bug to paper over. Apply the same escalation if the glass misses the 2% band.

- [ ] **Step 3: Stability check — 3 consecutive runs**

Run: `for i in 1 2 3; do uv run --no-sync pytest xqsa/tests/test_gpu_validation.py -q -k Cuda 2>&1 | tail -1; done`
Expected: `4 passed` three times. Record the three lines in your report. If any run flakes, escalate `num_sweeps` per Step 2 and repeat until 3/3 stable.

- [ ] **Step 4: Ruff, line-length, commit**

```bash
uv run --no-sync ruff check xqsa/tests/test_gpu_validation.py
awk 'length > 100 {print FNR" ("length")"}' xqsa/tests/test_gpu_validation.py
git add xqsa/tests/test_gpu_validation.py
git commit -m "test(xqsa): add CUDA GPU medium-model validation tests"
```

---

### Task 3: Metal gated class + whole-file verification

**Files:**
- Modify: `xqsa/tests/test_gpu_validation.py` (append)

**Interfaces:**
- Consumes: builders, `cpu_reference`, `GPU_PARAMS`, `GLASS_TOLERANCE`, `_IN_CI`, `_has_metal`, `SolverMetalGPU` from Task 1. Metal's constructor additionally takes `strategy` ("sa" or "gibbs").

- [ ] **Step 1: Append the Metal class**

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Verify the Metal class SKIPS locally (guard correctness)**

Run: `uv run --no-sync pytest xqsa/tests/test_gpu_validation.py -rs -q -k Metal`
Expected: `6 skipped` with reason "Metal GPU not available (skipped locally; runs and hard-fails in CI)". Record the skip lines in your report — observing the skip IS the local verification for this class.

- [ ] **Step 3: Whole-file + regression verification**

```bash
uv run --no-sync pytest xqsa/tests/test_gpu_validation.py -q
uv run --no-sync pytest xqsa/tests/ -q -m "not qpu and not metal and not quip"
```
Expected: file run = 8 passed, 6 skipped (4 references + 4 cuda pass; 6 metal skip). Full xqsa run: all pass (76 pre-existing + new), no new warnings.

- [ ] **Step 4: Ruff, line-length, commit**

```bash
uv run --no-sync ruff check xqsa/tests/test_gpu_validation.py
awk 'length > 100 {print FNR" ("length")"}' xqsa/tests/test_gpu_validation.py
git add xqsa/tests/test_gpu_validation.py
git commit -m "test(xqsa): add Metal GPU medium-model validation tests"
```

---

## Self-Review

- **Spec coverage:** guard idiom (Task 1 Step 1), four builders with exact energies (Task 1 Step 3), ungated reference class (Task 1), CUDA class incl. glass tolerance (Task 2), Metal sa + gibbs incl. the ≥3-colour triangles (Task 3), observed-skip verification (Task 3 Step 2), AC mapping complete.
- **Placeholder scan:** the only stubs are Task 1 Step 1's NotImplementedError builders, replaced in Step 3 of the same task — explicit TDD, not a placeholder.
- **Type consistency:** builders return `tuple[XQMX, int|None]` everywhere; `GPU_PARAMS` dict unpacks into both solver constructors (both accept `num_reads`, `num_sweeps`, `seed`; Metal adds `strategy` passed explicitly); `cpu_reference` used identically in Tasks 1–3.
- **Escalation rule** for stochastic misses is defined once (Task 2 Step 2) and referenced by the stability step; Metal cannot be escalated locally (skips) — CI will exercise it, and the budgets are shared via `GPU_PARAMS`, already validated on CUDA.

---

### Design Plans 2026 07 13 Qui 704 Spin Packing Eval (`docs/design/plans/2026-07-13-qui-704-spin-packing-eval.md`)

# QUI-704 Spin-Packing Evaluation Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build `scripts/bench_spin_packing.py`, benchmark dense `int32` vs bit-packed spin storage for `SolverCudaGPU` on the RTX 4090 at n = 512…4096, and produce a findings doc with an adopt/reject recommendation.

**Architecture:** One benchmark script. It builds seeded synthetic spin instances (an `XQMX` for the stock solver plus matching `h`/`J` numpy arrays for the prototype), runs the shipped `SolverCudaGPU`, and runs a bench-local bit-packed variant of the same kernel that consumes an *identical* RNG stream, identical init samples, and identical fixed `beta_range` — so trajectories are bit-identical and the prototype's faithfulness is asserted by exact best-sample equality, not statistics. Metrics: per-buffer analytic bytes, measured mempool peak, median wall time.

**Tech Stack:** cupy (RawKernel), numpy, `xqvm_py.xqmx.XQMX`, `xqsa.cuda_gpu.SolverCudaGPU`. No new dependencies. Metal is analytic-only (no hardware) — handled in the findings doc, not the script.

**Spec:** `docs/design/specs/2026-07-13-qui-704-spin-packing-eval-design.md` (approved 2026-07-13).

## Global Constraints

- Work ONLY in the worktree `/home/konrad/Quip/xquad/.claude/worktrees/feature+qui-704` (branch `feature/qui-704`, base 15918ed).
- Run Python via `uv run --no-sync ...` (plain `uv run` reverts the maturin-built xqffi).
- Bench parameters (fixed): `num_reads=64`, `num_sweeps=500`, `seed=42`, `beta_range=(0.1, 10.0)` passed explicitly to BOTH variants; sizes `n ∈ (512, 1024, 2048, 4096)`; 3 timing repetitions, median reported.
- **No changes to `xqsa/` source.** The packed kernel lives only in the bench script.
- Results JSON goes under `scratch/spin_packing/` (not committed); the script and findings doc are committed.
- Style: ≤100 lines/function, ≤5 positional params, 100-char lines (`awk 'length > 100'` — repo ruff allows 120 but 100 binds), ruff clean, full 16-line AGPL header + SPDX (copy from `scripts/scale_sweep.py` if present on this branch's base — otherwise from `xqsa/tests/test_xqsa.py`).
- Commits: imperative, ≤72-char subject, no co-author bylines/trailers.

## File Structure

- Create `scripts/bench_spin_packing.py` — instance builder, stock runner, packed prototype (kernel + driver), matrix runner, JSON output.
- Create `docs/design/specs/2026-07-13-qui-704-findings.md` — Task 3, from measured data.

---

### Task 1: Bench scaffold — instances, stock runner, CLI

**Files:**
- Create: `scripts/bench_spin_packing.py`

**Interfaces:**
- Produces (Task 2 depends on these exact signatures): `build_instance(n: int, seed: int) -> tuple[XQMX, np.ndarray, np.ndarray]` returning `(model, h, j_matrix)` where `h` is `float64[n]` zeros and `j_matrix` is the full symmetric `float64[n, n]` coupling matrix; `run_stock(model: XQMX, n: int) -> dict` returning `{"variant": "stock", "n": n, "wall_s": [3 floats], "energy": int, "sample": list[int], "mempool_peak_bytes": int}`; constants `NUM_READS = 64`, `NUM_SWEEPS = 500`, `SEED = 42`, `BETA_RANGE = (0.1, 10.0)`, `SIZES = (512, 1024, 2048, 4096)`, `REPS = 3`.

- [ ] **Step 1: Create the script**

Full 16-line AGPL header + SPDX first, then:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Verify with a single-size run**

Run: `uv run --no-sync python scripts/bench_spin_packing.py --sizes 512 --out scratch/spin_packing/smoke1.json`
Expected: one line `n=512 stock: median ...s energy ...` (energy is a deterministic negative int; the same command re-run prints the identical energy), then the JSON path. If `SolverCudaGPU.solve` rejects any argument or the energies differ between two runs with the same seed, STOP and report — determinism is a prerequisite for Task 2's exact check.

- [ ] **Step 3: Ruff, line length, commit**

```bash
uv run --no-sync ruff check scripts/bench_spin_packing.py
awk 'length > 100 {print FNR" ("length")"}' scripts/bench_spin_packing.py
git add scripts/bench_spin_packing.py
git commit -m "bench(xqsa): add spin-packing benchmark scaffold (QUI-704)"
```
awk must print nothing.

---

### Task 2: Bit-packed prototype — kernel, driver, exact-trajectory check

**Files:**
- Modify: `scripts/bench_spin_packing.py`

**Interfaces:**
- Consumes: everything Task 1 produced.
- Produces: `run_packed(h: np.ndarray, j_matrix: np.ndarray, n: int) -> dict` (same row shape, `"variant": "packed"`); `PACKED_KERNEL` (CUDA source string, entry `sa_spin_packed`).

**Background you need:** the shipped spin kernel is `_SA_SPIN_KERNEL` in `xqsa/cuda_gpu.py:108-161` — read it first. The driver logic to replicate is `_run_sa` in the same file (`xqsa/cuda_gpu.py:318-392`): RNG stream order is (1) `rng.integers(0, 2, size=(num_reads, n), dtype=cupy.int32)` mapped `*2-1` to spins, then (2) `rng.random(size=(num_reads, num_sweeps, n), dtype=cupy.float64)`. Reproduce that order exactly with `cupy.random.default_rng(SEED)` so the packed run consumes the identical stream. Also read how `solve()` builds `h`/`j_matrix` from the XQMX (the code just above `_run_sa`) — if it builds J symmetric-full as Task 1 assumes, the exact-trajectory check will pass; if the check fails, compare your bench `j_matrix` against the solver's construction and fix `build_instance` to match (report what you found).

- [ ] **Step 1: Append the packed kernel + driver**

```
/* Detailed source-code truncated for AI context efficiency. */
```

Note the timing asymmetry this creates: `run_stock` times the whole `solve()` call (includes RNG generation + host transfers), while the packed timer brackets only the kernel. Fix the asymmetry by moving the packed variant's `t0 = time.perf_counter()` to BEFORE the `rng = ...` line so both walls cover RNG + kernel + sync. Keep the row's `wall_s` semantics identical between variants; state remaining differences (XQMX extraction happens only in stock) in the findings doc.

- [ ] **Step 2: Wire the packed run + faithfulness assert into `main`**

Replace the loop body in `main` with:

```python
        model, h, j_matrix = build_instance(n, SEED)
        stock = run_stock(model, n)
        packed = run_packed(h, j_matrix, n)
        if packed["sample"] != stock["sample"]:
            raise SystemExit(
                f"TRAJECTORY DIVERGENCE at n={n}: packed prototype is not faithful "
                f"(first mismatch at index "
                f"{next(i for i, (a, b) in enumerate(zip(packed['sample'], stock['sample'])) if a != b)})"
            )
        analytic = analytic_bytes(n)
        rows.append({**stock, "analytic": analytic})
        rows.append({**packed, "analytic": analytic})
        med_s = sorted(stock["wall_s"])[1]
        med_p = sorted(packed["wall_s"])[1]
        print(
            f"n={n}: stock {med_s:.2f}s / packed {med_p:.2f}s "
            f"(x{med_p / med_s:.2f}), energy {stock['energy']}, samples identical"
        )
```

- [ ] **Step 3: Verify faithfulness at n=512**

Run: `uv run --no-sync python scripts/bench_spin_packing.py --sizes 512 --out scratch/spin_packing/smoke2.json`
Expected: `samples identical` in the output line. If TRAJECTORY DIVERGENCE fires: first check J-layout (read `solve()`'s array construction in `xqsa/cuda_gpu.py` and align `build_instance`), then RNG order. If it still diverges after both, STOP and report the mismatch details — do not weaken the check to a tolerance.

- [ ] **Step 4: Ruff, line length, commit**

```bash
uv run --no-sync ruff check scripts/bench_spin_packing.py
awk 'length > 100 {print FNR" ("length")"}' scripts/bench_spin_packing.py
git add scripts/bench_spin_packing.py
git commit -m "bench(xqsa): add bit-packed kernel prototype with exact-trajectory check"
```

---

### Task 3: Full matrix run + findings doc

**Files:**
- Create: `docs/design/specs/2026-07-13-qui-704-findings.md`
- Output (not committed): `scratch/spin_packing/results.json`

- [ ] **Step 1: Full run**

Run: `uv run --no-sync python scripts/bench_spin_packing.py`
Expected: four `n=...` lines, each ending `samples identical`; up to ~15 min at n=4096 (single-thread-per-block kernel is slow — that slowness is itself a documented finding from QUI-167). If n=4096 exceeds 30 min, record the sizes that completed and note the abort in the findings; do not kill smaller sizes.

- [ ] **Step 2: Write the findings doc**

Create `docs/design/specs/2026-07-13-qui-704-findings.md` with exactly these sections, filling every number from `scratch/spin_packing/results.json` (never hand-estimate):
1. Header: ticket link, date, machine (RTX 4090 / WSL2 / cupy version), bench parameters.
2. **Device-memory breakdown table** — per n: J bytes, randoms bytes, dense-samples bytes, packed-samples bytes, spins' % of total (analytic), measured mempool peaks for both variants.
3. **Throughput table** — per n: stock median wall, packed median wall, ratio.
4. **Faithfulness** — one line: exact best-sample equality held at every size (or details).
5. **Metal analysis (no hardware)** — analytic: no randoms buffer (on-device xorshift32, `metal_gpu.py` docstring), J float32 `n²×4` dominates; spins' % of Metal footprint at the same sizes; conclusion carries a fortiori.
6. **Recommendation** — adopt/reject against the ticket's criterion ("material footprint reduction without unacceptable throughput regression"), stated plainly with the measured shares.
7. **Follow-up candidates** (documented, not implemented): CUDA on-device RNG (eliminates the dominant randoms buffer, re-aligns solvers, lifts the 80%-VRAM guard); kernel occupancy (`block=(1,)` single-thread launch, consistent with QUI-167's 5× CPU-vs-GPU finding).

- [ ] **Step 3: Commit**

```bash
git add docs/design/specs/2026-07-13-qui-704-findings.md
git commit -m "docs: record QUI-704 spin-packing evaluation findings"
```

---

## Self-Review

- **Spec coverage:** instances + stock runner (Task 1), packed prototype + faithfulness (Task 2), full matrix + findings incl. Metal analytic section and follow-up candidates (Task 3). Linear comment/closeout is controller-side, per spec Deliverable 3.
- **Placeholder scan:** none — every code step is complete; findings sections enumerate exact required content sourced from the results JSON.
- **Type consistency:** row dict shape shared by `run_stock`/`run_packed`; constants used identically across tasks; `build_instance` tuple order `(model, h, j_matrix)` consistent.
- **Known risk, mitigated:** Task 1's assumption that `solve()` builds J symmetric-full is verified end-to-end by Task 2's exact-trajectory check, with an explicit diagnose-and-align instruction if it fires.

---

### Design Plans 2026 07 16 Qui 852 Gpu Sa Occupancy (`docs/design/plans/2026-07-16-qui-852-gpu-sa-occupancy.md`)

# QUI-852: GPU SA Kernel Occupancy (CUDA + Metal) — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Parallelize the O(n) local-field reduction inside the GPU SA kernels across the threads of each replica's block/threadgroup (currently 1 thread per replica), keeping spin updates strictly sequential — same proposal order, same acceptance rule, same RNG consumption order (Linear QUI-852).

**Architecture:** CUDA: launch `(num_reads,)` blocks × `(256,)` threads; each thread computes a strided partial sum of `Σⱼ J[i,j]·x[j]`, a shared-memory tree reduction combines them, and thread 0 alone applies Metropolis + flips the spin, with `__syncthreads()` barriers around the sequential section. Metal: identical shape with `threadgroup` memory and `threadgroup_barrier`, threadgroup width clamped to a power of two ≤ `maxTotalThreadsPerThreadgroup` (≤ 256). The Metal `gibbs` pipeline keeps 1 thread/threadgroup (its parallelization interacts with the coloring — out of scope). Verification uses a committed benchmark harness: on **integer-coefficient instances, float64/float32 sums are exact in any order, so the new kernels must be bit-identical to the shipped ones** — a hard gate; float instances get distribution-parity checks; n=4096 and n=512 timings are re-measured against a baseline captured at the branch base.

**Tech Stack:** CUDA via CuPy RawKernel, Metal Shading Language via pyobjc, numpy, pytest, `uv run`.

## Global Constraints

- **Branch is STACKED on `feature/qui-854`** (MR !91): branch `feature/qui-852` from `feature/qui-854`, NOT from main. The kernels being modified already take the beta buffer at arg index 5 (CUDA) / buffer 2 (Metal).
- **Markov chain definition unchanged:** proposal order (i = 0..n-1 sequential), acceptance rule, and RNG consumption order must be untouched. Only thread 0 reads RNG (CUDA: `rand_ptr[(long long)sweep * n + i]`; Metal: `rand_unit(rng)` on thread 0's state). Only floating-point *summation order* inside the local field may change.
- **Hard verification gate:** on integer-coefficient instances the new CUDA kernel must produce byte-identical results (raw_energy, energy, sample hash) to the baseline captured at the branch base. Not approximately — exactly.
- **Metal hardware validation is DEFERRED** (no Apple Silicon on this machine): MSL changes are verified structurally via the mocked suite (dispatch shape, ABI); the MR must state that real-hardware validation needs an M-series run (post-merge CI job `test:metal` or a reviewer machine). Do not claim Metal numerical verification.
- Thread counts: CUDA fixed `_THREADS_PER_REPLICA = 256`. Metal `_SA_THREADGROUP_WIDTH = 256`, clamped at dispatch to the largest power of two ≤ `pipeline.maxTotalThreadsPerThreadgroup()`; the tree reduction requires a power-of-two width.
- Repo rules: ruff format/lint at line-length 120; `uv run --no-sync pytest ...`; commits signed off (`git commit -s`), imperative ≤72-char subject, NO co-author bylines/Claude attribution. Commit after every green test cycle.
- Mocked CUDA test command: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "CudaGPUMocked" -v`
- Mocked Metal test command: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "MetalGPUMocked" -v`
- Real-GPU command: `uv run --extra cuda pytest xqsa/tests/test_xqsa.py -m cuda -v` (RTX 4090 present; first run installs cupy — allow 600s timeout)
- Bench harness runs use `uv run --extra cuda python scripts/bench_occupancy.py ...` — long timeouts (up to 1800000 ms for the timing baseline; run in foreground with that timeout).

---

### Task 1: Stacked branch + benchmark harness + baseline capture (runs the SHIPPED kernel)

**Files:**
- Create: `scripts/bench_occupancy.py`
- Create (uncommitted, scratch): `scratch/qui-852/baseline-trajectories.json`, `scratch/qui-852/baseline-parity.json`, `scratch/qui-852/baseline-timing.json`

**Interfaces:**
- Produces: `scripts/bench_occupancy.py` with subcommands `trajectories`, `parity`, `timing`, each writing JSON via `--out`, and `compare --mode {trajectories,parity,timing} BASELINE CURRENT` which exits 1 on trajectory mismatch and prints stats otherwise. Task 3 re-runs the same subcommands on the new kernel and compares.
- CRITICAL ORDERING: baselines MUST be captured in this task, before any kernel change.

- [ ] **Step 1: Verify branch state** (the controller prepared the worktree — confirm, don't create):

Run: `git branch --show-current && git log --oneline -1 && git merge-base --is-ancestor $(git rev-parse origin/feature/qui-854 2>/dev/null || git rev-parse feature/qui-854) HEAD && echo STACKED-OK`
Expected: `feature/qui-852`, and `STACKED-OK`.

- [ ] **Step 2: Write `scripts/bench_occupancy.py`** (complete file):

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Smoke-check the harness** (fast, real GPU):

Run: `uv run --extra cuda python -c "import scripts.bench_occupancy as b; m=b.build_maxcut(16,0,'binary'); r,_=b._solve(m,1,50,0); print(r)"` — expect a record dict printed, no traceback. (If `scripts` is not importable as a package, run the equivalent through `python scripts/bench_occupancy.py trajectories --out /tmp/smoke.json` after temporarily reducing nothing — instead just proceed to Step 4, which exercises the same code.)

- [ ] **Step 4: Capture baselines** (SHIPPED kernel — this is why this step precedes all kernel edits):

```bash
mkdir -p scratch/qui-852
uv run --extra cuda python scripts/bench_occupancy.py trajectories --out scratch/qui-852/baseline-trajectories.json
uv run --extra cuda python scripts/bench_occupancy.py parity --out scratch/qui-852/baseline-parity.json
uv run --extra cuda python scripts/bench_occupancy.py timing --out scratch/qui-852/baseline-timing.json
```

Timeouts: 600000 ms, 900000 ms, 1800000 ms respectively (the n=4096 timing case alone runs ~10 minutes on the shipped kernel). Record the printed timing numbers in your report.

- [ ] **Step 5: Lint + commit the harness** (scratch JSONs stay uncommitted; `scratch/` is gitignored):

```bash
uv run --no-sync ruff format --check scripts/bench_occupancy.py && uv run --no-sync ruff check scripts/bench_occupancy.py
git add scripts/bench_occupancy.py
git commit -s -m "bench(xqsa): add QUI-852 occupancy trajectory/parity/timing harness"
```

If `ruff format --check` fails, run `uv run --no-sync ruff format scripts/bench_occupancy.py` first.

---

### Task 2: CUDA kernels — parallel local-field reduction

**Files:**
- Modify: `xqsa/cuda_gpu.py` — `_SA_BINARY_KERNEL`, `_SA_SPIN_KERNEL`, the module constants block, and the kernel launch in `_run_sa`
- Modify: `xqsa/tests/test_xqsa.py` — `_capture_kernel_args` helper (record grid/block), one new mocked test

**Interfaces:**
- Consumes: kernel ABI from !91 — args `(h, J, samples, energies, randoms, betas, n, num_sweeps)`; the mocked fake kernel receives `(grid, block, args)` and simulates on CPU (it ignores `block`, so it needs NO functional change).
- Produces: launch shape `(num_reads,), (_THREADS_PER_REPLICA,)` with `_THREADS_PER_REPLICA = 256`; kernel arg list UNCHANGED. Task 3 verifies numerics; Task 4 mirrors the design in MSL.

- [ ] **Step 1: Add the failing mocked test.** Extend the existing `_capture_kernel_args` staticmethod in `TestSolverCudaGPUMocked` — change its body to also record the launch shape:

```python
    @staticmethod
    def _capture_kernel_args(solver) -> dict:
        """Shadow the cached binary kernel with a wrapper that records call args."""
        captured: dict = {}
        real_kernel = solver._binary_kernel

        def wrapper(grid, block, args):
            captured["grid"] = grid
            captured["block"] = block
            captured["args"] = args
            return real_kernel(grid, block, args)

        solver.__dict__["_binary_kernel"] = wrapper
        return captured
```

Then append inside `TestSolverCudaGPUMocked`:

```python
    def test_launch_shape_parallel_reduction_mocked(self, mock_cupy_env) -> None:
        """Each replica's block launches _THREADS_PER_REPLICA threads (QUI-852)."""
        from xqsa.cuda_gpu import _THREADS_PER_REPLICA, SolverCudaGPU as _Solver

        model = XQMX.binary_model(2)
        model.set_linear(0, 1.0)

        solver = _Solver(num_reads=7, num_sweeps=10, seed=1)
        captured = self._capture_kernel_args(solver)
        solver.solve(model)

        assert captured["grid"] == (7,)
        assert captured["block"] == (_THREADS_PER_REPLICA,)
        assert _THREADS_PER_REPLICA == 256
```

- [ ] **Step 2: Run it — expect FAIL** (`ImportError: cannot import name '_THREADS_PER_REPLICA'`):

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "launch_shape" -v`

- [ ] **Step 3: Rewrite `_SA_BINARY_KERNEL`.** Replace the full kernel source string with:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 4: Rewrite `_SA_SPIN_KERNEL`** — identical structure; the three differences from the binary kernel are the initial-energy loop, `delta_E`, and the flip:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 5: Host-side launch.** Under `_SUPPORTED_SCHEDULES = ...` add:

```python
# One CUDA block per replica; this many threads cooperate on the local-field
# reduction (QUI-852). Must be a power of two (tree reduction) and match the
# kernels' `__shared__ double sdata[256]`.
_THREADS_PER_REPLICA = 256
```

In `_run_sa`, change the launch:

```python
        kernel(
            (num_reads,),  # grid: one block per replica
            (_THREADS_PER_REPLICA,),  # block: cooperative local-field reduction
            (
```

(the args tuple is unchanged). Also update the comment line `# block: single thread per replica` if it survives anywhere.

- [ ] **Step 6: Run mocked suite** — all pass, including the new launch-shape test (the fake kernel ignores `block`, so pre-existing tests are unaffected):

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "CudaGPUMocked" -v`
Expected: 22 passed (21 existing + 1 new).

- [ ] **Step 7: Run the real-GPU suite** (NVRTC-compiles the new kernels; trivial-model results are exact-sum cases and must be unchanged):

Run: `uv run --extra cuda pytest xqsa/tests/test_xqsa.py -m cuda -v` (timeout 600000)
Expected: 18 passed.

- [ ] **Step 8: Lint + commit:**

```bash
uv run --no-sync ruff format --check xqsa/cuda_gpu.py xqsa/tests/test_xqsa.py && uv run --no-sync ruff check xqsa/cuda_gpu.py xqsa/tests/test_xqsa.py
git add xqsa/cuda_gpu.py xqsa/tests/test_xqsa.py
git commit -s -m "perf(xqsa): parallelize CUDA SA local-field reduction across block"
```

---

### Task 3: CUDA verification against baselines (RTX 4090)

**Files:**
- Create (uncommitted, scratch): `scratch/qui-852/new-trajectories.json`, `scratch/qui-852/new-parity.json`, `scratch/qui-852/new-timing.json`

**Interfaces:**
- Consumes: `scripts/bench_occupancy.py` (Task 1), the baselines in `scratch/qui-852/baseline-*.json`, and the new kernels (Task 2).
- Produces: the compare outputs (paste into your report verbatim); Task 5 copies the numbers into the findings doc.

- [ ] **Step 1: Re-run the three captures on the new kernel:**

```bash
uv run --extra cuda python scripts/bench_occupancy.py trajectories --out scratch/qui-852/new-trajectories.json
uv run --extra cuda python scripts/bench_occupancy.py parity --out scratch/qui-852/new-parity.json
uv run --extra cuda python scripts/bench_occupancy.py timing --out scratch/qui-852/new-timing.json
```

(timeouts 600000 / 900000 / 900000 — the new kernel should be much faster; if a run exceeds its timeout, that is itself a finding, report it).

- [ ] **Step 2: The trajectory gate (MUST exit 0):**

Run: `uv run --no-sync python scripts/bench_occupancy.py compare --mode trajectories scratch/qui-852/baseline-trajectories.json scratch/qui-852/new-trajectories.json`
Expected: `OK: all 120 trajectory records bit-identical`, exit 0. **If ANY record differs, the kernel violates the sequential-semantics contract — STOP, report BLOCKED with the mismatch list. Do not proceed and do not relax the gate.**

- [ ] **Step 3: Parity + timing comparisons (informational, paste output):**

```bash
uv run --no-sync python scripts/bench_occupancy.py compare --mode parity scratch/qui-852/baseline-parity.json scratch/qui-852/new-parity.json
uv run --no-sync python scripts/bench_occupancy.py compare --mode timing scratch/qui-852/baseline-timing.json scratch/qui-852/new-timing.json
```

Flag in your report if any parity `dmean` exceeds 2%, or if any timing case shows < 2x speedup (the reduction should be a large win at n=4096; a small win means the launch change didn't take effect).

- [ ] **Step 4: No commit** (scratch only). Write all numbers into your report file — they are the evidence Task 5 publishes.

---

### Task 4: Metal — MSL threadgroup reduction + per-strategy dispatch width

**Files:**
- Modify: `xqsa/metal_gpu.py` — `_SA_KERNEL` MSL source, buffer-ABI comment block (~lines 63-73), `_dispatch` (~line 521), the `sa` dispatch call site inside `_run_sa`, module constants
- Modify: `xqsa/tests/test_xqsa.py` — `mock_metal_env`'s `_Encoder.dispatchThreadgroups_threadsPerThreadgroup_` (record the width), two new mocked tests

**Interfaces:**
- Consumes: MSL `sa_metal` buffer ABI (0:h 1:J 2:beta 3:samples 4:energies 5:n 6:num_sweeps 7:base_seed 8:is_spin) — UNCHANGED; `_dispatch(pipeline, encoder, num_reads)` currently hardcodes `MTLSizeMake(1, 1, 1)`.
- Produces: `_dispatch(pipeline, encoder, num_reads, threads_per_group=1)`; `sa` dispatches with the clamped power-of-two width (≤ `_SA_THREADGROUP_WIDTH = 256`), `gibbs` stays at 1. The gibbs MSL kernel is untouched.

- [ ] **Step 1: Extend the Metal mock to record the threadgroup width.** In `mock_metal_env`'s `_Encoder`, replace:

```python
        def dispatchThreadgroups_threadsPerThreadgroup_(self, grid, per_group):
            self.num_reads = int(grid[0])
```

with:

```python
        def dispatchThreadgroups_threadsPerThreadgroup_(self, grid, per_group):
            self.num_reads = int(grid[0])
            self.threads_per_group = tuple(int(v) for v in per_group)
```

- [ ] **Step 2: Add the failing mocked tests** (inside `TestSolverMetalGPUMocked`, reusing that class's existing encoder-capture pattern — see `test_num_sweeps_per_beta_run_mocked` for the `capturing_queue` scaffold, and reuse it verbatim):

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Run — expect the sa test to FAIL** (`ImportError: cannot import name '_SA_THREADGROUP_WIDTH'`), the gibbs test to FAIL (`AttributeError: threads_per_group`) until Step 1's mock change lands (do Step 1 first, then gibbs fails only if the width is wrong):

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "dispatch_width" -v`

- [ ] **Step 4: Rewrite the `sa_metal` MSL kernel body** inside `_SA_KERNEL` (the `_RNG_PRELUDE` and the buffer parameter list are UNCHANGED; add the two thread-position parameters and restructure the body):

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 5: Host-side dispatch.** Under `_SUPPORTED_SCHEDULES = ...` in metal_gpu.py add:

```python
# Threadgroup width for the sa pipeline's cooperative local-field reduction
# (QUI-852). Clamped at dispatch to the largest power of two that the
# compiled pipeline supports; must match the kernel's `threadgroup float
# sdata[256]`. The gibbs pipeline stays at one thread per replica.
_SA_THREADGROUP_WIDTH = 256
```

Change `_dispatch`:

```python
    def _dispatch(self, pipeline, encoder, num_reads: int, threads_per_group: int = 1) -> None:
        """Encode and dispatch one threadgroup per replica."""
        encoder.setComputePipelineState_(pipeline)
        grid = self._metal.MTLSizeMake(num_reads, 1, 1)
        per_group = self._metal.MTLSizeMake(threads_per_group, 1, 1)
        encoder.dispatchThreadgroups_threadsPerThreadgroup_(grid, per_group)
        encoder.endEncoding()
```

At the `sa` dispatch call site in `_run_sa`, compute the clamped width and pass it (the gibbs call site is left untouched, defaulting to 1):

```python
        max_threads = int(pipeline.maxTotalThreadsPerThreadgroup())
        width = min(_SA_THREADGROUP_WIDTH, 1 << (max_threads.bit_length() - 1))
        self._dispatch(pipeline, encoder, num_reads, threads_per_group=width)
```

(Locate the existing `self._dispatch(pipeline, encoder, num_reads)` inside the sa path; `pipeline` is the variable already in scope there — keep its existing name if it differs.)

- [ ] **Step 6: Update the buffer-ABI comment block** (metal_gpu.py ~lines 63-73): change the line `# Each threadgroup (one thread) owns one replica via threadgroup_position_in_grid.` to:

```python
# Each threadgroup owns one replica via threadgroup_position_in_grid.
# sa_metal runs _SA_THREADGROUP_WIDTH cooperating threads per replica
# (local-field reduction, QUI-852); gibbs_metal remains single-thread.
```

- [ ] **Step 7: Run the mocked Metal suite** — all pass (the CPU reference in the mock is a semantic simulation keyed on the buffer ABI, which is unchanged):

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "MetalGPUMocked" -v`
Expected: all pass, including the two new dispatch-width tests.

- [ ] **Step 8: Full-file mocked regression + lint + commit:**

```bash
uv run --no-sync pytest xqsa/tests/test_xqsa.py -q
uv run --no-sync ruff format --check xqsa/metal_gpu.py xqsa/tests/test_xqsa.py && uv run --no-sync ruff check xqsa/metal_gpu.py xqsa/tests/test_xqsa.py
git add xqsa/metal_gpu.py xqsa/tests/test_xqsa.py
git commit -s -m "perf(xqsa): parallelize Metal SA local-field reduction across threadgroup"
```

Expected pytest: same pass/skip counts as before this task plus 2 (real-GPU/Metal classes skipped on this machine).

---

### Task 5: Findings doc + docstring updates

**Files:**
- Create: `docs/design/specs/2026-07-16-qui-852-findings.md`
- Modify: `xqsa/cuda_gpu.py` (class docstring, ~line 170), `xqsa/metal_gpu.py` (class docstring, ~line 228)

**Interfaces:**
- Consumes: the Task 1 baseline numbers and Task 3 compare outputs (in `.superpowers/sdd/task-1-report.md` and `.superpowers/sdd/task-3-report.md` — read both).

- [ ] **Step 1: Write the findings doc** with this structure (fill the bracketed numbers from the task reports — every number must come from an actual run, never invent one):

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Docstring touch-ups.** In `xqsa/cuda_gpu.py`, the class docstring sentence "Each replica (controlled by ``num_reads``) executes independently in its own CUDA thread block." — append: "Within a block, ``_THREADS_PER_REPLICA`` threads cooperate on the local-field reduction while spin updates remain sequential (QUI-852)." In `xqsa/metal_gpu.py`, find the equivalent per-replica sentence in the class docstring and append the analogous sentence naming `_SA_THREADGROUP_WIDTH` and noting gibbs stays single-threaded.

- [ ] **Step 3: Lint + commit:**

```bash
uv run --no-sync ruff format --check xqsa/cuda_gpu.py xqsa/metal_gpu.py && uv run --no-sync ruff check xqsa/cuda_gpu.py xqsa/metal_gpu.py
git add docs/design/specs/2026-07-16-qui-852-findings.md xqsa/cuda_gpu.py xqsa/metal_gpu.py
git commit -s -m "docs(xqsa): QUI-852 occupancy findings and solver docstrings"
```

---

### Task 6: Preflight and merge request (controller-executed)

- [ ] **Step 1:** `make preflight-py` — all green.
- [ ] **Step 2:** Push `feature/qui-852`; `glab mr create` with **target branch `feature/qui-854`** (stacked on MR !91 — GitLab retargets to main when !91 merges). Description covers: the reduction design, the trajectory gate result, parity stats, the timing table, the explicit Metal deferred-validation caveat, gibbs out-of-scope note, "Implements QUI-852", and the stacked-on-!91 note.
- [ ] **Step 3:** Verify pipeline starts; move QUI-852 to In Review on Linear with the MR link.

---

## Self-review (done at plan time)

- Ticket coverage: parallel reduction both solvers (Tasks 2, 4); sequential spins + RNG order preserved (kernel design + trajectory gate, Task 3); near-identical trajectory check → strengthened to bit-identical on integer instances (Task 3 Step 2); energy-distribution parity across seeds×instances (Tasks 1/3); best-energy + CUDA-vs-CPU gap context (timing cases n=512/4096, findings §3); gibbs/checkerboard out of scope (findings §5). ✓
- No placeholders: all kernels, harness, tests, and host changes shown in full; findings doc numbers are explicitly sourced from run reports. ✓
- Type consistency: `_THREADS_PER_REPLICA`/`_SA_THREADGROUP_WIDTH` = 256 everywhere; harness subcommand names match between Tasks 1 and 3; capture-helper keys (`grid`/`block`/`args`) match between helper and test. ✓

---

### Design Plans 2026 07 16 Qui 854 Cuda Num Sweeps Per Beta (`docs/design/plans/2026-07-16-qui-854-cuda-num-sweeps-per-beta.md`)

# QUI-854: `num_sweeps_per_beta` + `beta_schedule_type` for SolverCudaGPU — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Extend `SolverCudaGPU` with the `num_sweeps_per_beta` equilibration knob and `beta_schedule_type` ("geometric"/"linear"), replacing the in-kernel hardcoded linear beta ramp with a host-built per-sweep schedule buffer, so all three SA solvers share the same API (Linear ticket QUI-854, "fuller option").

**Architecture:** The CUDA kernels (`_SA_BINARY_KERNEL`, `_SA_SPIN_KERNEL` in `xqsa/cuda_gpu.py`) currently compute `beta = beta_start + (beta_end − beta_start) · (sweep / (num_sweeps − 1))` inline. We replace the two `double` scalar kernel params with a `const double* betas` buffer of length `num_sweeps`, built host-side: `num_betas = num_sweeps // num_sweeps_per_beta` schedule points expanded via `np.repeat(..., num_sweeps_per_beta)` — mirroring `SolverMetalGPU`. The mocked test fixture `mock_cupy_env` contains a **numpy reimplementation of the kernel** (fake `RawKernel`) that must change in lockstep with the kernel signature.

**Tech Stack:** Python 3.12, CuPy (`cupy-cuda12x`), numpy, pytest. Test runner: `uv run pytest`. Mocked CUDA tests run without a GPU; real-GPU tests run locally on the RTX 4090.

## Global Constraints

- **Bit-identical default (user decision):** with `num_sweeps_per_beta=1` and `beta_schedule_type="linear"` (the new default), the schedule must reproduce the retired in-kernel formula **bit-for-bit** in float64. Do NOT use `np.linspace` for the linear schedule — use the exact expression `beta_start + (beta_end − beta_start) * (np.arange(num_betas) / np.float64(num_betas − 1))`.
- **Divide semantics (QUI-685 parity):** `num_sweeps` stays the total sweep count; `num_betas = num_sweeps // num_sweeps_per_beta`; `num_sweeps` must be divisible by `num_sweeps_per_beta`, else `ValueError`. Random-buffer size (`num_reads * num_sweeps * n`) and its OOM pre-flight are unchanged.
- **CUDA default schedule type is `"linear"`** (the historical CUDA ramp), NOT `"geometric"` (Metal's default) — changing it would break the reproduce-current-output acceptance criterion. Document the divergence.
- **`num_betas == 1` semantics:** ~~single-element schedule at `beta_start` (hottest) — matches both the retired kernel's `num_sweeps <= 1` branch and `SolverMetalGPU`.~~
  **Superseded (2026-07-20, after !90 review):** the schedule is a single element at `beta_end` (coldest) — a greedy quench matching `SolverDWaveCPU` and the revised `SolverMetalGPU` (!90 commit `c9358c9`). This intentionally departs from the retired kernel's `num_sweeps <= 1` branch, so the bit-identity guarantee above holds only for `num_sweeps >= 2`. Cross-backend consistency was preferred over bit-identity on a level that performs no annealing.
- Repo rules: 100-char lines, ruff format + lint clean, Google-style docstrings on public APIs, AGPL headers untouched. Commits: imperative mood, ≤72-char subject, **signed off (`git commit -s`)**, NO co-author bylines. Commit after every green test cycle (user preference: frequent commits, not per-task).
- All work on branch `feature/qui-854` (Linear's `gitBranchName`), branched from `origin/main`. Do not touch `xqsa/metal_gpu.py` or `xqsa/dwave_cpu.py` (MR !90 owns those).
- Mocked test command: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "CudaGPUMocked" -v`
- Real-GPU test command: `uv run --extra cuda pytest xqsa/tests/test_xqsa.py -m cuda -v` (if `cupy` is already importable in the venv, `uv run --no-sync` works too)

---

### Task 1: Branch + `_compute_beta_schedule` on SolverCudaGPU

**Files:**
- Modify: `xqsa/cuda_gpu.py` (imports ~line 35, module constants ~line 44, new method after `_auto_beta_range` ~line 315)
- Test: `xqsa/tests/test_xqsa.py` (append to `class TestSolverCudaGPUMocked`, before the real-GPU section ~line 686)

**Interfaces:**
- Consumes: nothing new.
- Produces: `SolverCudaGPU._compute_beta_schedule(num_betas: int, beta_range: tuple[float, float], schedule_type: str) -> npt.NDArray[np.float64]`; module constant `_SUPPORTED_SCHEDULES = frozenset({"geometric", "linear"})`. Task 2 calls this from `_run_sa` and validates against `_SUPPORTED_SCHEDULES`.

- [ ] **Step 1: Create the branch**

```bash
cd /home/konrad/Quip/xquad && git checkout -b feature/qui-854 origin/main
```

- [ ] **Step 2: Write the failing tests** — append inside `TestSolverCudaGPUMocked` (after `test_oom_guard`):

```python
    def test_compute_beta_schedule_mocked(self, mock_cupy_env) -> None:
        """_compute_beta_schedule returns num_betas float64 points for both types."""
        from xqsa.cuda_gpu import SolverCudaGPU as _Solver

        solver = _Solver()
        for schedule_type in ("geometric", "linear"):
            schedule = solver._compute_beta_schedule(20, (0.05, 5.0), schedule_type)
            assert schedule.shape == (20,)
            assert schedule.dtype == np.float64

    def test_compute_beta_schedule_linear_bit_identical_mocked(self, mock_cupy_env) -> None:
        """The linear schedule reproduces the retired in-kernel ramp bit-for-bit."""
        from xqsa.cuda_gpu import SolverCudaGPU as _Solver

        solver = _Solver()
        schedule = solver._compute_beta_schedule(200, (0.05, 5.0), "linear")
        expected = 0.05 + (5.0 - 0.05) * (np.arange(200, dtype=np.float64) / np.float64(199))
        np.testing.assert_array_equal(schedule, expected)

    def test_compute_beta_schedule_single_level_mocked(self, mock_cupy_env) -> None:
        """A single temperature level anneals at beta_start (hottest)."""
        from xqsa.cuda_gpu import SolverCudaGPU as _Solver

        solver = _Solver()
        for schedule_type in ("geometric", "linear"):
            schedule = solver._compute_beta_schedule(1, (0.05, 5.0), schedule_type)
            assert schedule.shape == (1,)
            assert schedule[0] == np.float64(0.05)
```

- [ ] **Step 3: Run tests to verify they fail**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "compute_beta_schedule" -v`
Expected: 3 FAILED with `AttributeError: 'SolverCudaGPU' object has no attribute '_compute_beta_schedule'`

- [ ] **Step 4: Implement.** In `xqsa/cuda_gpu.py`:

(a) Under `import numpy as np` (line 35) add:

```python
import numpy.typing as npt
```

(b) Under `_SUPPORTED_STRATEGIES = frozenset({"sa"})` (line 44) add:

```python
_SUPPORTED_SCHEDULES = frozenset({"geometric", "linear"})
```

(c) After `_auto_beta_range` (ends line 315) add:

```python
    def _compute_beta_schedule(
        self, num_betas: int, beta_range: tuple[float, float], schedule_type: str
    ) -> npt.NDArray[np.float64]:
        """Build the per-temperature-level inverse-temperature schedule.

        Returns ``num_betas`` beta points; ``_run_sa`` expands this to the
        per-sweep kernel buffer via ``np.repeat(..., num_sweeps_per_beta)``.

        The ``"linear"`` branch reproduces the retired in-kernel ramp
        ``beta_start + (beta_end - beta_start) * (sweep / (num_sweeps - 1))``
        term-for-term in float64, so the default configuration
        (``num_sweeps_per_beta=1``) is bit-identical to the pre-buffer
        kernel output. Do not replace it with ``np.linspace``, which
        computes linear spacing differently in the last ulp.
        """
        beta_start, beta_end = beta_range
        if num_betas == 1:
            # A single temperature level anneals at beta_start (hottest),
            # matching the retired kernel's num_sweeps <= 1 branch and
            # SolverMetalGPU's single-level convention.
            return np.array([beta_start], dtype=np.float64)
        if schedule_type == "geometric":
            start = max(beta_start, 1e-12)
            return np.geomspace(start, beta_end, num_betas).astype(np.float64)
        steps = np.arange(num_betas, dtype=np.float64) / np.float64(num_betas - 1)
        return np.float64(beta_start) + (np.float64(beta_end) - np.float64(beta_start)) * steps
```

- [ ] **Step 5: Run tests to verify they pass**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "compute_beta_schedule" -v`
Expected: 3 PASSED

- [ ] **Step 6: Commit**

```bash
git add xqsa/cuda_gpu.py xqsa/tests/test_xqsa.py
git commit -s -m "feat(xqsa): add host-side beta schedule builder to SolverCudaGPU"
```

---

### Task 2: Kernel ABI change, parameter plumbing, and fixture lockstep

**Files:**
- Modify: `xqsa/cuda_gpu.py` — kernel sources (lines 50-162), `__init__` (193-217), `solve` (229-281), `_run_sa` (317-391), class docstring (166-191)
- Modify: `xqsa/tests/test_xqsa.py` — `mock_cupy_env` fake kernel (`_make_sa_kernel`, ~lines 495-556)
- Test: `xqsa/tests/test_xqsa.py` (append inside `TestSolverCudaGPUMocked`)

**Interfaces:**
- Consumes: `_compute_beta_schedule` and `_SUPPORTED_SCHEDULES` from Task 1.
- Produces: new kernel ABI — args `(h, J, samples, energies, randoms, betas, n, num_sweeps)` where `betas` is a float64 device array of length `num_sweeps` at **index 5**; new public params `num_sweeps_per_beta: int = 1` and `beta_schedule_type: str = "linear"` on `__init__` and `solve` kwargs; metadata params keys `{strategy, num_sweeps, num_sweeps_per_beta, num_betas, beta_range, beta_schedule_type, raw_energy}`. Task 3's capture tests read the buffer at args index 5.

- [ ] **Step 1: Write the failing validation/metadata tests** — append inside `TestSolverCudaGPUMocked`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Also **extend** the existing `test_metadata_schema_mocked` (test_xqsa.py:629) by adding after its current assertion:

```python
        assert set(result.metadata["params"].keys()) == {
            "strategy",
            "num_sweeps",
            "num_sweeps_per_beta",
            "num_betas",
            "beta_range",
            "beta_schedule_type",
            "raw_energy",
        }
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "CudaGPUMocked" -v`
Expected: the 3 new tests FAIL (no ValueError raised / KeyError `num_sweeps_per_beta`); `test_metadata_schema_mocked` FAILS on the key-set; all other mocked tests still PASS.

- [ ] **Step 3: Change both kernel sources.** In `_SA_BINARY_KERNEL` (lines 50-106) and `_SA_SPIN_KERNEL` (lines 108-162), apply the identical two edits:

(a) Replace the parameter list tail

```c
    int    n,
    int    num_sweeps,
    double beta_start,
    double beta_end
```

with

```c
    const double* __restrict__ betas,
    int    n,
    int    num_sweeps
```

placing `const double* __restrict__ betas,` directly after the `randoms` parameter (so the pointer params stay grouped, ABI order: h, J, samples, energies, randoms, betas, n, num_sweeps).

(b) Replace the in-loop beta computation

```c
        double beta;
        if (num_sweeps <= 1) {
            beta = beta_start;
        } else {
            beta = beta_start
                + (beta_end - beta_start)
                    * ((double)sweep / (double)(num_sweeps - 1));
        }
```

with

```c
        double beta = betas[sweep];
```

- [ ] **Step 4: Plumb the parameters through the host side.**

(a) `__init__` (line 193) — new signature and validation (mirror `SolverMetalGPU`'s ordering; add the schedule-type check after the strategy check at line 210, and store the new attributes):

```python
    def __init__(
        self,
        strategy: str = "sa",
        num_reads: int = 100,
        num_sweeps: int = 1000,
        num_sweeps_per_beta: int = 1,
        beta_range: tuple[float, float] | None = None,
        beta_schedule_type: str = "linear",
        seed: int | None = None,
    ) -> None:
```

after the existing strategy check:

```python
        if beta_schedule_type not in _SUPPORTED_SCHEDULES:
            raise ValueError(
                f"Unsupported beta_schedule_type {beta_schedule_type!r}. "
                f"Supported: {sorted(_SUPPORTED_SCHEDULES)}"
            )
```

and with the other attribute assignments:

```python
        self.num_sweeps_per_beta = num_sweeps_per_beta
        self.beta_schedule_type = beta_schedule_type
```

(b) `solve` (line 229) — read the kwargs alongside the existing ones:

```python
        num_sweeps_per_beta = kwargs.get("num_sweeps_per_beta", self.num_sweeps_per_beta)
        beta_schedule_type = kwargs.get("beta_schedule_type", self.beta_schedule_type)
```

after the existing `num_sweeps < 1` check add:

```python
        if beta_schedule_type not in _SUPPORTED_SCHEDULES:
            raise ValueError(
                f"Unsupported beta_schedule_type {beta_schedule_type!r}. "
                f"Supported: {sorted(_SUPPORTED_SCHEDULES)}"
            )
        if num_sweeps_per_beta < 1:
            raise ValueError(f"num_sweeps_per_beta must be >= 1, got {num_sweeps_per_beta}")
        num_betas, rem = divmod(num_sweeps, num_sweeps_per_beta)
        if rem != 0:
            raise ValueError(
                f"num_sweeps ({num_sweeps}) must be divisible by "
                f"num_sweeps_per_beta ({num_sweeps_per_beta})"
            )
```

pass both through the `_run_sa` call:

```python
        best_sample_dict, raw_energy = self._run_sa(
            model=model,
            h=h,
            j_matrix=j_matrix,
            num_reads=num_reads,
            num_sweeps=num_sweeps,
            num_sweeps_per_beta=num_sweeps_per_beta,
            beta_schedule_type=beta_schedule_type,
            beta_range=beta_range,
            seed=seed,
        )
```

and extend the metadata `params` dict (line 274) to:

```python
                "params": {
                    "strategy": strategy,
                    "num_sweeps": num_sweeps,
                    "num_sweeps_per_beta": num_sweeps_per_beta,
                    "num_betas": num_betas,
                    "beta_range": beta_range,
                    "beta_schedule_type": beta_schedule_type,
                    "raw_energy": raw_energy,
                },
```

(c) `_run_sa` (line 317) — make params keyword-only (it is already called with keywords) and add the two new ones:

```python
    def _run_sa(
        self,
        model: XQMX,
        *,
        h: cp.ndarray,
        j_matrix: cp.ndarray,
        num_reads: int,
        num_sweeps: int,
        num_sweeps_per_beta: int,
        beta_schedule_type: str,
        beta_range: tuple[float, float] | None,
        seed: int | None,
    ) -> tuple[dict[int, int], float]:
```

replace the two lines `beta_start, beta_end = beta_range` (line 339) with schedule construction (immediately after the `beta_range is None` auto-range block):

```python
        # Host-built per-sweep schedule: num_betas levels, each held for
        # num_sweeps_per_beta consecutive sweeps (divisibility validated
        # in solve). Replaces the retired in-kernel linear ramp.
        num_betas = num_sweeps // num_sweeps_per_beta
        betas_host = self._compute_beta_schedule(num_betas, beta_range, beta_schedule_type)
        beta_schedule = cupy.asarray(np.repeat(betas_host, num_sweeps_per_beta))
```

and change the kernel invocation args tuple (lines 370-380) to:

```python
            (
                h,
                j_matrix,
                samples,
                energies,
                randoms,
                beta_schedule,
                np.int32(n),
                np.int32(num_sweeps),
            ),
```

- [ ] **Step 5: Update the fake kernel in `mock_cupy_env` in lockstep.** In `_make_sa_kernel` (test_xqsa.py ~line 495), change the unpack line

```python
            h, J, samples, energies, randoms, n_val, ns_val, bs, be = args
```

to

```python
            h, J, samples, energies, randoms, betas, n_val, ns_val = args
```

delete the lines `beta_start = float(bs)` and `beta_end = float(be)`, and replace the in-loop block

```python
                for sweep in range(num_sweeps):
                    if num_sweeps <= 1:
                        beta = beta_start
                    else:
                        beta = beta_start + (beta_end - beta_start) * (sweep / (num_sweeps - 1))
```

with

```python
                for sweep in range(num_sweeps):
                    beta = float(betas[sweep])
```

- [ ] **Step 6: Run the full mocked class**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "CudaGPUMocked" -v`
Expected: ALL PASS — the 3 new tests, the extended `test_metadata_schema_mocked`, and every pre-existing mocked test (default path regression: same energies, since the default schedule is bit-identical).

- [ ] **Step 7: Commit**

```bash
git add xqsa/cuda_gpu.py xqsa/tests/test_xqsa.py
git commit -s -m "feat(xqsa): add num_sweeps_per_beta + beta_schedule_type to SolverCudaGPU"
```

---

### Task 3: Behavioral schedule-buffer tests (mocked)

**Files:**
- Test: `xqsa/tests/test_xqsa.py` (append inside `TestSolverCudaGPUMocked`)

**Interfaces:**
- Consumes: kernel ABI from Task 2 — the beta buffer is `args[5]` of the fake-kernel call; `_binary_kernel` is a `functools.cached_property`, so an instance-dict override shadows it.
- Produces: nothing for later tasks (leaf task).

- [ ] **Step 1: Write the tests** (they exercise already-implemented behavior; expected to pass — their value is regression):

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run the tests**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "CudaGPUMocked" -v`
Expected: ALL PASS (6 new + all prior). If any new test fails, the implementation from Task 2 has a real bug — debug it, do not weaken the test.

- [ ] **Step 3: Commit**

```bash
git add xqsa/tests/test_xqsa.py
git commit -s -m "test(xqsa): verify SolverCudaGPU beta buffer semantics via mocked kernel ABI"
```

---

### Task 4: Real-GPU tests, docstring, changelog

**Files:**
- Modify: `xqsa/tests/test_xqsa.py` — `class TestSolverCudaGPU` (real hardware, starts ~line 699): extend `test_default_params` / `test_custom_params`, append 3 tests
- Modify: `xqsa/cuda_gpu.py` — class docstring (lines 166-191)
- Modify: `CHANGELOG.md`

**Interfaces:**
- Consumes: public API from Task 2 (`num_sweeps_per_beta`, `beta_schedule_type`, metadata keys).
- Produces: nothing for later tasks.

- [ ] **Step 1: Extend the real-GPU param tests.** In `test_default_params` add after the `num_sweeps` assertion:

```python
        assert solver.num_sweeps_per_beta == 1
        assert solver.beta_schedule_type == "linear"
```

In `test_custom_params`, change the constructor call and add assertions:

```python
        solver = SolverCudaGPU(
            strategy="sa", num_reads=50, num_sweeps=500, num_sweeps_per_beta=5, seed=42
        )
        assert solver.num_reads == 50
        assert solver.num_sweeps == 500
        assert solver.num_sweeps_per_beta == 5
        assert solver.seed == 42
```

- [ ] **Step 2: Append the real-GPU behavior tests** (inside `TestSolverCudaGPU`, mirroring the Metal class pattern at test_xqsa.py:1663+ conventions):

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 3: Run the real-GPU suite locally (RTX 4090)**

Run: `uv run --extra cuda pytest xqsa/tests/test_xqsa.py -m cuda -v`
(If `cupy` is already in the venv, `uv run --no-sync pytest xqsa/tests/test_xqsa.py -m cuda -v`.)
Expected: ALL PASS, including every pre-existing real-GPU test (this compiles the modified kernels with NVRTC — it is the only step that proves the new CUDA source is valid). If NVRTC compilation fails, the kernel edit has a syntax error; fix `xqsa/cuda_gpu.py`, not the test.

- [ ] **Step 4: Update the class docstring.** In `xqsa/cuda_gpu.py`, after the sentence ending "its own CUDA thread block." (line 170) insert:

```
    ``num_sweeps_per_beta`` holds each inverse-temperature (beta) for that
    many consecutive sweeps, decoupling the number of temperature levels
    from the total sweep count: ``num_betas = num_sweeps //
    num_sweeps_per_beta`` (``num_sweeps`` must be divisible by it). The
    default of 1 keeps one beta per sweep, bit-identical to the previous
    behaviour. When ``num_betas == 1`` the whole run anneals at
    ``beta_start`` (hottest), matching ``SolverMetalGPU``.

    ``beta_schedule_type`` selects the schedule shape: ``"linear"`` (the
    default, preserving the historical CUDA ramp) or ``"geometric"``.
    Note: ``SolverMetalGPU`` defaults to ``"geometric"``.
```

and extend the `Raises:` section line for ValueError to:

```
        ValueError: if ``strategy`` or ``beta_schedule_type`` is not supported.
```

- [ ] **Step 5: Add the changelog entry.** In `CHANGELOG.md`, insert directly above `## [0.3.0] - 2026-07-13`:

```markdown
## [Unreleased]

### Added
- **xqsa**: Add `num_sweeps_per_beta` and `beta_schedule_type` to SolverCudaGPU (QUI-854)
```

(If an `## [Unreleased]` section already exists by then, add the bullet to its `### Added` list instead.)

- [ ] **Step 6: Re-run mocked suite + commit**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "CudaGPU" -v`
Expected: ALL PASS.

```bash
git add xqsa/cuda_gpu.py xqsa/tests/test_xqsa.py CHANGELOG.md
git commit -s -m "test(xqsa): cover SolverCudaGPU schedule params on real GPU; document"
```

---

### Task 5: Preflight and merge request

**Files:**
- No new files; runs checks and pushes.

**Interfaces:**
- Consumes: everything above, all committed on `feature/qui-854`.

- [ ] **Step 1: Full Python preflight**

Run: `make preflight-py` (from `/home/konrad/Quip/xquad`)
Expected: ruff format clean, ruff lint clean, pytest green (~725 passed, hardware-marked tests deselected). Fix any formatting the tooling flags, amend nothing — new commit `style(xqsa): appease ruff` only if needed.

- [ ] **Step 2: Push and open the MR**

```bash
git push -u origin feature/qui-854
glab mr create --title "feat(xqsa): add num_sweeps_per_beta and beta_schedule_type to SolverCudaGPU" \
  --description-file <path-to-description> --assignee kleczkowski
```

MR description must state: divide semantics matching QUI-685; kernel ABI change (scalars → per-sweep `betas` buffer at arg index 5); bit-identical default (linear schedule computed with the retired formula's exact float64 expression, verified by `test_default_schedule_bit_identical_mocked`); CUDA keeps `"linear"` default vs Metal's `"geometric"` and why; random buffer + OOM guard unchanged; testing evidence (mocked suite + real RTX 4090 run counts); "Implements QUI-854". Fill the repo's MR checklist honestly (preflight-py checked; preflight-rs/parity N/A; real-GPU `-m cuda` run results).

- [ ] **Step 3: Verify pipeline starts**

Run: `glab mr view feature/qui-854` and confirm the MR exists and CI is running.

---

## Self-review (done at plan time)

- Spec coverage: all five QUI-854 acceptance criteria have tasks (plumbing → Task 2; default reproduces output → Tasks 1-3 bit-identical tests; metadata → Task 2; mocked + real tests → Tasks 2-4; fuller option `beta_schedule_type` → Tasks 2-4). ✓
- No placeholders; every code step shows the code. ✓
- Type consistency: `_compute_beta_schedule` signature identical across Tasks 1/2; buffer at args index 5 consistent across Tasks 2/3; metadata keys consistent across Tasks 2/3/4. ✓

---

### Design Plans 2026 07 17 Qui 705 Cuda On Device Rng (`docs/design/plans/2026-07-17-qui-705-cuda-on-device-rng.md`)

# QUI-705: CUDA On-Device RNG Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace `SolverCudaGPU`'s pre-generated `(num_reads, num_sweeps, n)` float64 acceptance-randoms buffer with an on-device per-replica xorshift64 RNG (splitmix64-seeded), removing the buffer allocation and its 80%-of-VRAM OOM guard, matching `SolverMetalGPU`'s strategy.

**Architecture:** A CUDA prelude (`splitmix64` + `seed_for_replica` + `xorshift64` + 53-bit `rand_unit`) is prepended to both SA kernels, mirroring Metal's `_RNG_PRELUDE` structure but with 64-bit state so acceptance stays float64 (per the MR !88 review decision — Metal's float32 is an MSL platform limit, not a precision choice). Kernels drop the `randoms` pointer and take a `base_seed` scalar; the host drops the buffer allocation and OOM guard. Since the sampling stream changes, validation is distributional (energy-distribution parity + best-energy regression), never exact-trajectory.

**Tech Stack:** Python 3 + CuPy RawKernel (CUDA C), numpy-mocked CuPy for CI tests, pytest.

## Global Constraints

- **Base branch: `origin/feature/qui-852`** (NOT main). QUI-705 touches the same kernels as QUI-852 (MR !92, in review, stacked on !91→!90). Branch name: `feature/qui-705`. MR target: `feature/qui-852`. GitLab will retarget as the stack merges.
- **Worktree:** create via superpowers:using-git-worktrees at `.claude/worktrees/qui-705`. Subagents pin to their launch dir — every Bash command in a subagent must start with `cd /home/konrad/Quip/xquad/.claude/worktrees/qui-705 &&` (path may differ if the skill picks another location; use the actual one).
- **Worktree env setup (known gotchas from QUI-852):** run `make deps-py` first (writes `xq-rs-workspace.pth`; bare `uv sync` does not). Then `uv sync --extra cuda && make deps-py` (re-sync installs cupy but clobbers the workspace install; the second `deps-py` repairs it). After that, ALWAYS `uv run --no-sync ...` — a plain `uv run` re-syncs and removes cupy.
- **GPU:** local RTX 4090 (WSL2). Verify with `uv run --no-sync python -c "import cupy; print(cupy.cuda.runtime.getDeviceCount())"` → `1`.
- **Commit after every file-modification step** (user's standing preference), imperative mood, ≤72-char subject, no co-author bylines.
- New/edited Python files: AGPL header block (copy verbatim from `scripts/bench_spin_packing.py` lines 1–16), 100-char lines, ≤100-line functions, Google-style docstrings on public APIs. `make fmt-py lint-py` must be clean before each commit.
- Decisions already settled in the Linear ticket (do not re-litigate): float64 acceptance on-device; splitmix64 per-replica seeding over `(base_seed, rid)`; distributional success criterion.
- Decisions taken in this plan (flag in MR description): xorshift64 as the stream generator (simplest 64-bit analogue of Metal's xorshift32; statistical quality is ample for SA acceptance); `seed=None` draws a random `base_seed` from `np.random.default_rng(None)`; parity thresholds KS α=0.01, mean-shift ≤2%, best-energy ≤2% (2% mirrors `GLASS_TOLERANCE` in `xqsa/tests/test_gpu_validation.py:68`).

---

### Task 1: Distributional-parity harness

**Files:**
- Create: `scripts/validate_rng_parity.py`

**Interfaces:**
- Produces: CLI `run --out <json> [--smoke]` and `compare <baseline.json> <candidate.json>` (exit 0 = parity holds, 1 = failed). Task 2 runs `run` on the unmodified solver; Task 6 runs `run` + `compare` on the candidate.
- Consumes: `build_instance(n, seed)` from `scripts/bench_spin_packing.py` (returns `(XQMX, h, J)`; only the model is used).

- [ ] **Step 1: Write the harness**

Create `scripts/validate_rng_parity.py` with the AGPL header (copy lines 1–16 of `scripts/bench_spin_packing.py`), then:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Sanity-check the harness on the unmodified solver (needs the GPU)**

Run: `uv run --no-sync python scripts/validate_rng_parity.py run --smoke --out scratch/qui-705/smoke_a.json`
Expected: one `n=64 instance=7: best=... mean=...` line, `wrote scratch/qui-705/smoke_a.json`, exit 0.

Run it a second time to `scratch/qui-705/smoke_b.json`, then:
`uv run --no-sync python scripts/validate_rng_parity.py compare scratch/qui-705/smoke_a.json scratch/qui-705/smoke_b.json`
Expected: `PASS: 1 cells within KS/mean/best-energy thresholds`, exit 0. (Identical seeds on the same code give identical energies — D=0.)

- [ ] **Step 3: Lint and commit**

Run: `make fmt-py lint-py`
Expected: no diffs, no warnings.

```bash
git add scripts/validate_rng_parity.py
git commit -m "test(xqsa): add RNG distributional-parity harness for QUI-705"
```

---

### Task 2: Capture the pre-change baseline (GPU, before ANY solver edit)

The baseline must come from the unmodified solver. Do not start Task 3/4 until this task's JSON files exist.

**Files:**
- Create (not committed; `scratch/` is untracked): `scratch/qui-705/baseline.json`, `scratch/qui-705/sweep_baseline.jsonl`

- [ ] **Step 1: Full parity baseline**

Run: `uv run --no-sync python scripts/validate_rng_parity.py run --out scratch/qui-705/baseline.json`
Expected: 6 cell lines (2 sizes × 3 instances, 24 solves each) and `wrote scratch/qui-705/baseline.json`. Order minutes on the RTX 4090 with the QUI-852 kernels.

- [ ] **Step 2: QUI-167 sweep baseline (best-energy regression reference)**

```bash
mkdir -p scratch/qui-705
for n in 128 512 1024; do for s in 1 2 3; do
  uv run --no-sync python scripts/scale_sweep.py --point --problem maxcut \
    --backend rust --solver cuda-gpu --n $n --seed $s >> scratch/qui-705/sweep_baseline.jsonl
done; done
for n in 16 32; do for s in 1 2 3; do
  uv run --no-sync python scripts/scale_sweep.py --point --problem tsp \
    --backend rust --solver cuda-gpu --n $n --seed $s >> scratch/qui-705/sweep_baseline.jsonl
done; done
```

Expected: 15 JSON lines, each with `"status": "ok"` and an `energy` field. Verify: `wc -l scratch/qui-705/sweep_baseline.jsonl` → `15`; `rg -c '"status": "ok"' scratch/qui-705/sweep_baseline.jsonl` → `15`.

---

### Task 3: Update the mocked-CuPy tests to the new kernel contract (TDD red)

**Files:**
- Modify: `xqsa/tests/test_xqsa.py` (fixture `mock_cupy_env` ~line 458; `TestSolverCudaGPUMocked` ~line 562)

**Interfaces:**
- Consumes: nothing from other tasks (this is the failing-test step).
- Produces: the kernel-call contract Task 4 implements — args tuple `(h, J, samples, energies, betas, n, num_sweeps, base_seed)` with `base_seed` an `np.uint64`; Python RNG mirrors `_splitmix64`, `_seed_for_replica`, `_xorshift64`, `_rand_unit` whose constants Task 4's CUDA prelude must match bit-for-bit.

- [ ] **Step 1: Add Python mirrors of the device RNG above the `mock_cupy_env` fixture**

```python
# Python mirrors of the CUDA device RNG in xqsa.cuda_gpu._RNG_PRELUDE
# (QUI-705). Constants and update order must match the kernel bit-for-bit;
# the fake kernel below consumes these so the mocked solve reproduces the
# device sampling stream exactly.
_MASK64 = (1 << 64) - 1


def _splitmix64(x: int) -> int:
    x = (x + 0x9E3779B97F4A7C15) & _MASK64
    x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & _MASK64
    x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & _MASK64
    return x ^ (x >> 31)


def _seed_for_replica(base_seed: int, rid: int) -> int:
    s = _splitmix64(base_seed ^ ((0x9E3779B97F4A7C15 * (rid + 1)) & _MASK64))
    return s if s != 0 else 1


def _xorshift64(state: int) -> int:
    x = state
    x ^= (x << 13) & _MASK64
    x ^= x >> 7
    x ^= (x << 17) & _MASK64
    return x


def _rand_unit(state: int) -> tuple[float, int]:
    state = _xorshift64(state)
    return (state >> 11) / 9007199254740992.0, state
```

- [ ] **Step 2: Rewrite the fake kernel to the new signature and stream**

In `_make_sa_kernel`'s inner `_kernel`, replace the unpack line

```python
h, J, samples, energies, randoms, betas, n_val, ns_val = args
```

with

```python
h, J, samples, energies, betas, n_val, ns_val, base_seed = args
```

Inside the `for rid in range(num_reads):` loop, immediately after `x = samples[rid]`, add:

```python
rng_state = _seed_for_replica(int(base_seed), rid)
```

and replace the acceptance draw

```python
r = randoms[rid, sweep, i]
```

with

```python
r, rng_state = _rand_unit(rng_state)
```

- [ ] **Step 3: Delete `test_oom_guard`**

Remove the whole `test_oom_guard` method (currently `xqsa/tests/test_xqsa.py:660-680` on the base branch) — the guard it exercises is deleted in Task 4. Also remove the now-unused `_TinyDevice`-free leftovers if any (the class is local to the test; deleting the method removes it all).

- [ ] **Step 4: Re-index the captured-args tests from `args[5]` to `args[4]`**

With `randoms` gone, the betas buffer moves from index 5 to index 4. In these four tests, change `captured["args"][5]` → `captured["args"][4]`:
- `test_num_sweeps_per_beta_schedule_buffer_mocked`
- `test_default_schedule_bit_identical_mocked`
- `test_single_beta_level_uses_beta_start_mocked`
- `test_geometric_schedule_shape_mocked`

- [ ] **Step 5: Add seed-semantics tests to `TestSolverCudaGPUMocked`**

```python
    def test_same_seed_reproducible_mocked(self, mock_cupy_env) -> None:
        """Identical seeds give identical best energy and sample (QUI-705)."""
        from xqsa.cuda_gpu import SolverCudaGPU as _Solver

        model = XQMX.spin_model(6)
        model.set_quadratic(0, 1, 1.0)
        model.set_quadratic(1, 2, -1.0)
        model.set_quadratic(2, 3, 1.0)
        model.set_quadratic(3, 4, -1.0)
        model.set_quadratic(4, 5, 1.0)
        model.set_quadratic(0, 5, 1.0)

        r1 = _Solver(num_reads=4, num_sweeps=30, seed=7).solve(model)
        r2 = _Solver(num_reads=4, num_sweeps=30, seed=7).solve(model)

        assert r1.energy == r2.energy
        assert [r1.sample.get_linear(i) for i in range(6)] == [
            r2.sample.get_linear(i) for i in range(6)
        ]

    def test_seed_none_solves_mocked(self, mock_cupy_env) -> None:
        """seed=None draws a random base seed and still solves (QUI-705)."""
        from xqsa.cuda_gpu import SolverCudaGPU as _Solver

        model = XQMX.binary_model(2)
        model.set_linear(0, 1.0)
        model.set_linear(1, 1.0)

        result = _Solver(num_reads=10, num_sweeps=100, seed=None).solve(model)

        assert result.energy == 0
        assert result.metadata["seed"] is None
```

- [ ] **Step 6: Run the CUDA-mocked tests to verify they fail against the old implementation**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -k "Cuda" -x -q`
Expected: FAIL. The old `_run_sa` still passes 8 old-layout args, so the fake kernel binds the 3-D `randoms` array to `betas` and `float(betas[sweep])` raises `TypeError` (or similar unpack/shape error). The point is red-before-green, not the specific exception.

- [ ] **Step 7: Commit the red tests**

```bash
git add xqsa/tests/test_xqsa.py
git commit -m "test(xqsa): retarget mocked CUDA tests at on-device RNG contract"
```

(Repo hooks permitting a red suite; if a pre-commit hook runs pytest and blocks, commit Tasks 3+4 together at the end of Task 4 instead and note it in the MR.)

---

### Task 4: Implement the on-device RNG in `SolverCudaGPU` (TDD green)

**Files:**
- Modify: `xqsa/cuda_gpu.py` (kernel sources ~lines 57–192; `_run_sa` ~lines 415–496; class docstring ~line 196)

**Interfaces:**
- Consumes: the args-tuple contract and RNG constants from Task 3.
- Produces: `SolverCudaGPU` with unchanged Python API (`seed=` still reproducible, `seed=None` still random); no `randoms` buffer, no OOM guard.

- [ ] **Step 1: Add the RNG prelude and rewrite both kernel headers**

In `xqsa/cuda_gpu.py`, above `_SA_BINARY_KERNEL`, add:

```python
# Device RNG (QUI-705): mirrors SolverMetalGPU's on-device strategy
# (xqsa/metal_gpu.py _RNG_PRELUDE) with 64-bit state so acceptance draws
# stay float64 (53-bit uniform). Metal's float32 is an MSL platform limit,
# not a precision decision. Python mirrors live in tests/test_xqsa.py;
# keep constants in sync bit-for-bit.
_RNG_PRELUDE = r"""
typedef unsigned long long u64;

__device__ __forceinline__ u64 splitmix64(u64 x) {
    x += 0x9E3779B97F4A7C15ULL;
    x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
    x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
    return x ^ (x >> 31);
}

/* Per-replica stream seed: Metal's seed_for_replica shape (base ^ odd
   constant * (rid+1)) widened to 64 bits, finalized through splitmix64
   for stream decorrelation. xorshift64 has a zero fixed point, so never
   return 0. */
__device__ __forceinline__ u64 seed_for_replica(u64 base_seed, int rid) {
    u64 s = splitmix64(base_seed ^ (0x9E3779B97F4A7C15ULL * (u64)(rid + 1)));
    return (s == 0ULL) ? 1ULL : s;
}

__device__ __forceinline__ u64 xorshift64(u64 &state) {
    u64 x = state;
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    state = x;
    return x;
}

/* Uniform double in [0, 1): top 53 bits / 2^53. */
__device__ __forceinline__ double rand_unit(u64 &state) {
    return (double)(xorshift64(state) >> 11) * (1.0 / 9007199254740992.0);
}
"""
```

Change both kernel definitions to prepend it:

```python
_SA_BINARY_KERNEL = _RNG_PRELUDE + r"""
extern "C" __global__
void sa_binary(
...
```

(same for `_SA_SPIN_KERNEL`).

In **both** kernel signatures, delete the line

```c
    const double* __restrict__ randoms,
```

and add after `int    num_sweeps`:

```c
    unsigned long long base_seed
```

(mind the comma on the previous line).

- [ ] **Step 2: Swap the in-kernel draw (both kernels)**

In `sa_binary`, replace

```c
    const double* rand_ptr = randoms + (long long)rid * num_sweeps * n;
```

with

```c
    u64 rng = seed_for_replica(base_seed, rid);
```

and replace

```c
                double r = rand_ptr[(long long)sweep * n + i];
```

with

```c
                double r = rand_unit(rng);
```

Same two edits in `sa_spin` (its pointer line reads `randoms + (long long)rid * num_sweeps * n` with `const double* rand_ptr =` as well). Note the state variable is declared per-thread but only thread 0 ever draws from it (the acceptance block is `if (tid == 0)`), so the stream is strictly sequential per replica — same property the `randoms` indexing had. Update the comment block at the top of `sa_binary` that mentions "same RNG consumption order": the guarantee is now "thread 0 owns the per-replica RNG stream".

- [ ] **Step 3: Host-side `_run_sa` changes**

Delete the OOM guard and buffer allocation (the whole block):

```python
        # Pre-generate all random acceptance thresholds.
        # Memory: num_reads * num_sweeps * n * 8 bytes (float64).
        random_bytes = num_reads * num_sweeps * n * 8
        gpu_free = cupy.cuda.Device().mem_info[0]
        if random_bytes > gpu_free * 0.8:
            raise ValueError(
                f"Random buffer requires {random_bytes / 1e9:.1f} GB "
                f"GPU memory ({gpu_free / 1e9:.1f} GB free). "
                f"Reduce num_reads or num_sweeps."
            )
        randoms = rng.random(size=(num_reads, num_sweeps, n), dtype=cupy.float64)
```

Replace it with:

```python
        # Acceptance randomness is generated on-device per replica
        # (QUI-705); only a 64-bit base seed crosses the host boundary.
        # Derived from an independent numpy stream so the cupy `rng` used
        # for replica init keeps its historical consumption order.
        base_seed = np.uint64(np.random.default_rng(seed).integers(0, 2**64, dtype=np.uint64))
```

Update the kernel launch args from

```python
            (
                h,
                j_matrix,
                samples,
                energies,
                randoms,
                beta_schedule,
                np.int32(n),
                np.int32(num_sweeps),
            ),
```

to

```python
            (
                h,
                j_matrix,
                samples,
                energies,
                beta_schedule,
                np.int32(n),
                np.int32(num_sweeps),
                base_seed,
            ),
```

- [ ] **Step 4: Docstring/comment sweep in `cuda_gpu.py`**

- Class docstring: add one sentence after the QUI-852 paragraph: "Acceptance randomness is generated on-device per replica (xorshift64 seeded via splitmix64 from a 64-bit base seed), so no `(num_reads, num_sweeps, n)` buffer is allocated (QUI-705)."
- `solve()` Raises section: the OOM `ValueError` is gone; the remaining `ValueError` causes (domain, parameter validation) still hold — no text change needed unless it mentions memory. Check with `rg -n "GPU memory|Reduce num_reads" xqsa/cuda_gpu.py` → no hits after the edit.

- [ ] **Step 5: Run the mocked suite to green**

Run: `uv run --no-sync pytest xqsa/tests/test_xqsa.py -q`
Expected: PASS, zero failures, zero warnings. If a mocked-solve energy assertion fails (e.g. `test_solve_trivial_binary_mocked` no longer reaches ground state), the mirrors are out of sync with the prelude — diff the constants/shift orders between `_RNG_PRELUDE` and the test mirrors before touching anything else; trivial 2-var models converge under any healthy uniform stream.

- [ ] **Step 6: Lint, then commit**

Run: `make fmt-py lint-py`
Expected: clean.

```bash
git add xqsa/cuda_gpu.py
git commit -m "refactor(xqsa): move CUDA SA acceptance RNG on-device (QUI-705)"
```

---

### Task 5: Real-GPU test validation

**Files:** none modified (verification only).

- [ ] **Step 1: Real-kernel compile + medium-model validation on the RTX 4090**

Run: `CI=1 uv run --no-sync pytest xqsa/tests/test_gpu_validation.py -m cuda -q`
(`CI=1` forces hard-fail instead of silent skip if the env is broken.)
Expected: `TestCudaGPUMediumModels` — 4 passed. These are distributional assertions (exact known optima + 2% glass tolerance), so they must pass unchanged; a failure here is a real quality regression, not an expected stream change.

- [ ] **Step 2: Confirm the buffer is actually gone**

Run:
```bash
uv run --no-sync python - <<'EOF'
import cupy
from scripts_path_shim import *  # noqa: F401,F403  (not needed; see below)
EOF
```
Use this instead (single command):
```bash
uv run --no-sync python -c "
import sys; from pathlib import Path
sys.path.insert(0, 'scripts')
import cupy
from bench_spin_packing import build_instance
from xqsa.cuda_gpu import SolverCudaGPU
model, _, _ = build_instance(1024, 7)
cupy.get_default_memory_pool().free_all_blocks()
SolverCudaGPU(num_reads=64, num_sweeps=500, beta_range=(0.1, 10.0), seed=42).solve(model)
print('mempool peak MiB:', cupy.get_default_memory_pool().total_bytes() / 2**20)
"
```
Expected: peak well under 100 MiB. The retired randoms buffer alone was 64·500·1024·8 B = 250 MiB at these parameters (and QUI-704 measured it at 88.6–98.3% of the footprint), so a sub-100 MiB peak proves the allocation is gone. Record the number for the MR description.

---

### Task 6: Distributional parity + QUI-167 regression (the ticket's success criterion)

**Files:**
- Create: `scratch/qui-705/candidate.json`, `scratch/qui-705/sweep_candidate.jsonl` (not committed)
- Create: `docs/design/specs/2026-07-17-qui-705-findings.md`

- [ ] **Step 1: Candidate parity run**

Run: `uv run --no-sync python scripts/validate_rng_parity.py run --out scratch/qui-705/candidate.json`
Expected: 6 cells, exit 0.

- [ ] **Step 2: Compare against the Task 2 baseline**

Run: `uv run --no-sync python scripts/validate_rng_parity.py compare scratch/qui-705/baseline.json scratch/qui-705/candidate.json`
Expected: `PASS: 6 cells within KS/mean/best-energy thresholds`, exit 0.
If a cell fails: this is the ticket's gate — do NOT loosen thresholds to pass. Diagnose with superpowers:systematic-debugging (suspects, in order: mirror/prelude constant drift, per-replica seed collisions across `rid`, biased `rand_unit`).

- [ ] **Step 3: QUI-167 sweep candidate points + delta check**

```bash
for n in 128 512 1024; do for s in 1 2 3; do
  uv run --no-sync python scripts/scale_sweep.py --point --problem maxcut \
    --backend rust --solver cuda-gpu --n $n --seed $s >> scratch/qui-705/sweep_candidate.jsonl
done; done
for n in 16 32; do for s in 1 2 3; do
  uv run --no-sync python scripts/scale_sweep.py --point --problem tsp \
    --backend rust --solver cuda-gpu --n $n --seed $s >> scratch/qui-705/sweep_candidate.jsonl
done; done
uv run --no-sync python -c "
import json
def best(path):
    rows = [json.loads(l) for l in open(path)]
    return {(r['problem'], r['n'], r['seed']): r['energy'] for r in rows}
b, c = best('scratch/qui-705/sweep_baseline.jsonl'), best('scratch/qui-705/sweep_candidate.jsonl')
bad = []
for k in sorted(b):
    tol = 0.02 * abs(b[k])
    flag = '  REGRESSION' if c[k] > b[k] + tol else ''
    print(k, 'base', b[k], 'cand', c[k], flag)
    if flag: bad.append(k)
raise SystemExit(1 if bad else 0)
"
```
Expected: 15 comparison lines, no `REGRESSION` flags, exit 0. (If a `--point` record lacks an `energy` key, inspect one line of the baseline file and adjust the key in this snippet — the baseline from Task 2 defines the schema; both files share it.)

- [ ] **Step 4: Write the findings doc**

Create `docs/design/specs/2026-07-17-qui-705-findings.md` recording: the mempool peak before/after (Task 5 step 2 number vs QUI-704's 250 MiB-at-n=1024 / 1 GiB-at-n=4096 figures), the 6-cell parity table (baseline vs candidate mean/best per cell, KS D values), the 15-point sweep delta table, and the seed-semantics statement ("`seed=` reproducible, streams decorrelated per replica; sampling stream intentionally differs from the pre-QUI-705 solver"). Follow the structure of `docs/design/specs/2026-07-16-qui-852-findings.md`.

- [ ] **Step 5: Commit**

```bash
git add docs/design/specs/2026-07-17-qui-705-findings.md
git commit -m "docs(xqsa): record QUI-705 RNG parity validation findings"
```

---

### Task 7: Changelog, MR, Linear

**Files:**
- Modify: `CHANGELOG.md` (follow the existing unreleased-section format — check `git log -p -1 --follow CHANGELOG.md` for the house style)

- [ ] **Step 1: Changelog entry**

Add under the unreleased/v0.3.1 section, matching surrounding entry style:

```markdown
- `SolverCudaGPU` now generates acceptance randomness on-device (per-replica
  xorshift64, splitmix64-seeded), removing the `(num_reads, num_sweeps, n)`
  float64 buffer and its GPU-memory guard. Seeded solves remain reproducible;
  the sampling stream differs from previous releases (QUI-705).
```

- [ ] **Step 2: Full preflight + commit**

Run: `make preflight-py`
Expected: clean.

```bash
git add CHANGELOG.md
git commit -m "docs: changelog for QUI-705 on-device CUDA RNG"
```

- [ ] **Step 3: Push and open the MR (stacked)**

```bash
git push -u origin feature/qui-705
glab mr create --source-branch feature/qui-705 --target-branch feature/qui-852 \
  --title "refactor(xqsa): move CUDA SA acceptance RNG on-device (QUI-705)" \
  --description "<body>"
```

MR body content (plain factual language, no superlatives): what changed (kernel prelude, signature, host allocation removal), the validation evidence (parity PASS output, sweep delta table, mempool peak numbers), the intentional behavior change (sampling stream differs; `seed=` reproducibility preserved), and the stacking note (targets `feature/qui-852`; retarget to main as !91/!92 merge). Note the plan-level decisions (xorshift64 choice, thresholds) for reviewer sign-off.

- [ ] **Step 4: Linear**

Move QUI-705 to "In Review" and comment with the MR link and the parity/regression verdicts (use `save_issue`/`save_comment` via the Linear MCP tools).

---

## Self-Review Notes

- Ticket requirement → task mapping: on-device per-replica RNG (Task 4), remove OOM guard + allocation (Task 4 step 3, proven in Task 5 step 2), float64 acceptance kept (prelude `rand_unit` returns 53-bit doubles), splitmix64 per-replica seeding (prelude `seed_for_replica`), seed semantics confirmed (Task 3 step 5 tests + Task 6 step 4 statement), CUDA tests updated (Task 3), energy-distribution parity across seeds × instances with the bench generator (Tasks 1, 2, 6), QUI-167 best-energy regression (Tasks 2, 6), sequencing with QUI-852 (Global Constraints: stacked branch).
- Type/order consistency: kernel args order `(h, J, samples, energies, betas, n, num_sweeps, base_seed)` is identical in Task 3 step 2 (mock unpack), Task 4 step 3 (launch), and Task 3 step 4 (betas at index 4). RNG constants appear in exactly two places (prelude, test mirrors) and are byte-identical.
- Known risk: the mocked fake kernel is O(reads·sweeps·n²) pure Python — the new tests use tiny models (n≤6, ≤30 sweeps) to stay fast.

---

### Design Plans 2026 08 24 Qui 1056 Step Metering (`docs/design/plans/2026-08-24-qui-1056-step-metering.md`)

# QUI-1056 Step Metering Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make one XQVM step a bounded amount of work, so `pallet-xqvm`'s `WeightPerStep * steps` price is honest for programs that touch a model.

**Architecture:** The VM keeps two counters: `instructions` (the ordinal a tracer reports) and `steps` (metered cost units, what `step_limit` bounds and what the pallet prices). Every instruction charges a base cost of 1 before dispatch, exactly as today; the handlers whose work scales with program-controlled data charge additional units *before* doing that work, through a `charge_steps` helper that mirrors the existing memory `charge`. Cost constants and the pure cost functions live in a new `xqvm/src/metering.rs`, mirrored in `xqvm_py/metering.py`, kept in lockstep by a parity script, and specified normatively in `spec/xqvm/METERING.md`.

**Tech Stack:** Rust 2024 (`xqvm`, `xqffi`, `xquad-conformance`), Python 3.13+ (`xqvm_py`), cargo-nextest, pytest, mdbook.

**Spec:** [QUI-1056](https://linear.app/quip-network/issue/QUI-1056/fixxqvm-energy-and-the-other-omodel-opcodes-cost-one-step) plus the Design section below. Task 9 lands the normative specification at `spec/xqvm/METERING.md`; until then this document is the spec.

**Base:** the tree this plan was written against, before QUI-1056 landed. Every line number in this plan is from that tree and is not maintained as those files move; resolve a reference by the name it cites rather than by its line.

---

## Design

### The problem

`xqvm/src/vm.rs:515` charges one step per instruction:

```rust
if self.steps >= self.step_limit {
    return Err(Error::StepLimitExceeded { limit: self.step_limit });
}
self.steps += 1;
```

`ENERGY` (`vm.rs:2414`) clones the sample vector and walks every coefficient in the model, and pays one step. `ONEHOTR` writes `cols + cols*(cols-1)/2` coefficients in one step; `ONEHOTC` the same in `rows`; `EQUALITY`, `ATLEAST` and `ATLEASTW` the same in the term count. `ITER`, `LVAL`, `INPUT` and `OUTPUT` clone whole registers, and a register can hold a model. So the ratio between the cheapest step (`NOP`) and the dearest has no bound below the memory limit, and a flat per-step price is wrong by that factor.

The memory budget (`charge`, `charge_variables`, `charge_coefficient`, `charge_equality_expansion` at `vm.rs:670`ff, mirrored in `xqvm_py/executor.py:338`ff) already bounds the *space* those opcodes take, with the charge-before-you-allocate discipline and a Rust/Python-identical formula. This plan applies the same shape to *time*.

### The model

- **Two counters.** `instructions` counts dispatches and is what a `Tracer` sees. `steps` counts cost units, is what `set_step_limit` bounds, and is what an embedder prices. Today they are the same field; splitting them keeps traces readable once costs stop being 1.
- **Base cost 1.** Charged before dispatch, unchanged, so a straight-line arithmetic program's step count does not move.
- **Extra units charged before the work.** A handler computes the size of the work it is about to do, calls `charge_steps`, and only then does it. A program that cannot pay performs none of the work, exactly like an allocation it cannot afford.
- **Units are relative to `NOP`.** One unit is one `NOP` dispatch, rounded so that a unit is never cheaper than the operation it stands for. Task 2 measures the ratios; Task 3 freezes them as named constants.
- **Integer arithmetic, saturating throughout.** The counts are consensus-visible, so no floats and no platform-dependent rounding. `usize` widths are converted with `u64::try_from(..).unwrap_or(u64::MAX)`, matching `charge_variables`.

### Cost table

`B` = base cost 1, charged for every instruction. Everything below is *in addition* to `B`.

| Opcode(s) | Extra units | Constant(s) |
|---|---|---|
| `ENERGY` | `SAMPLE_COPY_STEPS * sample.len() + MODEL_TERM_STEPS * (linear_len + quadratic_len)` | both |
| `SETLINE`, `ADDLINE`, `SETQUAD`, `ADDQUAD` | `COEFF_WRITE_STEPS`, only when the register holds a model | `COEFF_WRITE_STEPS` |
| `ONEHOTR` | `equality_expansion_steps(cols)` | `COEFF_WRITE_STEPS` |
| `ONEHOTC` | `equality_expansion_steps(rows)` | `COEFF_WRITE_STEPS` |
| `EQUALITY` | `equality_expansion_steps(n)` where `n = idx_vec.len()` | `COEFF_WRITE_STEPS` |
| `ATLEAST`, `ATLEASTW` | `equality_expansion_steps(n + num_slacks)` | `COEFF_WRITE_STEPS` |
| `BSMX`, `SSMX`, `XSMX` | `SAMPLE_COPY_STEPS * size` (the buffer is filled) | `SAMPLE_COPY_STEPS` |
| `BQMX`, `SQMX`, `XQMX` | none: an empty model allocates no coefficients | -- |
| `RANGE` | `ELEMENT_COPY_STEPS * count` | `ELEMENT_COPY_STEPS` |
| `ITER` over `vec<int>` | `ELEMENT_COPY_STEPS * slice.len()` | `ELEMENT_COPY_STEPS` |
| `ITER` over `vec<xqmx>` | `value_copy_steps` summed over the sliced models | both |
| `LVAL`, `INPUT`, `OUTPUT` | `value_copy_steps(value)` | both |
| `SLACK` | `ELEMENT_COPY_STEPS * 2 * entries` | `ELEMENT_COPY_STEPS` |
| everything else | none | -- |

with

```
equality_expansion_steps(n) = (n + n*(n-1)/2) * COEFF_WRITE_STEPS

value_copy_steps(Unset | Int)   = 0
value_copy_steps(VecInt(v))     = ELEMENT_COPY_STEPS * v.len()
value_copy_steps(Sample(s))     = SAMPLE_COPY_STEPS * s.values.len()
value_copy_steps(Model(m))      = COEFF_WRITE_STEPS * (m.linear_len() + m.quadratic_len())
value_copy_steps(VecXqmx(v))    = sum of value_copy_steps over the models
```

`equality_expansion_steps` counts the same worst case as `equality_expansion_bytes` (`vm.rs:242`): `n` linear terms and one quadratic term per unordered pair. Repeated indices collide on one map key, so an expansion can write fewer entries than it is charged for -- the count bounds the work, it does not measure it.

### Consequences

- Step counts become observable and normative: `xqvm` and `xqvm_py` must produce identical counts for identical programs, and conformance asserts it.
- Existing programs that touch a model get larger step counts. This is a behavioural break; it lands in 0.4.0 with QUI-1019.
- `Error::StepLimitExceeded` gains the position and the refused charge, so a fault is diagnosable the way `MemoryLimitExceeded` already is.
- QUI-1054 can then calibrate a flat `WeightPerStep` against `NOP` plus a safety multiplier, because a step is now bounded work.

### Out of scope

- Changing the default step limit or `set_step_limit(0)` semantics (QUI-1053, already fixed).
- Pallet-side weight calibration (QUI-1054, QUI-1013) -- this plan only makes the unit meaningful.
- The stale `set_step_limit(0)` prose in `docs/book/src/xqvm/limits-and-errors.md:36` and `execution.md:53`, which predates QUI-1053.

## Global Constraints

- Every new source file starts with the AGPL header from `AGENTS.md` (`//` for Rust, `#` for Python).
- Conventional Commits, imperative, lowercase subject, max 72 chars, scope = crate/package name. Sign off with `git commit -s`. Never add AI co-author trailers.
- No emojis. No em-dashes in prose -- use `--`. Four spaces, no tabs.
- Rust: no `unwrap()`, no indexing/slicing (`.get()`/`.get_mut()`), handle or `let _ =` every result (`unused-results` is blocking), document every public item.
- After edits run `make fmt`; before declaring a task done run the task's stated checks.
- `xqvm` is `no_std`-capable: `metering.rs` must not use `std`.

---

## File Structure

**Created:**
- `xqvm/src/metering.rs` -- cost constants and the pure cost functions. Small and dependency-free so the parity script can read it and so `no_std` builds are unaffected.
- `xqvm/examples/metering_calibration.rs` -- one-off measurement harness that prints the ns/operation ratios the constants are derived from.
- `xqvm_py/metering.py` -- the Python mirror of `metering.rs`.
- `scripts/check-metering-parity.py` -- three-way guard: Rust constants, Python constants, and the `spec/xqvm/METERING.md` table must agree.
- `spec/xqvm/METERING.md` -- the normative cost table.
- `conformance/vectors/metering/*` -- vectors that assert exact step counts.

**Modified:**
- `xqvm/src/vm.rs` -- counter split, `charge_steps`, the charge call sites.
- `xqvm/src/error.rs:150` -- `StepLimitExceeded` fields, and the `pos()` arm at `:262`.
- `xqvm/src/lib.rs` -- re-export the metering module.
- `xqvm/src/tracer/*` -- `StepState.step` fed from `instructions`.
- `xqvm/tests/integration.rs` -- metering tests.
- `xqffi/src/vm.rs:288` -- add `instructions()` next to `steps()`.
- `xqvm_py/executor.py` -- counter split and charge call sites.
- `conformance/src/lib.rs` -- `steps` on `Expected`/`Outcome`/`PyOut`.
- `Makefile` -- `metering-parity` target wired into `check-parity`.
- `spec/xqvm/SPEC.md` (Runtime Limits), `spec/xqvm/ISA.md`, `docs/book/src/xqvm/execution.md`, `docs/book/src/xqvm/limits-and-errors.md`.

---

### Task 1: The metering primitive

Splits the counters and adds the charge helper. No opcode changes yet, so no step count moves.

**Files:**
- Modify: `xqvm/src/vm.rs:272` (field), `:461-471` (accessors), `:506-520` (run loop), `:670-711` (charge helpers)
- Modify: `xqvm/src/error.rs:149-151`, `:246-255`
- Test: `xqvm/tests/integration.rs`

**Interfaces:**
- Consumes: nothing.
- Produces:
  - `Vm::steps(&self) -> u64` -- metered cost units (existing name, new meaning).
  - `Vm::instructions(&self) -> u64` -- dispatch ordinal (new).
  - `Vm::charge_steps(&mut self, pos: usize, units: u64) -> Result<(), Error>` -- private; every later task calls this.
  - `Error::StepLimitExceeded { pos: Option<usize>, requested: u64, used: u64, limit: u64 }`.

- [ ] **Step 1: Write the failing tests**

Append to `xqvm/tests/integration.rs`:

```rust
// ---------------------------------------------------------------------------
// Metering (QUI-1056)
// ---------------------------------------------------------------------------

#[test]
fn instructions_and_steps_agree_when_every_opcode_costs_base() {
    // 100 NOPs and a HALT: nothing here charges beyond the base cost, so the
    // metered count and the dispatch ordinal are the same number.
    let mut b = InstructionBuilder::new();
    for _ in 0..100 {
        b.emit_nop();
    }
    b.emit_halt();
    let bytecode = b.build().expect("builder build");

    let mut vm = Vm::new();
    vm.set_unlimited_steps();
    vm.run(&bytecode).expect("runs to HALT");
    assert_eq!(vm.steps(), 101);
    assert_eq!(vm.instructions(), 101);
}

#[test]
fn step_limit_error_reports_the_refused_charge() {
    let mut b = InstructionBuilder::new();
    b.emit_push(1).emit_push(2).emit_add();
    let bytecode = b.build().expect("builder build");

    let mut vm = Vm::new();
    vm.set_step_limit(2);
    let err = vm.run(&bytecode).expect_err("two steps is one too few");
    let Error::StepLimitExceeded {
        requested,
        used,
        limit,
        ..
    } = err
    else {
        panic!("expected StepLimitExceeded, got {err:?}");
    };
    assert_eq!(requested, 1, "the base charge is what was refused");
    assert_eq!(used, 2);
    assert_eq!(limit, 2);
}
```

- [ ] **Step 2: Run the tests to verify they fail**

```bash
cargo nextest run -p xqvm -E 'test(instructions_and_steps_agree_when_every_opcode_costs_base) + test(step_limit_error_reports_the_refused_charge)'
```

Expected: FAIL -- `no method named instructions`, and `StepLimitExceeded` has no `requested` field.

- [ ] **Step 3: Widen the error**

In `xqvm/src/error.rs`, replace the `StepLimitExceeded` variant:

```rust
    /// Execution exceeded the configured step budget.
    ///
    /// `requested` is the charge that could not be paid: `1` for the base
    /// cost every instruction pays before dispatch, or the extra units an
    /// opcode asked for before doing work whose size the program controls.
    /// `pos` is absent only for the base charge, which is levied before the
    /// instruction is decoded.
    #[error(
        "step charge of {requested} exceeds the step limit of {limit} \
         ({used} steps already charged)"
    )]
    StepLimitExceeded {
        pos: Option<usize>,
        requested: u64,
        used: u64,
        limit: u64,
    },
```

and move it out of the `None` arm of `pos()` into its own arm beside `ArithmeticOverflow`:

```rust
            Self::ArithmeticOverflow { pos } | Self::StepLimitExceeded { pos, .. } => *pos,
```

- [ ] **Step 4: Split the counters and add the helper**

In `xqvm/src/vm.rs`, add the field next to `steps` (`:272`) and reset it wherever `steps` is reset (`:297`, `:471`, `:506`):

```rust
    /// Instructions dispatched by the last run. Distinct from `steps`, which
    /// counts metered cost units: an opcode whose work scales with the
    /// program's own data charges more than one step for one dispatch.
    instructions: u64,
```

Add the accessor beside `steps()` (`:461`):

```rust
    /// Return the number of instructions dispatched by the last
    /// [`run`](Self::run) call.
    ///
    /// Distinct from [`steps`](Self::steps): a tracer numbers instructions,
    /// an embedder prices steps. See `spec/xqvm/METERING.md`.
    pub fn instructions(&self) -> u64 {
        self.instructions
    }
```

Replace the run-loop charge (`:515-520`) with a call to the helper, and count the dispatch:

```rust
            self.charge_steps_base()?;
            self.instructions += 1;
```

Add both helpers next to `charge` (`:670`):

```rust
    /// Charge `units` of execution against the step budget.
    ///
    /// Callers charge *before* they do the work, so an instruction that
    /// cannot pay does none of it -- the same discipline as [`charge`], and
    /// for the same reason: the budget is a bound on what a program can make
    /// the host do, which is worth nothing if the work happens first.
    fn charge_steps(&mut self, pos: usize, units: u64) -> Result<(), Error> {
        self.charge_steps_at(Some(pos), units)
    }

    /// Charge the base cost every instruction pays before dispatch. Levied
    /// before the instruction is decoded, so it carries no position.
    fn charge_steps_base(&mut self) -> Result<(), Error> {
        self.charge_steps_at(None, 1)
    }

    fn charge_steps_at(&mut self, pos: Option<usize>, units: u64) -> Result<(), Error> {
        let total = self.steps.saturating_add(units);
        if total > self.step_limit {
            return Err(Error::StepLimitExceeded {
                pos,
                requested: units,
                used: self.steps,
                limit: self.step_limit,
            });
        }
        self.steps = total;
        Ok(())
    }
```

- [ ] **Step 5: Feed the tracer from the ordinal**

In `xqvm/src/vm.rs`, the `StepState` construction at `:556` reads `step: self.steps`. Change it to `step: self.instructions` so a trace stays numbered `1, 2, 3, ...` once opcodes charge more than one unit.

- [ ] **Step 6: Run the tests**

```bash
cargo nextest run -p xqvm && cargo test --doc -p xqvm
```

Expected: PASS, including the pre-existing `zero_step_limit_executes_nothing`, `unlimited_steps_runs_to_completion`, `step_limit_equal_to_instruction_count_succeeds` and `step_limit_one_below_instruction_count_fails`, none of which change value.

- [ ] **Step 7: Check the rest of the workspace still builds**

```bash
make fmt && cargo clippy --workspace --all-targets --all-features -- -D warnings
```

Expected: clean. `conformance/src/lib.rs:377` matches `E::StepLimitExceeded { .. }` and is unaffected by the new fields.

- [ ] **Step 8: Commit**

```bash
git add xqvm/src/vm.rs xqvm/src/error.rs xqvm/tests/integration.rs && git commit -s -m "refactor(xqvm): meter steps through a charge helper"
```

---

### Task 2: Calibrate the unit against NOP

Measures what the constants in Task 3 have to be. Produces numbers, not behaviour.

**Files:**
- Create: `xqvm/examples/metering_calibration.rs`

**Interfaces:**
- Consumes: `Vm` from Task 1 (unchanged public API).
- Produces: measured ns/operation ratios, recorded in the commit message and reused by Task 3.

- [ ] **Step 1: Write the harness**

Create `xqvm/examples/metering_calibration.rs` with the AGPL header, then:

```rust
//! Measure the cost of the operations the step meter charges for, relative
//! to a `NOP` dispatch.
//!
//! Not a test and not run by CI: the constants in `xqvm::metering` are frozen
//! from a run of this harness and re-checked by hand when the interpreter
//! changes shape. Run it in release mode, on an otherwise idle machine:
//!
//! ```sh
//! cargo run --release --example metering_calibration
//! ```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run it**

```bash
cargo run --release --example metering_calibration
```

Expected: four lines of output, `nop_ns` in the neighbourhood of the 6.73 ns recorded in QUI-1056. If `nop_ns` is an order of magnitude off, the machine is busy or the build is not release -- rerun before trusting the ratios.

- [ ] **Step 3: Record the ratios**

Round each ratio **up** to the next integer, with a floor of 1. Those integers are `COEFF_WRITE_STEPS`, `MODEL_TERM_STEPS` and `ELEMENT_COPY_STEPS` in Task 3. Take `SAMPLE_COPY_STEPS = ELEMENT_COPY_STEPS`: both are one `i64` written into a contiguous buffer. Write the four measured ns values and the four integers into the commit message body -- Task 9 copies them into `spec/xqvm/METERING.md`.

Rounding up is deliberate: a unit that is cheaper than the operation it stands for reintroduces exactly the underpricing this ticket is about.

- [ ] **Step 4: Commit**

```bash
git add xqvm/examples/metering_calibration.rs && git commit -s -m "test(xqvm): add the step-cost calibration harness"
```

---

### Task 3: The metering module

**Files:**
- Create: `xqvm/src/metering.rs`
- Modify: `xqvm/src/lib.rs`

**Interfaces:**
- Consumes: the four integers measured in Task 2.
- Produces (all `pub`, re-exported from the crate root as `xqvm::metering`):
  - `pub const BASE_STEPS: u64`, `COEFF_WRITE_STEPS: u64`, `MODEL_TERM_STEPS: u64`, `SAMPLE_COPY_STEPS: u64`, `ELEMENT_COPY_STEPS: u64`
  - `pub fn equality_expansion_steps(n: u64) -> u64`
  - `pub fn model_eval_steps(sample_len: u64, terms: u64) -> u64`
  - `pub fn value_copy_steps(value: &RegVal) -> u64`
  - `pub fn widen(n: usize) -> u64` -- the shared saturating `usize`-to-`u64` conversion

Later tasks assert against the *constants*, never against their numeric values, so a recalibration does not rewrite the test suite.

- [ ] **Step 1: Write the failing tests**

Create `xqvm/src/metering.rs` with the AGPL header and only its test module for now:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run the tests to verify they fail**

```bash
cargo nextest run -p xqvm -E 'test(metering::)'
```

Expected: FAIL -- `metering` is not a module of the crate.

- [ ] **Step 3: Write the module**

Above the test module in `xqvm/src/metering.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Replace the three non-`BASE_STEPS` constant values with the integers measured in Task 2 if they differ from the ones above.

- [ ] **Step 4: Wire the module into the crate**

In `xqvm/src/lib.rs`, add `pub mod metering;` beside the other module declarations, in the same alphabetical position the existing list uses.

- [ ] **Step 5: Run the tests**

```bash
cargo nextest run -p xqvm -E 'test(metering::)' && cargo clippy -p xqvm --all-targets --all-features -- -D warnings
```

Expected: PASS, clean clippy.

- [ ] **Step 6: Check the no_std build still works**

```bash
cargo build -p xqvm --no-default-features
```

Expected: success. `metering.rs` uses only core and `crate::RegVal`.

- [ ] **Step 7: Commit**

```bash
git add xqvm/src/metering.rs xqvm/src/lib.rs && git commit -s -m "feat(xqvm): add calibrated execution cost units"
```

---

### Task 4: Charge `ENERGY`

The ticket's headline case: an unbounded model walk for one step.

**Files:**
- Modify: `xqvm/src/vm.rs:2414-2449` (`exec_energy`)
- Test: `xqvm/tests/integration.rs`

**Interfaces:**
- Consumes: `charge_steps` (Task 1), `metering::model_eval_steps`, `metering::MODEL_TERM_STEPS`, `metering::SAMPLE_COPY_STEPS` (Task 3).
- Produces: `ENERGY` charges `BASE_STEPS + model_eval_steps(sample.len(), linear_len + quadratic_len)`.

- [ ] **Step 1: Write the failing test**

Append to `xqvm/tests/integration.rs`:

```rust
#[test]
fn energy_charges_for_the_model_it_evaluates() {
    // Two models of different size, evaluated identically. If ENERGY still
    // cost one step the two runs would cost the same, which is the bug.
    fn steps_for(terms: i64) -> u64 {
        let mut b = InstructionBuilder::new();
        b.emit_push(terms).emit_bqmx(Register(0));
        for i in 0..terms {
            b.emit_push(i).emit_push(1).emit_set_line(Register(0));
        }
        b.emit_push(terms).emit_bsmx(Register(1));
        b.emit_energy(Register(0), Register(1)).emit_halt();
        let bytecode = b.build().expect("builder build");

        let mut vm = Vm::new();
        vm.set_unlimited_steps();
        vm.run(&bytecode).expect("vm run");
        vm.steps()
    }

    let small = steps_for(4);
    let large = steps_for(8);
    assert!(
        large > small,
        "a bigger model must cost more: {large} vs {small}"
    );

    // And by exactly the modelled amount: four more sample elements to copy
    // and four more linear terms to accumulate.
    assert_eq!(
        large - small,
        // four more instructions of setup (PUSH, PUSH, SETLINE) plus their
        // coefficient writes, four more sample elements, four more terms.
        4 * (3 * xqvm::metering::BASE_STEPS + xqvm::metering::COEFF_WRITE_STEPS)
            + 4 * xqvm::metering::SAMPLE_COPY_STEPS
            + 4 * xqvm::metering::MODEL_TERM_STEPS
    );
}
```

Note this test also depends on Task 5 charging `SETLINE`; run it again at the end of Task 5.

- [ ] **Step 2: Run the test to verify it fails**

```bash
cargo nextest run -p xqvm -E 'test(energy_charges_for_the_model_it_evaluates)'
```

Expected: FAIL -- both runs cost the same, so `large > small` is false.

- [ ] **Step 3: Charge before evaluating**

In `exec_energy` (`vm.rs:2414`), the charge goes after both register types are established and before `m.energy(..)` is called -- and, importantly, before the sample is cloned. Restructure the head of the function so the sizes are read first:

```rust
        // Both halves of this are O(model): the sample is copied out of its
        // register and every coefficient is accumulated. Charge for both
        // before either happens, or a program can buy an arbitrarily large
        // model walk for one step (QUI-1056).
        let sample_len = match self.reg(sample) {
            RegVal::Sample(s) => s.values.len(),
            other => {
                return Err(Error::RegisterType {
                    reg: sample.slot(),
                    expected: "sample",
                    got: other.kind().kind_name(),
                });
            }
        };
        let terms = match self.reg(model) {
            RegVal::Model(m) => m.linear_len().saturating_add(m.quadratic_len()),
            other => {
                return Err(Error::RegisterType {
                    reg: model.slot(),
                    expected: "model",
                    got: other.kind().kind_name(),
                });
            }
        };
        self.charge_steps(pos, model_eval_steps(widen(sample_len), widen(terms)))?;
        let sample_values: Vec<i64> = match self.reg(sample) {
            RegVal::Sample(s) => s.values.clone(),
            _ => unreachable!("sample register type checked above"),
        };
        let RegVal::Model(m) = self.reg(model) else {
            unreachable!("model register type checked above")
        };
        let energy = m.energy(&sample_values)?;
```

Keep the existing doc comment about the model-as-sample shortcut: the type checks above preserve that behaviour, including the order in which the two register types are validated (sample first), which existing tests depend on.

Add `use crate::metering::{model_eval_steps, widen};` to the imports at the top of `vm.rs`; later tasks extend that same line.

- [ ] **Step 4: Run the tests**

```bash
cargo nextest run -p xqvm && cargo test --doc -p xqvm
```

Expected: the new assertion on the exact difference still fails (Task 5 charges `SETLINE`); the `large > small` half passes. If any *other* test fails, it is a real regression -- investigate before continuing.

- [ ] **Step 5: Commit**

```bash
git add xqvm/src/vm.rs xqvm/tests/integration.rs && git commit -s -m "fix(xqvm): charge ENERGY for the model it evaluates"
```

---

### Task 5: Charge the coefficient writes and constraint expansions

Every site that already charges bytes gets the matching step charge, computed from the same `n`.

**Files:**
- Modify: `xqvm/src/vm.rs` at `:1702`, `:1731`, `:1783`, `:1810` (coefficient writes), `:1998` (`ONEHOTR`), `:2051` (`ONEHOTC`), `:2199` (`EQUALITY`), `:2256` (`ATLEAST`), `:2344` (`ATLEASTW`), `:1593` (`SLACK`)
- Test: `xqvm/tests/integration.rs`

**Interfaces:**
- Consumes: `charge_steps`, `metering::{equality_expansion_steps, COEFF_WRITE_STEPS, ELEMENT_COPY_STEPS}`.
- Produces: the expansion opcodes charge `equality_expansion_steps(n)` for the same `n` their byte charge uses.

- [ ] **Step 1: Write the failing test**

Append to `xqvm/tests/integration.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run the tests to verify they fail**

```bash
cargo nextest run -p xqvm -E 'test(onehot_charges_for_the_expansion_it_writes) + test(a_coefficient_write_costs_more_than_a_nop)'
```

Expected: FAIL -- both differences come out at zero extra units.

- [ ] **Step 3: Charge each site**

Add `equality_expansion_steps`, `COEFF_WRITE_STEPS` and `ELEMENT_COPY_STEPS` to the `use crate::metering::{..}` line in `vm.rs`, then insert one line immediately **after** each existing byte charge, using the same `n` that charge uses:

| Existing line in `vm.rs` | Line to insert directly below |
|---|---|
| `:1702` `self.charge_coefficient(pos, reg, LINEAR_ENTRY_BYTES)?;` | `self.charge_coefficient_steps(pos, reg)?;` |
| `:1731` same | `self.charge_coefficient_steps(pos, reg)?;` |
| `:1783` `self.charge_coefficient(pos, reg, QUAD_ENTRY_BYTES)?;` | `self.charge_coefficient_steps(pos, reg)?;` |
| `:1810` same | `self.charge_coefficient_steps(pos, reg)?;` |
| `:1998` `self.charge_equality_expansion(pos, cols)?;` | `self.charge_steps(pos, equality_expansion_steps(widen(cols)))?;` |
| `:2051` `self.charge_equality_expansion(pos, rows)?;` | `self.charge_steps(pos, equality_expansion_steps(widen(rows)))?;` |
| `:2199` `self.charge_equality_expansion(pos, idx_vec.len())?;` | `self.charge_steps(pos, equality_expansion_steps(widen(idx_vec.len())))?;` |
| `:2256` `self.charge_equality_expansion(pos, n.saturating_add(num_slacks))?;` | `self.charge_steps(pos, equality_expansion_steps(widen(n.saturating_add(num_slacks))))?;` |
| `:2344` same as `:2256` | `self.charge_steps(pos, equality_expansion_steps(widen(n.saturating_add(num_slacks))))?;` |
| `:1593` `self.charge(pos, entries.saturating_mul(2 * VEC_ELEMENT_BYTES))?;` | `self.charge_steps(pos, entries.saturating_mul(2).saturating_mul(ELEMENT_COPY_STEPS))?;` |

Add the two private helpers next to `charge_coefficient` (`vm.rs:699`):

```rust
    /// Charge the step cost of one coefficient write into `reg`, but only
    /// when `reg` holds a model. The byte twin is [`charge_coefficient`];
    /// the reasoning about samples and repeated writes is identical.
    fn charge_coefficient_steps(&mut self, pos: usize, reg: Register) -> Result<(), Error> {
        if matches!(self.reg(reg), RegVal::Model(_)) {
            self.charge_steps(pos, COEFF_WRITE_STEPS)?;
        }
        Ok(())
    }
```

`widen` in the table above is `metering::widen` -- add it to the same `use crate::metering::{..}` line rather than writing a second copy in `vm.rs`.

`EQUALITY` and the `ATLEAST` family clone their index and coefficient vecs a few lines above the charge. That copy is O(n) and momentarily uncharged, but it is bounded by the same `n` that the expansion charge covers and it happens before the quadratic work, so the instruction still cannot escape its budget by more than a linear term. Leave the ordering as it is rather than restructuring three handlers; note it in `METERING.md` (Task 9).

- [ ] **Step 4: Run the tests**

```bash
cargo nextest run -p xqvm && cargo test --doc -p xqvm
```

Expected: PASS, including `energy_charges_for_the_model_it_evaluates` from Task 4, whose exact-difference assertion needs `SETLINE` charged.

- [ ] **Step 5: Commit**

```bash
git add xqvm/src/vm.rs xqvm/tests/integration.rs && git commit -s -m "fix(xqvm): charge constraint expansions for the terms they write"
```

---

### Task 6: Charge the allocators and the register copies

**Files:**
- Modify: `xqvm/src/vm.rs` at `:1438`, `:1447`, `:1458` (sample allocators), `:919` (`RANGE`/`ITER` int path), `:951` (`ITER` xqmx path), `:864` (`LVAL`), `:1082` (`INPUT`), `:1120` (`OUTPUT`)
- Test: `xqvm/tests/integration.rs`

**Interfaces:**
- Consumes: `charge_steps`, `metering::{value_copy_steps, SAMPLE_COPY_STEPS, ELEMENT_COPY_STEPS}`.
- Produces: no register-sized copy costs one step any more.

- [ ] **Step 1: Write the failing test**

Append to `xqvm/tests/integration.rs`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

- [ ] **Step 2: Run the tests to verify they fail**

```bash
cargo nextest run -p xqvm -E 'test(allocating_a_sample_costs_its_length) + test(reading_a_model_out_of_a_register_costs_its_coefficients)'
```

Expected: FAIL -- both differences are short by the copy cost.

- [ ] **Step 3: Charge each site**

Add `value_copy_steps` and `SAMPLE_COPY_STEPS` to the `use crate::metering::{..}` line, then:

- `exec_bsmx` (`:1438`), `exec_ssmx` (`:1447`), `exec_xsmx` (`:1458`): after each `self.charge_variables(pos, size)?;` add

  ```rust
        self.charge_steps(pos, widen(size).saturating_mul(SAMPLE_COPY_STEPS))?;
  ```

  Leave `exec_bqmx`, `exec_sqmx` and `exec_xqmx` alone: an empty model allocates no coefficients, so the base cost is honest.

- `exec_iter` int path (`:919`): after `self.charge_variables(pos, range.len())?;` add

  ```rust
                self.charge_steps(pos, widen(range.len()).saturating_mul(ELEMENT_COPY_STEPS))?;
  ```

- `exec_iter` xqmx path (`:951`): the handler already folds the slice to compute `cost` in bytes. Add a second fold in the same shape, immediately after `self.charge(pos, cost)?;`:

  ```rust
                let step_cost = v
                    .get(range.clone())
                    .unwrap_or_else(|| unreachable!("resolve_iter_slice already validated"))
                    .iter()
                    .fold(0u64, |acc, m| {
                        let terms = widen(m.linear_len().saturating_add(m.quadratic_len()));
                        acc.saturating_add(terms.saturating_mul(COEFF_WRITE_STEPS))
                    });
                self.charge(pos, cost)?;
                self.charge_steps(pos, step_cost)?;
  ```

  Compute `step_cost` from the same borrow that computes `cost`, before the `charge` call, so the two folds share one lookup.

- `exec_l_val` (`:864`), `exec_input` (`:1082`), `exec_output` (`:1120`): each clones a value. Charge for what is about to be cloned, before the clone:

  ```rust
        let copy_cost = value_copy_steps(&value_to_clone);
        self.charge_steps(pos, copy_cost)?;
  ```

  where `value_to_clone` is the borrow the handler already resolves. In `exec_output` (`:1120`) that means replacing

  ```rust
        let val = self.reg(reg).clone();
  ```

  with

  ```rust
        // A register can hold a model, and cloning one clones its
        // coefficient maps -- O(model) work for one dispatch (QUI-1056).
        let copy_cost = value_copy_steps(self.reg(reg));
        self.charge_steps(pos, copy_cost)?;
        let val = self.reg(reg).clone();
  ```

  Apply the same two-line prologue at `:864` and `:1082`, adjusting the expression that names the value.

- `RANGE` (`exec_range`, whose `charge_variables(pos, range.len())` is the call at `:919`'s sibling site): charge `widen(count).saturating_mul(ELEMENT_COPY_STEPS)` for the generated values, next to the existing byte charge.

- [ ] **Step 4: Run the whole suite**

```bash
cargo nextest run -p xqvm && cargo test --doc -p xqvm && cargo clippy --workspace --all-targets --all-features -- -D warnings
```

Expected: PASS, clean clippy. Doc tests that assert a step count -- notably the `set_memory_limit` example at `vm.rs:378` -- must still pass; if one now trips a step limit, that is a real finding: report it rather than raising the limit in the example.

- [ ] **Step 5: Commit**

```bash
git add xqvm/src/vm.rs xqvm/tests/integration.rs && git commit -s -m "fix(xqvm): charge allocators and register copies for their size"
```

---

### Task 7: Mirror the metering in the Python VM

Step counts are now observable, so `xqvm_py` must produce identical ones.

**Files:**
- Create: `xqvm_py/metering.py`
- Modify: `xqvm_py/executor.py` (`:314-324` run loop, `:338-368` charge helpers, and every `self._charge*` call site), `xqvm_py/state.py:268`, `xqvm_py/errors.py:158`
- Test: `xqvm_py/tests/test_executor.py`

**Interfaces:**
- Consumes: the constants and formulas frozen in Task 3.
- Produces: `xqvm_py.metering` with `BASE_STEPS`, `COEFF_WRITE_STEPS`, `MODEL_TERM_STEPS`, `SAMPLE_COPY_STEPS`, `ELEMENT_COPY_STEPS`, `equality_expansion_steps(n)`, `model_eval_steps(sample_len, terms)`, `value_copy_steps(value)`; `Executor.instructions` property; `StepLimitExceeded(limit, requested, used)`.

- [ ] **Step 1: Write the failing tests**

Append to `xqvm_py/tests/test_executor.py`, inside the class that holds the existing step-limit tests (`test_step_limit_raises_an_xqvm_error` at `:2980`):

```python
    def test_energy_charges_for_the_model_it_evaluates(self):
        """ENERGY walks every coefficient; one step for that is QUI-1056."""
        from xqvm_py.metering import (
            BASE_STEPS,
            COEFF_WRITE_STEPS,
            MODEL_TERM_STEPS,
            SAMPLE_COPY_STEPS,
        )

        def steps_for(terms: int) -> int:
            source = [f"PUSH {terms}", "BQMX r0"]
            for i in range(terms):
                source += [f"PUSH {i}", "PUSH 1", "SETLINE r0"]
            source += [f"PUSH {terms}", "BSMX r1", "ENERGY r0 r1", "HALT"]
            ex = Executor()
            ex.execute(assemble("\n".join(source)))
            return ex.steps

        delta = steps_for(8) - steps_for(4)
        assert delta == 4 * (3 * BASE_STEPS + COEFF_WRITE_STEPS) + 4 * (
            SAMPLE_COPY_STEPS + MODEL_TERM_STEPS
        )

    def test_instructions_and_steps_are_separate_counters(self):
        ex = Executor()
        ex.execute(assemble("PUSH 2\nBQMX r0\nPUSH 0\nPUSH 3\nSETLINE r0\nHALT"))
        assert ex.instructions == 6
        assert ex.steps > ex.instructions
```

Use whatever assembler helper the surrounding tests already use to turn source into a `Program`; do not introduce a second one.

- [ ] **Step 2: Run the tests to verify they fail**

```bash
uv run --no-sync pytest xqvm_py/tests/test_executor.py -k "charges_for_the_model or separate_counters" -v
```

Expected: FAIL -- `xqvm_py.metering` does not exist.

- [ ] **Step 3: Write the mirror module**

Create `xqvm_py/metering.py` with the AGPL header and a module docstring that says it mirrors `xqvm/src/metering.rs` value for value and points at `spec/xqvm/METERING.md`, then the same five constants and three functions. Python integers are unbounded, so the saturating arithmetic has to be written out:

```python
U64_MAX = (2**64) - 1


def _saturating(value: int) -> int:
    """Clamp to the u64 range the Rust VM's counters live in.

    Python integers do not overflow, so a charge that Rust saturates to
    ``u64::MAX`` would otherwise grow past it here and the two VMs would
    disagree about which programs are affordable.
    """
    return min(max(value, 0), U64_MAX)


def equality_expansion_steps(n: int) -> int:
    """Worst-case cost of an equality expansion over ``n`` terms."""
    n = _saturating(n)
    pairs = _saturating(n * (n - 1)) // 2
    return _saturating(_saturating(n + pairs) * COEFF_WRITE_STEPS)
```

`model_eval_steps` and `value_copy_steps` follow the Rust definitions in the Design section. `value_copy_steps` dispatches on the Python register value types the executor uses (`int`, `list[int]`, `XQMX` in model vs sample mode, `list[XQMX]`); read `_get_register_as_xqmx` and the `XQMX` class in `xqvm_py/xqmx.py` for the accessors that correspond to `linear_len()` and `quadratic_len()`.

- [ ] **Step 4: Split the counters and add the charge helper**

In `xqvm_py/state.py:268`, add `self.instructions = 0` beside `self.steps = 0` and reset it in the same place.

In `xqvm_py/executor.py`, replace the run loop at `:314-317`:

```python
        while not self.state.halted and self.state.pc < len(program):
            self._charge_steps(BASE_STEPS, step_limit)
            self.state.instructions += 1
            self.step()
```

and add, beside `_charge` (`:338`):

```python
    def _charge_steps(self, units: int, limit: int | None = None) -> None:
        """Charge ``units`` of execution against the step budget.

        Callers charge *before* they do the work, so an instruction that
        cannot pay does none of it -- the same discipline as ``_charge``.
        """
        limit = self._step_limit if limit is None else limit
        if limit is not None and self.state.steps + units > limit:
            raise StepLimitExceeded(limit, requested=units, used=self.state.steps)
        self.state.steps += units
```

Store the limit on the executor (`self._step_limit = step_limit` at the top of `execute`, beside `self._memory_limit`) so handlers can charge without threading it through, and add the `instructions` property next to `steps` (`:322`).

Extend `StepLimitExceeded` in `xqvm_py/errors.py:158` with the two new keyword fields, defaulted so existing raise sites keep working, and mirror the Rust message text.

- [ ] **Step 5: Charge the same sites the Rust VM charges**

Work through the Rust diff from Tasks 4-6 and add the twin call at each Python site. Every one of them is directly below an existing `self._charge*` call except `ENERGY`, `LVAL`, `INPUT` and `OUTPUT`:

| Python site | Charge |
|---|---|
| `_runner_ENERGY` (`:1145`) | `model_eval_steps` over the sample length and the model's term count, before `compute_energy` |
| `_runner_SETLINE`/`ADDLINE`/`SETQUAD`/`ADDQUAD` | `COEFF_WRITE_STEPS`, only when the register holds a model |
| `_runner_ONEHOTR` / `ONEHOTC` | `equality_expansion_steps(cols)` / `(rows)` |
| `_runner_EQUALITY` / `ATLEAST` / `ATLEASTW` | `equality_expansion_steps(n)` / `(n + num_slacks)` |
| `_runner_BSMX` / `SSMX` / `XSMX` | `size * SAMPLE_COPY_STEPS` |
| `_runner_RANGE` / `ITER` | `ELEMENT_COPY_STEPS` per element, `value_copy_steps` per model for the xqmx path |
| `_runner_LVAL` / `INPUT` / `OUTPUT` | `value_copy_steps(value)` |
| `_runner_SLACK` | `2 * entries * ELEMENT_COPY_STEPS` |

- [ ] **Step 6: Run the Python suite**

```bash
uv run --no-sync pytest xqvm_py/tests -q && make lint-py && make fmt-check-py
```

Expected: PASS. `xqvm_py/tests/test_cli_run.py:77` exercises the `--step-limit` flag; if it now trips, the fixture's program got more expensive -- adjust the fixture's limit, not the metering.

- [ ] **Step 7: Commit**

```bash
git add xqvm_py/metering.py xqvm_py/executor.py xqvm_py/state.py xqvm_py/errors.py xqvm_py/tests/test_executor.py && git commit -s -m "fix(xqvm-py): mirror the Rust step metering"
```

---

### Task 8: Lock the two implementations together

A number both VMs must agree on needs a guard that fails when they drift.

**Files:**
- Create: `scripts/check-metering-parity.py`, `conformance/vectors/metering/energy_scales_with_model/{program.xqasm,inputs.json,expected.json}`, `conformance/vectors/metering/onehot_expansion/{program.xqasm,inputs.json,expected.json}`
- Modify: `conformance/src/lib.rs:55-60` (`PyOut`), `:160-205` (`Expected`), `:215-235` (`Outcome`), `:344` and `:527` (the two runners), `:556` (`check`); `Makefile:91`

**Interfaces:**
- Consumes: `xqvm::metering` (Task 3), `xqvm_py.metering` (Task 7), `Vm::steps()`, `executor.steps` (already emitted by `xqvm_py/cli/run.py:159`).
- Produces: `make metering-parity`; an optional `steps` field on `expected.json` that both runners assert.

- [ ] **Step 1: Write the parity script's failing state**

Create `scripts/check-metering-parity.py` modelled on `scripts/check-opcode-parity.py` (read it first for the repo's conventions on argument handling, exit codes and diff output). It compares three sources and exits non-zero on any disagreement:

1. `xqvm/src/metering.rs` -- regex `^pub const (\w+): u64 = (\d+);` over the file.
2. `xqvm_py/metering.py` -- import the module and read the same names.
3. `spec/xqvm/METERING.md` -- the constants table, parsed as `| \`NAME\` | value | ...`.

Report every mismatched name with all three values, not just the first.

- [ ] **Step 2: Run it and watch it fail**

```bash
uv run --no-sync python scripts/check-metering-parity.py
```

Expected: FAIL -- `spec/xqvm/METERING.md` does not exist yet. That file arrives in Task 9; this script is what makes Task 9 non-optional.

- [ ] **Step 3: Wire it into the Makefile**

In `Makefile`, add the target beside `opcode-parity-py` and extend `check-parity` (`:91`):

```make
check-parity: opcode-parity conformance example-smoke metering-parity

metering-parity: deps-py
	uv run --no-sync python scripts/check-metering-parity.py
```

- [ ] **Step 4: Teach the conformance harness about steps**

In `conformance/src/lib.rs`:

- Add `steps: Option<u64>` to `RawExpected` and to `Expected::Success`; a vector that omits it asserts nothing about cost, so the existing 100-odd vectors keep passing unchanged.
- Add `steps: u64` to `Outcome::Success`, filled from `vm.steps()` in `run_rust_program` (`:344`) and from a new `steps: Option<u64>` field on `PyOut` in `run_python_file` (`:527`). The Python CLI already emits it.
- In `check` (`:556`), compare steps only when the vector supplies them:

```rust
            if let Some(exp_steps) = exp_steps {
                if steps != exp_steps {
                    let _ = writeln!(
                        detail,
                        "  steps:\n    expected: {exp_steps}\n    actual:   {steps}"
                    );
                }
            }
```

  Fold it into the existing mismatch-reporting shape rather than adding a second failure path.
- Update the module doc comment at `:25` to describe the new field.

- [ ] **Step 5: Add the vectors**

`conformance/vectors/metering/energy_scales_with_model/program.xqasm`: build a 4-variable binary model with four linear coefficients, allocate a matching sample, `ENERGY`, `STOW`, `OUTPUT`, `HALT`. Head it with a comment naming QUI-1056 and stating what the vector pins: that both implementations charge the same for a model walk.

`conformance/vectors/metering/onehot_expansion/program.xqasm`: a 4-column model, `RESIZE`, `ONEHOTR` with a penalty, `HALT`. Same comment discipline.

`inputs.json` for both: `{"calldata": [], "output_slots": 1}` (or `0` for the ONEHOTR vector, which produces no output).

`expected.json`: run each vector under both implementations, confirm the two agree, hand-check the number against the cost table in the Design section, and record it:

```json
{"outputs": [0], "final_stack": [], "steps": 42}
```

If the two implementations disagree, that is the guard doing its job -- fix the implementation, never the expectation.

- [ ] **Step 6: Run the conformance suite both ways**

```bash
make conformance
```

Expected: PASS for both `conformance-rs` and `conformance-py`, including the two new vectors.

- [ ] **Step 7: Commit**

```bash
git add scripts/check-metering-parity.py Makefile conformance && git commit -s -m "test(conformance): assert step counts agree across implementations"
```

---

### Task 9: Specify it

A count the chain prices is consensus-visible and belongs in the spec. `spec/xqvm/` currently says nothing about step budgets at all.

**Files:**
- Create: `spec/xqvm/METERING.md`
- Modify: `spec/xqvm/SPEC.md:91-102` (Runtime Limits) and `:127` (Related Specifications), `spec/xqvm/ISA.md` (notation section), `spec/xqvm/VERIFIER.md`, `docs/book/src/xqvm/execution.md:45-56`, `docs/book/src/xqvm/limits-and-errors.md:26`, `xqffi/src/vm.rs:288`

**Interfaces:**
- Consumes: the constants from Task 3 and the measured ns values recorded in Task 2's commit message.
- Produces: `spec/xqvm/METERING.md`, the third source `scripts/check-metering-parity.py` reads; `XqVm.instructions()` on the FFI surface.

- [ ] **Step 1: Write the specification**

Create `spec/xqvm/METERING.md` following the house style of `spec/xqvm/HLF.md` (normative language, tables, no emojis, `--` not em-dash). It must contain:

- **What a step is.** A unit of work, not an instruction. One step is one `NOP` dispatch. Every instruction charges `BASE_STEPS` before dispatch; opcodes whose work scales with program-controlled data charge more, before doing the work.
- **The constants table**, in the exact shape `scripts/check-metering-parity.py` parses:

```markdown
| Constant | Value | Meaning |
|----------|-------|---------|
| `BASE_STEPS` | 1 | One instruction dispatch. |
| `COEFF_WRITE_STEPS` | 8 | One coefficient written into a model's sparse map. |
| `MODEL_TERM_STEPS` | 2 | One model term accumulated into an energy. |
| `SAMPLE_COPY_STEPS` | 1 | One sample element written. |
| `ELEMENT_COPY_STEPS` | 1 | One `i64` copied out of a vec. |
```

  with the values from Task 3 and a note recording the measured nanoseconds each was derived from.
- **The per-opcode cost table** from the Design section above, verbatim.
- **The formulas** for `equality_expansion_steps`, `model_eval_steps` and `value_copy_steps`, including the statement that all arithmetic is integer and saturating, and that an expansion count is a worst case that bounds the work rather than measuring it.
- **The known slack**: `EQUALITY` and the `ATLEAST` family clone their index and coefficient vecs before charging, so up to `O(n)` element copies precede the charge for the `O(n^2)` expansion that follows.
- **Conformance**: step counts are observable; two implementations executing the same program must report the same count.

- [ ] **Step 2: Link it from the spec**

In `spec/xqvm/SPEC.md`, replace the `Loop nesting` and `XQMX size` notes' silence about time with a metering row in the Runtime Limits table:

```markdown
| Step budget | Implementation-defined | A step is a unit of work, not an instruction. Implementations meter execution as specified in [METERING.md](METERING.md) and raise `StepLimitExceeded` when the budget is exhausted. |
```

Add `METERING.md` to the Related Specifications list at `:127`.

In `spec/xqvm/ISA.md`, add one line to the Notation section: opcode cost is specified in `METERING.md`, not per-row here.

In `spec/xqvm/VERIFIER.md`, state that a step count is data-dependent and cannot be predicted statically: the verifier bounds execution, it does not predict its cost. This mirrors what is already true of the memory budget.

- [ ] **Step 3: Update the book**

- `docs/book/src/xqvm/execution.md:45-56`: the paragraph claiming the counter "increments after every instruction dispatch" is now wrong. Rewrite it around the two counters, and link `spec/xqvm/METERING.md`.
- `docs/book/src/xqvm/limits-and-errors.md:26`: the `Step count` row needs the same correction, plus the new `StepLimitExceeded` message shape at `:114`.

Leave the stale `set_step_limit(0)` prose at `execution.md:53` and `limits-and-errors.md:36` alone -- it predates QUI-1053 and is a separate fix.

- [ ] **Step 4: Expose the ordinal on the FFI**

In `xqffi/src/vm.rs`, beside `steps()` (`:288`):

```rust
    /// Total instructions dispatched by the last run.
    ///
    /// Distinct from `steps()`, which counts metered cost units: an opcode
    /// whose work scales with the program's own data charges more than one
    /// step for one dispatch. See `spec/xqvm/METERING.md`.
    fn instructions(&self) -> u64 {
        self.inner.instructions()
    }
```

and correct the `set_step_limit` doc comment at `:233`, which currently calls it "the instruction-step limit".

- [ ] **Step 5: Run every guard**

```bash
make preflight
```

Expected: PASS. In particular `metering-parity` now finds all three sources, `check-docs-drift` accepts the new page links, and `check-mermaid-render` is unaffected.

- [ ] **Step 6: Commit**

```bash
git add spec docs xqffi/src/vm.rs && git commit -s -m "docs(xqvm): specify step metering"
```

---

## Verification

Run before declaring the branch done:

```bash
make preflight
```

That covers `preflight-rs` (fmt, taplo, clippy, rustdoc, deny, unit/integration/doc tests), `preflight-py`, `preflight-parity` (opcode parity, metering parity, conformance both ways, example smoke), `preflight-docs` and `preflight-policy`.

Then confirm the ticket's own claim by hand:

```bash
cargo run --release --example metering_calibration
```

The ratio between the dearest charged operation and `NOP` is the factor by which a flat per-step price was previously wrong. That number belongs in the QUI-1056 close-out comment and is the input QUI-1054 needs for its safety multiplier.

## Follow-ups, not this branch

- QUI-1054 / QUI-1013: recalibrate `WeightPerStep` against `NOP` plus a safety multiplier now that a step is bounded work.
- QUI-1019: this is a behavioural break for any program that touches a model; it ships in 0.4.0.
- The stale `set_step_limit(0)` prose in the book (`execution.md:53`, `limits-and-errors.md:36`), left over from QUI-1053.

---

### Design Plans 2026 08 25 Qui 1178 Followups (`docs/design/plans/2026-08-25-qui-1178-followups.md`)

# QUI-1178 follow-ups

Findings from reviewing `xqvm_py/executor.py` against `xqvm/src/vm.rs` while
fixing the pop-before-register-read ordering (QUI-1178). None of these is a
pop-order defect, so none was fixed inside that ticket. Each was verified
against the code in this branch (and, where noted, against a small script
run through `xqvm_py`) rather than assumed from an earlier description. The
human files these as separate Linear tickets, each linked to QUI-1178.

Finding 10 was added later, during review of !134, and is the only entry
here that was fixed rather than filed and deferred (QUI-1202).

**Base:** the branch this document was written on, before it was rebased.
Every line number here is from that tree and is not maintained as those
files move; resolve a reference by the name it cites -- a function, a
section, a table row -- rather than by its line.

## Status against the merge requests open alongside this one

Re-checked 2026-08-27, after !133 merged into `main` (`e45acd1`) and this
branch was rebased onto it. **Read this before filing any ticket below.**

Verified against `main` at rebase time: findings 1 and 4 are closed by
!133, and finding 3 is still live in `exec_equality`. Findings 2 and 8
were re-confirmed by inspecting `_get_register_as_vec`/`_get_register_as_xqmx`
on `main`. Findings 5, 6 and 7 were *not* re-verified against `main` — they
were established against this branch's original base and must be re-read
before a ticket is filed.

| MR | Branch | Bearing on this list |
|---|---|---|
| [!133](https://gitlab.com/quip.network/xquad/-/merge_requests/133) | `feature/qui-1062-1029-1174` | **Merged (`e45acd1`). Closes findings 1 and 4.** Also fixed a coefficient-index divergence that is *not* in this list — see below. |
| [!132](https://gitlab.com/quip.network/xquad/-/merge_requests/132) | `feature/qui-1056` | Step metering. Touches no finding here, but edits most of the same runner bodies. |
| [!134](https://gitlab.com/quip.network/xquad/-/merge_requests/134) | `feature/qui-1178` | This ticket. |

**Already fixed by !133, do not file:**

- **Finding 4** (`EXCLUDE`/`IMPLIES` conditional charge). !133 replaces both
  `_charge_coefficient(reg, ...)` calls with an unconditional
  `self._charge(...)` placed ahead of the register read, matching
  `exec_exclude`/`exec_implies`.
- **Finding 1** (`EQUALITY`'s model register). !133 sizes the charge from
  `_peek_model_size`, which returns 0 for a sample as Rust's peek does (1a);
  resolves the model through `_get_register_as_model` *after* the vec length
  comparison and before the size is grown, so a sample is rejected without
  being mutated (1b, 1c).

**Not in this list, already handled by !133 — do not re-derive it as new:**

- **Coefficient indices were unbounded above.** `XQMX.get_linear` had no
  bound at all, and the quadratic accessors rejected only negative indices,
  so a write past the declared size landed in the sparse coefficient map and
  grew the model beyond the count its allocator declared — the program then
  carried constraints over variables that do not exist and still solved
  cleanly. !133 adds `bounded_index` (`xqvm/src/vm.rs`) and
  `XQMX._check_index` (`xqvm_py/xqmx.py`), bounding reads the same way as
  writes on both VMs. This is the most security-relevant item in flight
  across the three MRs.

**Overlap warning — resolved for !134.** !133 reordered the same 22 runners
as !134 and landed first, so !134 was rebased onto it: the duplicated
reorder hunks and the `EQUALITY` charge-sizing rework were dropped rather
than re-applied, leaving the AST guard, the route-B vectors and these
notes. !132 (step metering) still edits most of the same bodies and needs
the same treatment.

## Priority note: the Python VM is being decommissioned

Recorded 2026-08-25, revised 2026-08-27 (finding 1 since closed).
Findings 2, 5, 6, 8 and 9 are all cases where the
**Rust VM is already correct and Python is not**. They are alignment work on
a component scheduled for removal, so they can be closed wholesale when it
goes rather than fixed. Finding 7 is conformance-harness ergonomics whose
value also shrinks once the corpus becomes Rust-only golden tests.

**Finding 3 is the only entry below that is a defect in the shipping Rust
VM**, and it is the only one worth scheduling on its own merits.

## 1. `EQUALITY`'s model register: charge sizing, fault identity, and
   register order all diverge between the two VMs — CLOSED BY !133, DO NOT FILE

**Status: closed by !133** (`e45acd1`), which lands all three parts. The
finding is kept below as the record of what was wrong and how it was
verified; the line numbers cited are this branch's original base, not
`main`.

**Evidence:** `xqvm/src/vm.rs:2542-2598` (`exec_equality`) vs.
`xqvm_py/executor.py:1309-1344` (`_runner_EQUALITY`) and
`xqvm_py/xqmx.py:525-559` (`expand_equality`).

Three distinct, verified divergences live in the same instruction:

**a. Charge sizing for a `Sample` in the model register differs.** Rust
peeks the model with `match self.reg(model) { RegVal::Model(m) => m.size,
_ => 0 }` (`vm.rs:2553-2556`) — a `Sample` sizes to `0`. Python peeks with
`current_size = peeked_model.size if isinstance(peeked_model, XQMX) else 0`
(`executor.py:1331-1332`) — `XQMX` is the single dataclass both `Model` and
`Sample` are instances of (`xqvm_py/xqmx.py:62-82`, distinguished only by a
`mode` field), so a `Sample` contributes its *real* size. Against a model
growth close to the budget, the two VMs can charge different amounts for
the same program and disagree on whether `MemoryLimitExceeded` fires.

**b. A `Sample` in the model register mutates before it faults, with a
different fault identity than Rust's.** Verified by direct execution
(script below): with a `Sample(mode=SAMPLE, size=4)` in `r0` and
`indices=[10]`, Python's `_runner_EQUALITY` writes
`model.size = max(model.size, needed)` (`executor.py:1343`) — mutating the
sample's `size` field from 4 to 11 — *before* `expand_equality` reaches its
`require_model_mode(model, "EQUALITY")` guard (`xqmx.py:539`) and raises
`XQMXModeError`. Rust never touches the sample: `as_model_mut()`
(`vm.rs:2585-2592`) rejects it immediately with `RegisterType` (mapped to
`Fault::TypeMismatch`), and `XQMXModeError` maps to a different fault,
`Fault::XqmxMode` (`conformance/src/lib.rs:435`). So for this input both
VMs fault, but with different fault identities, and only Python leaves a
mutated side effect behind first. (The original description of this
finding claimed Python "runs to completion" — that is not what happens;
confirmed by running the program in-process. The mutation-before-fault and
the fault-identity mismatch are the real, verified bugs.)

  ```python
  # Reproduction (run from the repo root):
  from xqvm_py.executor import Executor
  from xqvm_py.xqmx import XQMX, XQMXMode, XQMXDomain
  from xqvm_py.program import Program, Instruction
  from xqvm_py.opcodes import Opcode
  # r0 <- Sample(size=4); r1 <- [10]; r2 <- [1]; EQUALITY r0 r1 r2
  # raises XQMXModeError, but sample.size is already 11 by then.
  ```

**c. The two VMs check the vec length and the model's type in opposite
order.** Rust resolves and clones `indices`/`coeffs` as `vec<int>`, checks
`idx_vec.len() != coeff_vec.len()` (`vm.rs:2577-2584`), and only *then*
calls `as_model_mut()` (`vm.rs:2585-2592`) — a length mismatch is caught
before the model register is ever examined. Python resolves the model
register (`_get_register_as_xqmx`, `executor.py:1341`, which accepts a
`Sample` too) and mutates `model.size` *before* `expand_equality`'s length
check (`xqmx.py:541-543`) ever runs. Confirmed by execution: a program with
`r0` holding a plain int (not an `XQMX`) and `indices`/`coeffs` of
mismatched length raises `TypeMismatch` on Python (model check fires
first) where Rust raises `VecLengthMismatch` (length check fires first) —
a genuine, consensus-relevant fault-identity divergence.

  (Earlier drafts of this review also claimed "`_runner_EQUALITY` has no
  vec-length check at all." That claim is false: `expand_equality` already
  checks `len(indices) != len(coeffs)` and raises `VecLengthMismatch`
  (`xqmx.py:541-543`), confirmed by running a mismatched-length program
  through `xqvm_py` — it does raise `VecLengthMismatch`. The real bug is
  the *ordering* relative to the model-type check described in (c), not a
  missing check.)

**Why out of scope for QUI-1178:** none of this is a pop-before-register-
read defect — pops already precede the register reads in the fixed
`_runner_EQUALITY`. It is a set of separate register-order and
type-acceptance divergences between the two VMs' `EQUALITY` handlers.

## 2. A `vec<xqmx>` in `EQUALITY`'s or `ATLEASTW`'s index/coefficient
   registers crashes Python outside the fault system

**Evidence:** `xqvm_py/executor.py:537-542` (`_get_register_as_vec` accepts
any `Vec`, not just `vec<int>`) vs. Rust's `as_vec_int`
(`xqvm/src/value.rs:206-214`), which only accepts `RegVal::VecInt`.

Verified by direct call: `expand_equality(model, [xqmx_instance], [1], 0,
1)` raises a bare `TypeError: '<' not supported between instances of
'XQMX' and 'int'` from `XQMX.add_linear`'s bounds check `if i < 0 or i >=
self.size` (`xqvm_py/xqmx.py:196`) — not an `XQVMError` subclass, so the
conformance harness's Rust-side error mapping has nothing to catch. (The
original description guessed the crash comes from using `XQMX` values as
unhashable dict keys — `XQMX` is indeed an `eq=True` dataclass with
`__hash__ = None`, but that path is never reached: the plain `<` comparison
in the bounds check fails first. The conclusion — an unmapped Python
exception escapes the fault system — is confirmed either way.)

`ATLEASTW` has the same exposure: `weight_sum = check_i64(weight_sum + w,
...)` (`xqvm_py/executor.py:1399`) raises `TypeError: unsupported operand
type(s) for +` when `w` is an `XQMX` pulled from a `vec<xqmx>` coeffs
register.

**Why out of scope for QUI-1178:** register-content validation, not
pop/charge ordering.

## 3. Rust's `EQUALITY` charge-sizing narrows before charging — live on
   64-bit hosts too, not just wasm32

**Evidence:** `xqvm/src/vm.rs:2542-2550`:

```rust
let (n_terms, needed) = match self.reg(indices) {
    RegVal::VecInt(v) => (
        v.len(),
        v.iter()
            .max()
            .and_then(|&max_idx| max_idx.checked_add(1))
            .and_then(|needed| usize::try_from(needed).ok())
            .unwrap_or(0),
    ),
    _ => (0, 0),
};
```

The first link in this chain, `.and_then(|&max_idx| max_idx.checked_add(1))`
(`vm.rs:2547`), already fails before the target-width narrowing on the next
line ever runs: `indices` is a `vec<i64>`, so an `indices` vector containing
`i64::MAX` makes `checked_add(1)` overflow `i64` and return `None` —
`usize::try_from` is never reached. `.unwrap_or(0)` then sizes the model
growth to zero, no charge is raised, and `exec_equality` proceeds past the
charge to `indices_to_usize` (`vm.rs:2591`), which raises
`IndexOutOfBounds` against the tiny (or zero-grown) model. This is a plain
`i64` overflow, so it reproduces identically on a 64-bit host and on
wasm32 — the `usize::try_from` step that follows is a second, narrower
hazard specific to a 32-bit `usize` (an index at or above `u32::MAX` with
no `i64::MAX` involved), but it is not the only way into this bug, and not
the one that matters on the reference target.

Python's `needed` is an unbounded Python int (`executor.py:1330`) with no
equivalent overflow, so for the same `indices=[i64::MAX]` input it charges
`needed` (~8 EiB) and raises `MemoryLimitExceeded`, while Rust — on the
64-bit reference VM as well as the wasm32 chain target — silently charges
zero and raises `IndexOutOfBounds` instead. Same divergence class as the
wasm32-only framing this section previously used, but reachable without a
32-bit target at all.

`xqvm/src/vm.rs:807-824` (`Vm::allocation_size`) already documents and
implements the correct pattern for exactly this hazard: charge off the
`i64`/`u64` value *before* narrowing to `usize`, specifically "including
... the wasm32 runtime ... where `usize::try_from` would otherwise decide
the answer." `exec_equality` does not follow its own codebase's documented
pattern, and the fix needs to charge off the pre-`checked_add` value, not
just swap the narrowing step for a wider one.

**Why out of scope for QUI-1178:** not a pop/charge-ordering defect within
one VM; it is a target-independent charge-sizing bug in Rust alone that
happens to also have a second, wasm32-specific manifestation.

**Severity: fix-soon, not release-gating. It is not exploitable.** Traced
end to end before filing, so whoever picks this up does not have to treat
it as a security incident:

- The skipped charge is for growth that also never happens. `needed` sizes
  to `0`, so `if needed > m.size` is false, the model never grows, and
  `indices_to_usize` then always fails. No unpriced allocation, no state
  change; `charge_equality_expansion` still fires, so the expansion stays
  priced.
- It cannot go the other way. For the narrowing to be swallowed, `max_idx`
  must exceed anything `m.size` could reach, so there is no input where
  wasm32 succeeds and a 64-bit host faults.
- It is not a consensus hazard. All consensus execution is the same wasm
  blob, so every validator reaches `IndexOutOfBounds` together.

What it costs is developer trust: `xquad run` natively reports
`MemoryLimitExceeded` where the chain reports `IndexOutOfBounds` for the
same program. The reason to fix it anyway is that the fix is three lines
against a pattern already in the same file, and `allocation_size`'s
docstring is normative — one unfixed violation beside it is how that
invariant stops being true.

**The test gap is worth more than the fix.** No test in the repo can
observe this class: `make conformance` and `test-substrate-fixture` both
run natively, and `test:wasm` builds `wasm32v1-none` but its fixture
(`fixtures/xqvm-wasm/src/lib.rs`) holds five smoke tests — push/add/halt,
calldata, roundtrip, stack underflow — none touching allocation or index
narrowing, and the job is path-gated so it usually does not run. A sweep of
`vm.rs` found 28 `usize::try_from` sites, 27 of which turn a failed
narrowing into a fault with identical identity on both targets, plus two
`(i64::BITS - leading_zeros) as usize` expressions bounded by 64 — so this
is currently the only instance. Nothing would report a second one.

## 4. `EXCLUDE` and `IMPLIES` charge unconditionally in Rust, conditionally
   in Python — FIXED IN FLIGHT BY !133, DO NOT FILE

**Evidence:** `xqvm/src/vm.rs:2470` (`exec_exclude`) and `:2498`
(`exec_implies`) both call `self.charge(pos, ...)` directly, charging
regardless of what the register holds. Python's `_runner_EXCLUDE`
(`executor.py:1296`) and `_runner_IMPLIES` (`executor.py:1305`) instead
call `self._charge_coefficient(reg, ...)`
(`executor.py:463-476`), which charges only `if isinstance(value, XQMX) and
value.is_model()` — nothing is charged for a `Sample` or an unset/wrong-kind
register.

**Failure scenario:** a tight budget with a `Sample` register in `r0`:
`EXCLUDE r0` raises `MemoryLimitExceeded` on Rust (it charges
unconditionally) and does not on Python (charges nothing, then faults on
`_get_register_as_xqmx`'s wrong-mode check further down, or succeeds if
the register happens to be model-shaped enough elsewhere) — a fault
divergence driven purely by register kind.

**Why out of scope for QUI-1178:** `_charge_coefficient` already precedes
the register read in both runners — the pop/charge/read order this ticket
fixes is already correct here. The bug is which existing helper Python
calls (`_charge_coefficient`, conditional) versus what Rust actually does
(`charge`, unconditional), not the order of operations.

**Status: closed by !133** (`feature/qui-1062-1029-1174`), which replaces
both calls with an unconditional `self._charge(...)` ahead of the register
read. Kept here as a record of the finding; no ticket needed. If !133 is
ever reworked or dropped, this becomes live again.

## 5. `SETQUAD`/`ADDQUAD`/`GETQUAD` type-check more loosely in Python than
   in Rust

**Evidence:** Rust's `exec_get_quad`/`exec_set_quad`/`exec_add_quad`
(`xqvm/src/vm.rs:2055-2062`, `:2077-2085`, `:2104-2112`) all resolve the
register with `as_model()`/`as_model_mut()`, which reject a `Sample`.
Python's counterparts (`_runner_GETQUAD`/`_runner_SETQUAD`/`_runner_ADDQUAD`,
`executor.py:1133-1158`) all call `_get_register_as_xqmx`
(`executor.py:544-549`), which accepts both `Model` and `Sample`.

(By contrast, `GETLINE`/`SETLINE`/`ADDLINE` are *not* part of this finding:
Rust's `as_xqmx_grid`/`as_xqmx_grid_mut` for those three explicitly accept
`model|sample`, matching Python.)

**Failure scenario:** `SETQUAD r0 ...` with a `Sample` in `r0` succeeds on
Python (mutating the sample's `quadratic` map) and raises `RegisterType`
("expected model, got sample") on Rust.

**Why out of scope for QUI-1178:** a register-type-acceptance divergence,
not a pop/charge/read ordering defect.

## 6. `require_model_mode` in `ATLEAST`/`ATLEASTW` has no direct Rust
   counterpart — open spec question

**Evidence:** `xqvm_py/executor.py:1367` and `:1407` call
`require_model_mode(model, ...)` explicitly. Rust's `exec_at_least`
(`vm.rs:2646-2653`) and `exec_at_least_w` have no equivalent explicit call
— but this is because Rust's `RegVal` splits `Model` and `Sample` into
separate enum variants (`xqvm/src/value.rs:119-131`), so `as_model_mut()`
already excludes a `Sample` at the type level; there is no separate "mode"
to check. Python's single `XQMX` dataclass conflates both under one type
with a `mode` field, so `_get_register_as_xqmx` (which accepts either)
needs the extra `require_model_mode` call to reject a `Sample` the same
way Rust's type system does structurally.

Whether this makes Python's explicit checks in `ATLEAST`/`ATLEASTW`
correct patches for a real gap (and `EQUALITY`, item 1, is the one still
missing its patch), or whether the whole `_get_register_as_xqmx` accepting
`Sample` in these grid/coefficient opcodes is itself the wrong design, is a
spec question, not a reorder.

**Why out of scope for QUI-1178:** design/spec decision, not an ordering
defect.

## 7. `conformance::Inputs` cannot express a memory limit

**Evidence:** `conformance/src/lib.rs:74-99` — `Inputs` carries `calldata`,
`output_slots`, and `step_limit`, with no `memory_limit` field.

Several of the route-B fault-ordering cases this branch introduced are
only reachable against a near-exhausted allocation budget (the
`MemoryLimitExceeded` row in `conformance/README.md`'s new "Fault ordering
(QUI-1178)" table: `VECPUSH`, `SETLINE`, `ADDLINE`, `SETQUAD`, `ADDQUAD`,
`EXCLUDE`, `IMPLIES`, `REDUCE`). Without a `memory_limit` field on
`Inputs`, cases that need a *small*, deliberately tight budget (rather
than a program engineered to overflow the default) can only be pinned as
per-VM unit tests (`xqvm_py/tests/test_executor.py::TestFaultOrdering`),
not as shared conformance vectors that exercise both runtimes identically.

**Suggested fix:** add `memory_limit: Option<u64>` (or a sentinel
"unset = default") to `Inputs`, plumbed to `Vm::set_memory_limit` on the
Rust side and to a `--memory-limit` flag on `python -m xqvm_py run` on the
Python side.

**Why out of scope for QUI-1178:** harness/tooling gap, not a runner bug.

## 8. An unset register produces different fault identities depending on
   which accessor the opcode uses

**Evidence:** Python's `_get_register_as_vec`/`_get_register_as_xqmx`
(`executor.py:537-549`) both call `self.state.get_register(slot)`, which
raises `RegisterNotFound` (`xqvm_py/state.py:244-246`) for an unset slot —
mapped to `Fault::UnsetRegister` (`conformance/src/lib.rs:424`).

Rust already has a helper that discriminates this correctly —
`Vm::require_vec`/`Vm::require_vec_int` (`vm.rs:895-921`) explicitly match
`RegVal::Unset` to `Error::UnsetRegister`, as the surrounding doc comment
says in as many words: *"folding 'never written' into the type arm makes
an unset register a `TypeMismatch` here and an `UnsetRegister` there."*
But `require_vec_int` is used by exactly one opcode, `SLACK`
(`vm.rs:1881-1882`) — `exec_equality`, `exec_at_least`, and
`exec_at_least_w` all resolve their vec registers directly through
`self.reg(...).as_vec_int()` (e.g. `vm.rs:2559-2576`), which does not
discriminate `Unset` and maps it to `Error::RegisterType` →
`Fault::TypeMismatch` instead. The same applies to `as_model_mut` for the
model register in these same opcodes and in `SETQUAD`/`ADDQUAD`/`GETQUAD`.

**Failure scenario:** `EQUALITY r0 r1 r2` with `r1` (indices) never
written raises `UnsetRegister` on Python and `TypeMismatch` on Rust.

**Why out of scope for QUI-1178:** VM-wide accessor inconsistency (some
opcodes already use the discriminating `require_vec`/`require_vec_int`
helper; most don't), not specific to the 22 opcodes this ticket reordered,
and not a pop/charge/read ordering defect.

## 9. The `test_pops_precede_the_register_read` AST guard can be defeated
   into silence, not into a false alarm

**Evidence:** `xqvm_py/tests/test_executor.py:3186-3221`
(`TestFaultOrdering._first_positions` and `REGISTER_READS`).

The guard walks each runner's AST, takes the source position of its first
call to `_get_register_as_xqmx`/`_get_register_as_vec` and the position of
its first call to `.pop()`/`.pop_n()`, and fails only when a read's
position is earlier than a pop's (`min(reads) < min(pops)`, in effect,
via `test_pops_precede_the_register_read`'s `read < pop` check). A prior
draft of this document described the guard's weakness as a *false hit* —
the guard incorrectly flagging correct code. That has the risk direction
backwards: because the check only ever compares the *first* read against
the *first* pop, everything that can go wrong with it makes the guard
under-report, silently passing a runner that violates the ordering the
spec requires. Three distinct ways to do that, none hypothetical:

1. **A `.pop()`/`.pop_n()` call on something that is not the operand
   stack, executed before the register read.** The guard matches on
   method name alone (`name in TestFaultOrdering.POPS`, keyed off
   `ast.Attribute.attr` or a bare identifier), not on the receiver. Any
   call named `pop`/`pop_n` anywhere in the runner — on a list, a dict, a
   local collection — lowers `min(pops)` below the register read's
   position and the runner passes, even though not one operand was
   actually popped off the VM stack first.
2. **A real stack pop, then the register read, then more stack pops.**
   The guard only compares the *first* pop's position to the *first*
   read's position. A runner that pops one operand, reads a register, and
   then pops its remaining operands has `min(pops) < min(reads)` and
   passes, even though the register read still runs before the operand
   stack is fully drained — exactly the ordering the spec forbids for any
   operand popped after that read.
3. **A register read through an accessor the guard does not know about.**
   `REGISTER_READS` hardcodes accessor method names, so a runner
   calling `self.state.get_register(...)` directly, or reaching a register
   through an accessor absent from that set, reads a register in
   a way the guard's `ast.walk` never recognizes as a "read" at all —
   `min(reads)` stays `None`, `_first_positions` returns no read position,
   and `test_pops_precede_the_register_read`'s `if read is not None and
   pop is not None and read < pop` guard clause skips the runner (neither
   branch of the `and` is true), so it is silently exempt rather than
   flagged.

The third was live when this was first written: `_get_register_as_model`
exists on `main` and `EQUALITY`/`ATLEAST`/`ATLEASTW` resolve their model
register through it. `REGISTER_READS` now lists it and
`_get_register_as_int` alongside the two vec/xqmx accessors, so the set is
the whole accessor family rather than a subset — but it is still a
hardcoded list, and a new accessor added without a matching entry is
silently exempt again. Blind spots 1 and 2 are untouched.

None of the three currently escapes the guard in `xqvm_py/executor.py`, confirmed
by direct inspection rather than assumed: every `.pop(`/`.pop_n(` call in
the file is `self.state.pop(...)`/`self.state.pop_n(...)` (the operand
stack, not some other object) and — separately — no runner interleaves a
pop after a register read today; no runner bypasses the `_get_register_as_*` helpers to call
`self.state.get_register(...)` directly. The guard's blind spots are latent, not yet triggered.

**Why out of scope for QUI-1178:** a test-harness gap in the invariant
guard added by this ticket, not an ordering defect in a runner it
currently covers.

## 10. `ONEHOTR`/`ONEHOTC` size their charge off a sample the Python runners
   accept — FIXED IN !134, QUI-1202

**Found in review of !134, 2026-08-28, filed as
[QUI-1202](https://linear.app/quip-network/issue/QUI-1202) and then fixed
in the same MR. Kept as the record of what was wrong; do not file it
again.** The fix adds `_peek_model_grid` (`xqvm_py/executor.py`) and moves
both ONEHOT runners to peek, charge, then `_get_register_as_model`. Pinned
by `TestFaultOrdering::test_onehot_does_not_charge_for_a_sample`.

The `EQUALITY` analogue is finding 1a, which !133 closed with
`_peek_model_size`. `ONEHOTR`/`ONEHOTC` were not part of that fix, which is
why this survived the rebase.

**Evidence:** `exec_one_hot_r` (`xqvm/src/vm.rs:2480-2493`) sizes the
charge from a peek — `match self.reg(reg) { RegVal::Model(m) => m.cols, _
=> 0 }` — takes `charge_equality_expansion`, and only then resolves the
register with `as_model_mut`, which rejects a `Sample`. So a sample
charges nothing and raises `RegisterType`.

`_runner_ONEHOTR`/`_runner_ONEHOTC` (`xqvm_py/executor.py:1341-1371`) have
no peek at all. Both go pop, `_get_register_as_xqmx` (which accepts a
`Sample`, `executor.py:611-616`), dimension check, then charge — so a
sample's real `rows`/`cols` size the charge.

**Failure scenario:** with a 3x3 `BSMX` sample in `r0` and a budget with no
headroom past the setup, `xqvm_py` raises `MemoryLimitExceeded` (240 bytes
against a 72-byte budget) where the Rust VM charges nothing and raises
`RegisterType` (`TYPE_MISMATCH`). Confirmed by running the program below
through both VMs.

At a generous budget the same program diverges for a second, separate
reason: Python raises `XQMXModeError` (`Fault::XqmxMode`) where Rust
raises `RegisterType` (`Fault::TypeMismatch`). **That one is not part of
this finding and was not fixed.** `spec/xqvm/SPEC.md:211` records
`XqmxMode` as the one unresolved row in the fault table — either identity
is defensible until the question is settled, and no vector may assert
either. What the fix removes is the budget's ability to change the answer:
the same fault now comes out at any budget, as it does on Rust.

**The verifier admits it, two ways.** `check_reads` requires `R::Model` for
`OneHotR`/`OneHotC` (`xqvm/src/verifier/reg_type.rs:186-190`), so a
register the verifier knows is a `Sample` is rejected statically. It does
not always know.

**Route 1 -- host calldata.** `Vm::set_calldata` (`xqvm/src/vm.rs:400`)
accepts any `RegVal`, samples included, and says so; `exec_input` clones
the entry into the register. `INPUT` writes `RegType::Any`, which
satisfies every requirement, so `PUSH 0 / INPUT r0 / PUSH 0 / PUSH 1 /
ONEHOTR r0` verifies clean and arrives holding whatever the embedder put
in slot 0. The `--calldata` CLI flag parses i64s and cannot express it,
which is what makes this easy to miss from the command line; the embedding
API is the reachable surface, and on a chain runtime the host fills those
slots. Confirmed post-fix with a 3x3 `binary_sample` in slot 0: the same
`XQMXModeError` at a 72-byte budget and at 2^30.

**Route 2 -- a branch join.** `meet_reg` merges two different concrete
types to `RegType::Any` (`xqvm/src/dataflow/register.rs:95-101`) and `Any`
satisfies every requirement (`reg_type.rs:69-72`), so a branch that writes
`BQMX` on one path and `BSMX` on the other joins to `Any` and verifies
clean using only its own instructions:

  ```asm
  PUSH 0
  INPUT r1
  LOAD r1
  JUMPI .0
  PUSH 9        ; selector == 0: r0 is a SAMPLE
  BSMX r0
  JUMP .1
  .0:
  PUSH 9
  BQMX r0
  .1:
  PUSH 3
  PUSH 3
  RESIZE r0
  PUSH 0
  PUSH 1
  ONEHOTR r0
  HALT
  ```

  `xquad verify --text` accepts this. `xquad run --text --calldata 0
  --memory-limit 80` raises `register r0 holds sample, expected model`.
  The same program on `xqvm_py` raised `MemoryLimitExceeded` before the
  fix and raises `XQMXModeError` at any budget after it.

**Fix as landed:** `_peek_model_grid` is the `(rows, cols)` twin of
`_peek_model_size`, gated on `is_model()`. Both runners now peek, charge
bytes and steps, then resolve with `_get_register_as_model`. The explicit
`rows == 0 or cols == 0` check went with it: `row_indices`/`col_indices`
raise the same `InvalidGridDimensions` through `grid_row_extent`, and
placing it after the charge is what `exec_one_hot_r` does (`grid_row_index`
follows `charge_equality_expansion`).

**Still open, deliberately:** whether Rust's peek should read `cols` off a
grid rather than a model is the same design question as finding 6, and the
`XqmxMode` naming question above is SPEC.md:211's. Neither is touched.

---

--- METRICS ---
- Files Extracted: 58
- Estimated Token Budget: ~131902 tokens
- Canonical Reference: https://codewiki.google/github.com/QuipNetwork/xq-rs
