All notable changes to this project will be documented here. Versioning follows
the source-of-truth package.json version.
Breaking (0.7.11 → 0.8.0): migration 021 drops circuits.crumble_url and
circuits.crumble_url_annotated, and the two keys are gone from the circuit YAML schema.
Nothing reads them any more — the link is derived where it is shown — but anything holding
a copy of the database or parsing the YAML will need the columns removed.
- The Cirq body format, all 718 files and 13 MB of it. A Cirq circuit's text form is a
per-moment ASCII grid whose width scales with the qubit count, so it grew fastest exactly
where it read worst — and it never said anything the STIM body does not say better. Gone
with it: the
stimcirqdependency (and itscirq-core,pandas,sympyandduettransitive weight), the CIRQ tab, and the.cirqextension from the two body-file readers. The format shortcuts are1/2now;3finds no tab and does nothing, which is what it already did past the last tab. - The stored Crumble URLs —
crumble_urlon 725 circuits andcrumble_url_annotatedon 398, 528 KB of YAML. A Crumble URL is a pure string transform of the body it shows: join the lines with;, abbreviateQUBIT_COORDS/DETECTOR/OBSERVABLE_INCLUDE, write the spaces as_. Storing it therefore kept a second, lossy copy of every circuit, and the copy drifted — 76 of the 725 links pointed at a body the page had stopped displaying, and 32 more dropped a syndrome round's own measurement.crumbleHrefinsrc/lib/stim-format.tsnow derives it: the detail page server-renders it from the body it already has, code pages fill it in when the row's bodies are lazy-loaded, and the Detectors switch moves it by re-deriving rather than by swapping to a second stored string. The transform is stim's own, checked byte-for-byte against all 1123 URLs that used to be stored. - Quirk URLs above 40 qubits — 28 links, 1.70 MB. Quirk's editor tops out around 16
qubits, so these were dead links, and the URL scales with width: one was 242,264
characters, and seven of them made up 91% of
/codes/144-12-12. The width gate was already in the pipeline (LARGE_CIRCUIT_MAX_QUBITS); the stored data predated it. Below the gate nothing changed — 697 circuits keep their Quirk link. - The
no-private-registriesCI job. It failed the build on anyjfrog.ioreference — a guard from the period when this code sat next to a private artifactory. Nothing in the repository has referenced one for a long time (git grep -F jfrog.iois empty), and both lockfiles resolve exclusively against public registries, so the job was spending a run on every PR to assert something no longer at risk.
- The Crumble link is gated on the length of the URL, not on the qubit count. It used to
be blanked past 40 qubits, which denied a link to every 144-qubit circuit in the library
while saying nothing about the number that actually matters.
CRUMBLE_MAX_URL_LENGTHis 65,536 characters — the narrowest ceiling among current browsers (Chrome accepts ~2 MB and Safari ~80 KB; Firefox is the binding constraint at ~64 KB) — so a link that is offered opens everywhere. 950 of 972 circuits get one, against 697 before; the 22 refused are all 720 qubits or wider, where the string runs to 232 KB plain and 782 KB with detectors. The cap looks at both views together, so the Detectors switch can never turn a working link into a broken one. /codes/144-12-12is 90% smaller: 1,259,209 → 124,946 bytes./codes/108-8-10, 781,983 → 124,974. The circuit pages follow:#27, 426,081 → 150,455;#163, 738,916 → 95,579.
npm test—crumbleUrlagainst stim's ownto_crumble_url(), over all 1646 committed bodies. The Crumble link is now derived by a hand-written reimplementation of a stim function, and nothing else in the repository compares the two: a stim upgrade, or an edit to the abbreviation table, would break every link on the site and pass every other check. A second assertion keeps the corpus a fixed point ofstr(stim.Circuit(...)), because the exactness only holds for canonical text — a hand-edited body using a gate alias (CORRELATED_ERRORforE) or carrying a#comment would derive a link for differently-spelled text. Runs in CI ascrumble-url-parity, node's own test runner, no new dependency.
-
A circuit row announced itself as a button that does something else. It carried
role="button",tabindex="0"andaria-expanded="false", so a screen reader said "button, collapsed" — and thenEnteron it navigated to the circuit page.l, Space and a mouse click expanded; the one key the announced role promises did not. That is WCAG 4.1.2: the name, role and state have to describe what the thing actually does, and here the role described a toggle that only some of the row implements.role="button"also carries ARIA's Children Presentational: True — a button's contents are a label, not controls — so the row's eight interactive descendants (the permalink, the favourite toggle, the detail link and five tag links) were stripped of their own roles and folded into the row's accessible name, a 149-character run of#190G: 112Q: 8D: 3Q: 7Circuit-Synth Zero State Preparation…1D-AODlogical-state:zero…. Every affordance in the row existed for a mouse and for nobody else. The row now claims nothing it does not do:role="group"with a name (Circuit #190: <name>), not a bare focusablediv. It still has to be focusable —j/kmove between rows by focusing them — and something focusable with no role announces as an unnamed container.groupnames it, and, unlikebutton, does not flatten what is inside it: all eight descendants are individually exposed again, and the row's own name is 76 characters that say which circuit it is.- The chevron is a real
<button>, which is the only element here that is a toggle, and it ownsaria-expandedplus anaria-controlspointing at the panel. Its name is fixed ("Circuit #190 details") rather than flipping between "Expand" and "Collapse":aria-expandedalready carries the state, and a name that repeats it says the same thing twice. It has no click handler of its own — the click bubbles to the row's existing one — so it cannot toggle twice, and there is one implementation, not two. - No documented keyboard behaviour changed.
Enterstill navigates,l/h/Space still expand and collapse,j/kstill move,fstill favourites, and clicking anywhere on the row still expands it.Enteron the chevron toggles instead of navigating, which is what a focused button should do.
-
Two page scripts selected the open row by its
aria-expanded, and would have died silently.[id] > .circuit-toggle[aria-expanded="true"] + .circuit-detailis how/codes/[code]and/searchfind the row that1/2/3,c/yanddact on, andcircuit-bodies-clientuses the same shape to load bodies for a row already expanded by a#iddeep link. Movingaria-expandedonto the chevron would have left all three matching nothing, with no error and no test to catch it. The row therefore mirrors the state indata-expanded, all three selectors read that, andlist-keynav'sexpandedAttris pointed at it on both pages. The split is deliberate rather than incidental: ARIA describes the control to assistive technology, a data attribute is what page scripts are allowed to depend on, and one function (setExpanded) writes both plus the chevron rotation so they cannot drift. -
Giving the panel an
idbroke the "collapse the other rows" loop, and only in the state nobody looks at.aria-controlsneeds a target id, and the loop found each other row's header withpanel.closest("[id]")— which starts at the element itself, so it now returned the panel, found no header under it, and left that row'sdata-expanded,aria-expandedand chevron all saying "open" while the panel was visibly shut. Two rows then matched the "open row" selector and1/2/3would have acted on the wrong one. It walks topreviousElementSiblingnow. Found by checking the collapsed row's attributes in a browser after the change, not by anything that would have shown on screen. -
Every keypress on
/searchthrew aTypeError.initCircuitActions()was called with no argument, sofindActiveContainer()readconfig.activeContainerSelectoroffundefinedon any key that reached it. The page still worked: the throw is confined to one of severaldocumentkeydown listeners, soj/k/Enter/f— handled separately byinitListKeynav— kept working, and typing in the search box never reached it at all (isInputFocused()returns first). What was missing was1/2/3,c/yandd, plus an error per keystroke in the console. That is why it went unnoticed./searchrenders the same expandableCircuitRows as a code page, so it now passes the same selector, and1/2/3,c/yanddact on the open row. NodownloadAllSelector: there is no such button on/search, and/api/downloadreads URL filter params rather than a query. Found byastro check, which had never run before this week. -
aria-expanded={String(open)}on the collapsible section typed asstringwhere the attribute acceptsboolean | "true" | "false". Astro renders the boolean correctly, so this was a type error rather than a rendered one — but it was the reason that file sat in the typecheck exclusion list. -
tsconfig.check.jsonis gone, andnpm run typecheckruns against the real config. It existed to hold two errors out of CI while the files they were in belonged to other open PRs — a debt register with two entries, both now paid. The full project type-checks clean: 92 files, 0 errors. Thecheck-jsonexclusion that existed only to parse it went with it. -
Seven accessibility defects, each measured in a browser rather than inferred from a linter. They had nothing in common but the failure mode: every one of them looked right to a sighted mouse user, which is exactly why none had been noticed.
- The focus ring was all but invisible.
ring-blue-500/40measured 1.64:1 against a row in light mode and 1.74:1 in dark — WCAG 1.4.11 asks 3:1 — and, worse, only 1.33:1 and 1.27:1 against the row's own resting border, so "focused" and "not focused" were nearly the same picture. Dropping the alpha leaves solid blue-500: 3.76:1 on white, 5.35:1 on gray-950, and 3.04:1 / 3.90:1 against the resting border. Six call sites, not the two that were first found:CircuitRow,CodeCard, the/searchquery and filter inputs, the/filter input, and the rowfavoritesbuilds client-side. On the inputs the ring also has the field's owngray-300/gray-700border beside it, and there it goes 1.12:1 → 2.55:1 (light) / 2.74:1 (dark) — short of 3:1 against that one edge, but the ring's other neighbour is the page background, which it clears. text-amber-600measured 3.20:1 on white, under the 4.5:1 body text needs, and neither of the two places that show it — the alpha superscript in the header (14px/600) and the active-sort label (12px/600) — is large enough to claim the 3:1 exception. Nowamber-700: 5.03:1 in light, and dark mode keepsamber-400at 11.69:1. Bumped at all ten call sites at once, because a token that means "this is the sort you asked for" stops meaning it the moment two shades are in play.- Two of those ten are links whose only hover feedback was that colour, so darkening
the resting state flattened the hover step: the header α and "Clear all active filters"
went from a 2.22:1 step to 1.41:1. Both now go to
amber-900on hover — 1.80:1, still a slight step — and, more to the point, underline. Dark mode never had a usable step either (amber-400→amber-200is 1.38:1) and gains the same underline. A hover affordance that exists only as a colour change is one an awful lot of people cannot see; this is the first of the two that does not depend on hue at all. /codesskipped fromh1straight toh3—CodeCard's heading, the only heading-order violation on the site. It is anh2now; the class list is unchanged and so is the rendering, since the card sets its own size and weight.ToolCardkeeps itsh3, which is correct:/toolshas a realh2above it.- Three
text-gray-500s on/searchhad no dark variant, measuring 4.16:1 againstgray-950. Every sibling paragraph in the file already haddark:text-gray-400, andglobal.cssstates the pairing as house rule, so these were omissions rather than choices. 7.74:1 now. - The format tabs said which format was active in colour only (WCAG 4.1.2) — the
server emitted plain buttons and the client rewrote
classNameanddisplay. They now carryaria-pressed, the patternCodeMatricesalready uses. The initial state is read off the visible body rather than assumed to be the first tab, because the tabs on code pages are cloned fromCircuitBodiesTemplateand start with no ARIA of their own. - The header quick-search was an incomplete combobox. The input had no
role="combobox",aria-expanded,aria-controlsoraria-autocomplete, and the results list had noid— which made thearia-activedescendantthe script had been writing all along point at nothing, so no screen reader ever announced the highlighted result. All five are wired up now, andaria-expandedtracks the dropdown. They are set from the script, not the markup: without JS there is no dropdown, and an input that claims to own a popup that cannot open is worse than a plain search field. Two consequences of making it a real combobox:aria-live="polite"came off the results list. It was there because nothing else announced the results — a reasonable call whilearia-activedescendantpointed at nothing. Now that the combobox announces the active option, the live region announces the list on top of it, and the user hears everything twice.role="listbox"and the label stay.- Tabbing out left
aria-expanded="true"on an input nothing was focused on.hide()was reachable from Escape, a click outside, and a query under two characters — not from losing focus. There is afocusouthandler now, guarded on a non-nullrelatedTarget: the result<li>s are not focusable, so pressing the mouse on one blurs the input with a nullrelatedTarget, and hiding there would take the item away between mousedown and the click that navigates.
/aboutre-documented the keyboard shortcuts and had drifted from them. It omittedHomeandEnd, and describedEscas collapsing the open row when it also closes the help overlay. Corrected in place. The duplication itself stays:KeyboardHelp's flat 17-row table and/about's topic-grouped prose are different shapes, and collapsing them into one source would cost more than it saves.
- The focus ring was all but invisible.
-
Writing about a Tailwind class put it back in the stylesheet. Tailwind v4 finds its own sources, which means it reads every tracked file — including prose that only names a class. The CHANGELOG entry above, explaining that
text-amber-600had been replaced, regenerated.text-amber-600{}; the design plans underdocs/were keepingdark:text-amber-300alive for a component that stopped using it months ago. The wasted bytes are the small half. The large half is that "the class is gone from the built CSS" stops being a way to check that a sweep across call sites was complete — and it fails in the direction that hides a missed one, which is how fourring-blue-500/40sites survived the first pass of the fix above. Both paths are now@source notinglobal.css. -
294 circuits displayed — and handed over on Copy and Download — a body missing its final measurement.
bodyForDisplaysubtracted the readout epilogue unconditionally, but a circuit with nostim-annotatedbody has no Detectors switch to bring it back, so for those the subtraction was permanent and invisible./circuits/662rendered two lines while its Crumble link opened the same circuit withM 10, and a user could download a circuit that no longer measured anything.stripReadoutnow subtracts only from a body that carries annotations — which is exactly the condition under which the switch exists. The 674 circuits that do have an annotated body are unaffected: none of the 63 prologue-only ones ends in a readout. -
The pre-commit hooks corrupted the repository, and nothing had noticed because nothing ran them. They were never wired into CI, so they only ever fired for someone who had run
pre-commit install— and on this repository a full run rewrites 2476 files and leaves Python that does not parse. Three separate causes, each now configured out in_typos.tomland.pre-commit-config.yaml:typosrewritesanctoand. It is how the importers spell "ancilla", 29 times, andfor row, anc in enumerate(...)becomesfor row, and in ...— a syntax error incircuit_validate.py,syndrome_extraction.pyand three of the flag-at-origin drivers. It also translates the German legal pages into English a word at a time (Sie→Size,oder→order), which would quietly damage a compliance document, and "corrects" thebainside the content-addressed digest18ed8bee546ba80fthat 32 circuits reference — breakingdb:create. Now: the German pages anddata_yaml/are excluded, and the QEC vocabulary (anc,ket,IZ), the author nameWilleand the citationLondare named. Notlocale = "en-gb", which sounds right for the prose and would rewrite Tailwind'stext-centerand the/favoritesroute: 178 findings become 2233.end-of-file-fixerandtrailing-whitespacerewrote 2476 circuit bodies..stim,.qasmand.cirqfiles are emitted byte-for-byte by the pipeline; a hook that edits them puts the stored body out of step with what the pipeline would produce. Excluded fromdata_yaml/.check-jsoncannot readtsconfig.check.json, which is JSONC — it carries the comments explaining which two files it excludes and why. Excluded.
-
/api/searchwas cached at the edge for a week, keyed on?q=.middleware.tsstampss-maxage=604800on any response without its ownCache-Control, which is safe for a page whose output is fixed at deploy time and not for one whose key space anyone can enumerate./searchhad set its own 600 s for exactly this reason since it was written; the quick-search behind it had not. It does now. -
A zip download only type-checked by accident.
/api/downloadhanded a NodeBufferstraight toResponse, whoseBodyInitwants a view onto a plainArrayBufferand not theArrayBufferLikeunion Node declares. The bytes were always right; the types said otherwise. It now copies into its ownUint8Array, which also stops.buffermeaning "Node's shared allocation pool" rather than "this zip". -
Renovate could auto-merge a moving target.
pyproject.tomlpinsmqt-qeccto a git branch, andlockFileMaintenanceruns withautomerge: true— so a weekend lock refresh landed whatever that branch pointed at, unread, in the ingestion pipeline. It already had: f476ffa1. Git-sourced dependencies anduv.lockrefreshes now need a human. -
.gitignoreignored a directory nobody uses. It listed.worktrees/, while agent worktrees are created under.claude/worktrees/— so every worktree showed up as untracked in the main checkout. Corrected, narrowly:.claude/itself stays tracked, because.claude/agents/and.claude/commands/are committed. -
Six documentation defects, five of them the same defect: a maintained count. CLAUDE.md said "1028 circuit files", "1019 of 1028" and "one paper backs up to 370 circuits"; each was right the day it was written and wrong within days, three times over as imports landed. Updating them would only reset the clock, so the counts are gone and the arguments they supported are stated without one — a paper backs "hundreds" of circuits, and widening
toric codeto "any term" matches "all but a handful" because "code" occurs in nearly every circuit's text. Same treatment for the stale counts in code comments (index.astro,CircuitRow.astro,add_circuit/annotate.py) and for the samplePapers: N, linked to M circuits.build output quoted indocs/database.mdand/add-circuit. -
CLAUDE.md's static/SSR page lists were wrong on the day they were written, and read as an inventory:
/favoriteswas missing from the static list,/toolsand five/api/*routes from the SSR one. Replaced with the rule that actually decides it — a route that reads SQLite isprerender = false— plus examples marked as examples and a pointer togrep -rn "prerender" src/pages/, which cannot go stale. -
The matrices lived in three places at once, on paper. They moved to content-addressed
data_yaml/matrices/<digest>.yamland no*.original.yamlhas existed since, butdocs/database.mdstill omitted the directory from its tree and filed matrices undercircuits/originals/,/add-circuitstill named*.original.yaml, anddocs/adding-circuits.mdcontradicted itself — line 444 against its own "Original submission" section. All four now match CLAUDE.md and the pipeline. -
"The pipeline always preserves the original" was never true. The flag gadgets are written directly with the pipeline helpers, so there is nothing earlier to preserve: they have no
circuit_originalsrow and no "Original submission" section, whichindex.astroalready knew and the docs did not. Corrected indocs/adding-circuits.md,docs/adding-circuits-agent.md,docs/database.mdand CLAUDE.md's schema comment. -
The QUITS README pointed at a file in an unmerged branch.
data-imports/autqec/find_sigma.pydoes not exist here — that import is still PR #127 — so a reader had no way to reproduce the two BB permutations. The README now says so outright and points at the matcher that is in the repo (scripts/add_circuit/find_sigma.py, withdata-imports/qldpc/find_sigma.pyas the worked example of driving it);sigma_precomputed.json's two provenance strings say the same. The permutations themselves are untouched, andadd_circuitstill re-verifies each by row-space equality on every run — which is what makes shipping them acceptable. -
The agent doc's "Current tags" table listed about a third of the vocabulary. It was missing seven code tags (
LDPC,topological,subsystem,bivariate-bicycle-code, …) and six circuit tags and families (gadget,partial-ft,x-type/z-type,distance:*,circuit-distance:*, …) while telling the agent it may only use tags that already exist. A second copy of a table that imports extend was never going to hold, so it is replaced by the query againsttags/taggingsthat returns the real vocabulary, plus the shape of it in prose. -
scripts/annotate_circuits.pydescribed itself as state-prep/encoding-only though it has dispatched tobuild_annotated_sefor syndrome extraction since that landed. Its docstring now covers both branches and why a round cannot be reset-free. -
A collapsed circuit row kept every one of its controls in the tab order. The panel is hidden with
max-height: 0andoverflow: hidden, which clips it visually and does nothing at all to focus — so a keyboard user tabbing down a code page fell through hundreds of buttons and links inside rows they could not see, with no way to tell where the focus ring had gone. Measured on the built site: 396 focusable controls inside the collapsed rows of/codes/steane-code, 1366 on/codes/flag-gadgets, and 7 in the closed mobile nav below 640px — now 0 in all three. The collapsed check-matrices section was the same bug (3 controls). The containers carryinert, and every handler that writesmax-heightnow writesinertbeside it; expanding a row makes its ten controls focusable again, collapsing takes them back out. -
66 of the 84 code pages shared a
<title>with another page. 17 groups of codes have the same bare name — nine are called "Balanced Product Cyclic Code" — so their tabs, bookmarks and search results were indistinguishable. The page already computed the disambiguatedcodeTitle(name plus[[n,k,d]]) and handed it to the JSON-LD; the<title>was the one place still gettingcode.name. All 84 code page titles are now distinct. -
The
<h1>ran the name into the parameters: "Steane Code[[7,1,3]]". Astro strips the whitespace between two adjacent expressions, and the measured gap was 0.00px. Laid out the same wayCodeCard.astroalready does it — flex with an explicit gap — which renders 8px of space. -
/favoritesstopped at 200 circuits and did not say so./api/circuitscapped the id list at 200 "to prevent abuse" while the client allows 5000 favourites and sends them all in one request, so a reader who favourited the whole library saw a fifth of it and had no reason to suspect the rest existed. It was never a URL-length guard either: 200 four-digit ids is ~1000 characters. The cap stays — an unbounded id list is a real way to make the server build an arbitrarily large query — but it is now 2000, comfortably above the library's 972 circuits and still inside Node's 16 KB header limit, and truncation is reported (X-Truncated) instead of hidden, with/favoritesshowing a notice naming both counts and pointing at "Export list". Verified: 400 ids in, 400 back; all 972 in, all 972 back; 2100 in,X-Truncated: true. -
"Download all visible circuits" was not true while the Favorites filter was on. That filter lives in localStorage and hides rows client-side, but
/api/downloadreads the query string — by design, and the contract is unchanged — so the zip still contained the hidden non-favourites. The label and tooltip now say so while the filter is active, and revert when it is switched off. The[data-list-count]label is untouched: it describes the URL filters, which the download does honour. -
/abouthotlinked its funding badge from img.shields.io — the site's only third-party request, on a page whose privacy policy discloses no external content, and one that reserved no space so the paragraph below it moved when the badge arrived. The badge is now committed atpublic/unitary-foundation-badge.svgand served locally with explicitwidth/height./aboutnow issues zero cross-origin requests. -
Dedup absorbed a subsystem code into a different one with the same centre. A code's
canonical_hashis taken over the gauge group precisely because two subsystem codes can share a stabilizer group and differ in what a decoder may measure — so Phase 1 of the scan separates them, and Phase 2 compared onlyhand merged them again. A [[9,4,3]] with Bacon-Shor's centre but a smaller gauge group came backmatch / existing / 9-1-3, its computed k=4 discarded. A candidate now has to agree on the gauge group too: presence (a subsystem code is not the stabilizer code at its centre, which also stops a genuine [[9,5,3]] being absorbed), rank, and row space under the same permutation the stabilizer search returned. A permutation that fits the centre but not the gauge is reporteduncertainrather than guessed at — the search returns the first σ that fits and does not enumerate the rest. -
validate_circuits.pyrecorded a "skipped" result and then crashed on it. Theinputs is Nonebranch of the encoder input-count check never returned, solen(None)raised, the blanketexceptdowngraded it to an "error", and a circuit that was merely unanalysable FAILED — exit 1 in CI. Dead against all 86 stored encoders, but a real regression: #136 rewroteelif len(inputs) != k:into anifand dropped the exclusivity. -
npm run generateproduced a treenpm run db:createrejects. It assembled thedata_yaml/files itself rather than going throughadd_circuit, and had drifted from it: a per-circuitoriginals/<stem>.original.yamlwith nooriginal_matricesreference — the exact shapecreate_database.mjsthrows on — aqec_idreallocated on every run against the documented "permanent, never reused" rule, and one filename for every--stimwhen--circuit-namewas omitted. It now delegates, so all three are gone by construction rather than by keeping two writers in step.--circuit-nameis required one per--stim, and--overwrite/--assume-newexpose the matchingadd_circuitarguments. -
24 circuits were missing their
.cirqbody. Eight codes from #44 were added with only.yaml/.stim/.qasm, beforeLARGE_CIRCUIT_MAX_QUBITSexisted to explain an absent one. At 15–37 qubits they are well under the threshold of 40. Regenerated from each circuit's canonical.stimwith the pipeline's own converter, so the bodies are what ingestion would have written. -
Re-running an import silently dropped its measured
circuit-distance:tags, and no document said so. All three syndrome-extraction importers hardcodeoverwrite=True, andadd_circuitcarries over onlyqec_id, so a re-import writes a freshtags:list — losing all 91 stored tags (47 asyndrome, 26 qLDPC, 18 QUITS). Nothing is unrecoverable:scripts/measure_circuit_distance.py --writestrips and re-measures, so one idempotent command restores them. It is now in the "Next:" recipe each importer prints, and each README has a "## Re-running" section saying what is lost and what is not —crumble_url_annotatedand thestim-annotatedbodies are regenerated byannotate_circuits.py, so nobody goes looking for a loss that is not there. -
The qLDPC rounds were never measured for circuit-level distance, and the docs say an absent
circuit-distance:tag means the search ran out of budget. It did not: 26 of the 28 settle, 20 of them in under a second.scripts/measure_circuit_distance.pyarrived in #137 and #136 had already merged, so nothing ever pointed it at them. Now tagged, which puts a number on the caveat those circuits already carry — the library states the strategy is not guaranteed distance-preserving, and [[4,2,2]], [[6,2,2]] and [[15,7,3]] come out atcircuit-distance:1: one fault anywhere in the round flips a logical without firing a detector. Two circuits exceed even a 400 s budget and stay untagged. -
Fifty-six flag gadgets were stored twice or more. A gadget depends on the stabiliser's weight, not on the code distance it was verified at, so the same file ships at every distance from the one it first appears in up to 11 — the weight-4 X gadget is byte-identical at d=3, 5, 7, 9 and 11 and was five circuits, distinguishable only by the number in the title. The distances are the information, not the copies: 20 circuits now carry several
distance:tags each, stay findable by filtering on any of them, and name the span (X-type weight-4 FT gadget (d=3-11)). 354 source files, 298 circuits. -
Five pairs of qLDPC rounds were identical without saying so. Colouring the X- and Z-check subgraphs separately returns the joint colouring, byte for byte, whenever the Tanner graph is small enough that the joint one already separates them — [[4,2,2]], [[6,2,2]], [[7,1,3]], [[15,7,3]] and the tetrahedral code, 5 of the 14 codes carrying both. Nothing on either page said so, which left two entries a reader had to diff by hand. Both are still stored: they are different algorithms, and on the other 9 codes they differ, the toric [[16,2,4]] by depth 4 against 9. Each now names the other strategy in its notes, so the agreement is a stated property of the code rather than an unexplained repeat.
-
The changelog claimed 61 of 127 circuits measured. That count came from a branch that also held the AlphaSyndrome import when it was not yet merged, and counted 127 circuits that were never all on
mainat once. With every round in place it is 87 of 155. Corrected, along with how many preserve the code's distance. -
circuit-distance:fell into the filter's "Other" group whiledistance:sits under Fault tolerance. Same question, different number — they belong together. -
Every page had two indexable URLs, and the prerendered ones canonicalised to the URL the sitemap does not list.
astro.config.mjsset notrailingSlash, so Astro defaulted to"ignore"andLayout.astroechoed back whatever path the request carried./codesand/codes/both answered 200 and each named itself canonical — one page, two self-canonicalising URLs — while/aboutbakedhttps://qecirc.com/about/against asitemap.xmlthat listshttps://qecirc.com/about. NowtrailingSlash: "never", and the canonical is normalised rather than echoed, so it holds for an SSR page whose request the router did not shape. Crawled before and after on a built server, every route in both forms: each slash-free form answers exactly as it did, and all 16 trailing-slash variants — 12 of which used to answer 200 as a duplicate, 4 the 404 body — now301to the slash-free URL with the query string intact (/search/?q=steane%20code&tag=x→/search?q=steane%20code&tag=x). Canonical,og:urlandsitemap.xmlnow agree on one URL per page, prerendered and SSR alike. -
The site-wide
SearchActionadvertised a pathrobots.txtforbids.Layout.astrotold every crawler it could search at/search?q={search_term_string};robots.txt.tsdisallows/searchfor every agent, and deliberately — the?q=key space is unbounded. Both landed in0abfe8c5, whose PR justifies theDisallowwithout mentioning the markup it contradicts. Removed rather than reconciled: Google retired the sitelinks searchbox this fed in November 2024, so the four lines were inert as well as wrong. -
/toolsshipped no structured data though it is the exact sibling of/codes, which has carriedCollectionPage+ItemList+BreadcrumbListsince the JSON-LD went in — and CLAUDE.md asks for matchingjsonLdwhen an entity page type is added. It now carries the same graph, enumerating the unfiltered 11 tools:?tag=filters that page on the server but every variant canonicalises to/tools, so the enumeration has to describe that one URL. Tools have no page of their own, so each item links to its anchor (ToolCardalready rendersid={tool.slug}). Not added to/search: it isDisallowed, so nothing would ever read it. -
Codes whose name already carries their parameters printed them twice — "Gottesman [[8,3,3]] Code [[8,3,3]]", "[[20,2,6]] Code [[20,2,6]]". The guard tested
name === params, which only catches a name that is nothing but its parameters: of the 5 codes whose name contains them, 3 are exactly them, so 2 doubled. It had been written out by hand at eight call sites — the<title>, the<h1>, the meta description, two JSON-LDnamefields, the code cards,llms.txt, the quick-search JSON and the/searchcode filter — and correcting them one at a time is how the first pass fixed three and left the page they link to still saying it twice. There is now one predicate,showCodeParams/codeDisplayNamebesideformatCodeParams, and all eight sites call it. Verified on the built server: all 84 code page titles,/codes, its ItemList,llms.txt,/api/searchand the/searchfilter — 0 doubled, with "Steane Code [[7,1,3]]" and the bare "[[16,2,4]]" both still rendering exactly once. -
/searchprinted two different empty states, three lines apart — "No circuits found for X" from the result count, then "No circuits match this query." from the empty state proper. One empty state now, the centred one, and it carries what the count line used to: the query as actually resolved (a correction rewrites it), whether the filters are to blame, and the way out of each. The three result-set notices are suppressed when there is no result set — "Showing results for steane." sitting directly above "No circuits match" told the user two opposite things — and the escape hatch from a correction moves into the empty state, which is the only thing rendered in that case. -
The header quick-search could show results for a query the user had typed past.
doSearchawaitedfetchandres.json()and wroteinnerHTMLunconditionally: no ordering guard, nores.okcheck, nocatch— and it is called from asetTimeoutwith its promise dropped, so a 5xx (which answers with an HTML error page, makingres.json()reject) threw where nothing could catch it and left the dropdown frozen on stale results. A generation counter now discards any response that is not the newest, and a failed request leaves the previous list up for the next keystroke to replace. Dismissing counts as newest:hide()bumps the counter too, so a response still in flight cannot reopen a dropdown closed with Escape or an outside click, nor answer a query backspaced below the two-character floor. Verified in the browser by holding responses open and releasing them in a chosen order: the stale response never wins, a dismissed one leaves the list hidden and untouched, a 500 changes nothing and logs no unhandled rejection, and the next keystroke recovers. -
aria-activedescendantoutlived the option it named. Arrowing to the third result and then typing on left the input pointing atsearch-result-0-2after that node had been removed with the rest of the list — the attribute is cleared alongsideactiveIndexnow. -
Ten unguarded
localStoragecalls, where a denied store throws on read. Chrome's "block all cookies" and some embedded webviews make every access raiseSecurityError, so these were not lost preferences but thrown exceptions mid-module. The worst sat incodes/[code].astro: the favourites-filter read is above the "download all" wiring, so a browser that refuses storage lost the download button, the keyboard shortcuts and the tag dropdowns with it.src/lib/safe-storage.tsnow wraps get/set — a read answersnull— and the twois:inlinescripts that cannot import it (the theme, the search-filter disclosure) inline the same try/catch. Proved by serving the built site through a proxy that makeslocalStoragethrow before any page script runs:/codes/steane-coderenders all 81 rows with zero page errors, the favourites filter, theme toggle and tag-view toggle all still respond, and "Download all" still fetches/api/download?code=steane-code.A write reports whether it landed, and favourites act on that. Swallowing the failure is right for a theme or a tag view and wrong for the user's own list:
importFavoritescounts a merge it performs in memory, so a dropped write would have toasted "Imported 3 new favorites" and calledlocation.reload(), landing the user back on an empty page with nothing to explain it — and a heart would fill for something never stored.setItemreturns a boolean, favourite writes turnfalseintoStorageBlockedError, and the message says what actually happened ("this browser is blocking site storage") instead of the file-format error the import's catch-all would have shown. The toast is raised infavorites-clientrather than at each call site, so the hearts inCircuitRow.astroexplain themselves too. Verified under denied storage: the import toasts the storage message, does not claim a count and does not reload; the hearts on a code page and on a circuit page stay in their old state. Verified with storage working: the import still stores[101,102,103], reloads, and renders 3 rows, and a heart still round-trips.
- Four gaps closed in what CI actually checks.
- A
pre-commitjob. The hooks now run on every PR instead of only for contributors who installed them, which is what let the corruption above sit unnoticed.prettieris skipped — it is the one hook needingnode_modules, andqualityalready runs the same check. ruffcoversdata-imports/. Every importer lives there, it is where the ingestion bugs have actually been, and it was outside both the lint and format gates. Clean when widened, so it costs nothing now and stops the next drift.- The coverage gate measures five scripts, not one, and asks for 80% rather than 60%.
It watched
scripts/add_circuitalone, leaving the four top-level maintainer scripts unmeasured — includingvalidate_circuits.py, which CI runs as its own step and which was carrying a live bug. All five together come to 85%. stim-annotatedbodies must be a fixed point of the annotator. They are derived from their circuit but committed rather than built, so editing a circuit without re-running the annotator ships a memory experiment describing the previous schedule, and nothing else in CI would ever notice.- The two whole-library sweeps now run as their own jobs, in parallel. Each parses
every circuit through stim and they share nothing but the checkout, so as steps of one
job they added up: 78 s of tests + 246 s validating circuits + 236 s checking annotated
bodies, a 9m35s job. Split, the wall clock is the slowest of the three — about 4
minutes, quicker than the workflow was before either check existed — for two extra
uv syncsetups at ~8 s each.
- A
npm run typecheck, because nothing in CI had ever read a TypeScript type.astro buildtranspiles without checking andeslint.config.mjsuses the non-type-awaretseslint.configs.recommended, so all ofsrc/was compiled and shipped on trust. The new script runsastro checkas its own CI step, and found three errors onmain— one of them a page whose keyboard shortcuts have simply never worked. Two of the three live in files owned by other open PRs and are excluded, individually and by name, intsconfig.check.json; that file is a debt register with two entries, not an ignore list.typescriptbecomes a direct dev dependency in the process, instead of whichever majorastroandtypescript-eslinthappened to hoist between them.- AlphaSyndrome syndrome-measurement schedules (55 of them), imported from
acasta-yhliu/asyndrome
(arXiv:2601.12509, ASPLOS '26) by
data-imports/asyndrome/. 12 new codes come with
them — hyperbolic surface ×6, hyperbolic colour ×3, plus the [[61,1,9]] colour code,
the 5×9 rotated surface code and a self-dual bivariate bicycle code. Every schedule is
scored against a named decoder, so
decoder:is what separates two otherwise identical rounds for one code.- 10 of the dataset's 69 schedules do not survive validation and are left out —
every
google.jsonand everytrivial.json. Their X- and Z-checks interleave so that two ancillas sharing two data qubits come out entangled, which makes each outcome individually random even though every check is applied exactly once. The import README traces it to the hard-coded tick tables inasyndrome/special.pyand measures what it costs in the paper's own metric. Their author has since confirmed the two reference schedules were not meant to stand as correct, and points to arXiv:2602.09099 for one that is. - The two defect surface codes and their 4 schedules are left out too, at the author's request: they were exploratory, not a result of the paper.
- One code is stored with a distance the dataset does not give.
code_distance.pycomputes the exact CSS distance by exhaustive enumeration, so it is a distance rather than a bound: the self-dual bivariate bicycle code shipsd: -1and is [[42,6,6]]. - The hyperbolic surface [[36,8,4]] shares
nandkwith the QUITS balanced-product36-8-4-bpcand nothing else: row-space ranks 11/17 against 14/14, which no relabeling reconciles. It gets its own entry, with the refutation recorded next to theassume_newthat allows it.
- 10 of the dataset's 69 schedules do not survive validation and are left out —
every
- Syndrome-extraction circuits are a supported kind. The pipeline could describe an
encoder and a state prep; a round of syndrome extraction fitted neither, and there was
nothing to check one against.
validate_syndrome_extraction_h(scripts/add_circuit/circuit_validate.py) — a round must measure exactly the stabilizer group and leave the encoded state, logicals included, alone. It cannot be a codespace test the way the prep and encoder checks are: a round acts on an already-encoded state, so there is nothing to simulate it on. It is a stabilizer-flow check instead, and the ancilla-to-check correspondence is derived rather than assumed.build_se_round(scripts/add_circuit/syndrome_extraction.py) — ticks of (data, ancilla, basis) into one canonical round, refusing a tick that reuses a qubit or an ancilla asked to carry both bases.round_check_matrix— which operator each measurement reads, which is what placing detectors needs. Deliberately narrow: no map is better than a wrong one, and nothing in validation depends on it.- A round's
stim-annotatedbody is the memory experiment it belongs to — reset the data,REPEAT dof the round, terminal readout, detectors, observable. Unlike a prep or an encoder, a round is not reset-free and has no tableau, so none of the derive/fit machinery may be pointed at it. schedule:anddecoder:join themethodtag category. They pair up: a schedule found by search is co-designed with the decoder it was scored against, so two rounds for one code can differ only bydecoder:.scripts/add_circuit/find_sigma.py— the codeword-based permutation matcher, for codes that match a stored one on every cheap invariant but defeatfind_code_permutation's budget. Written for the asyndrome import; three importers need it, so it lives in the shared package rather than in one of them.
-
Bacon-Shor [[9,1,3]], [[16,1,4]] and SHYPS [[49,9,4]], the library's first subsystem codes, now that #144 gives them somewhere to live.
code.matrixis qLDPC's gauge group, so the importer passes the stabilizer group (get_stabilizer_ops()) ashand the gauge group alongside it; k comes outn - rank(h) - gauge_qubitsrather thann - rank(h), which is the difference between [[9,1,3]] and [[9,5,3]].- An encoder and a |0> prep each. qLDPC's memory experiments do not support subsystem
codes, so the two syndrome-extraction strategies report
unsupportedfor them rather than silently producing nothing. - A subsystem encoder exposes
k + gauge_qubitsfree inputs, notk— five for Bacon-Shor, one logical and four gauge.logical_input_countand the annotator both compared againstkalone and refused every such circuit. Both now add the stored gauge count. Deliberately added to the stored k rather than derived fromh: the latter is the same number but would compare the circuit against itself and stop catching a stored k that disagrees with the code, which is what the check is for. add_circuit(gauge=...)passes the group through; #144 stopped atcompute_code_data_h.
- An encoder and a |0> prep each. qLDPC's memory experiments do not support subsystem
codes, so the two syndrome-extraction strategies report
-
Subsystem codes can be stored (
data/migrations/020). A subsystem code is described by two groups, not one: the gauge group a decoder may measure, and the stabilizer group — its centre — whose outcomes are deterministic. The difference between them is real qubits,(rank(G) − rank(S)) / 2of them, which carry no information and are not corrected.The library read k off a single check matrix as
n − rank(h), which counts those gauge qubits as logical ones: Bacon-Shor [[9,1,3]] would have been stored as [[9,5,3]] and SHYPS [[49,9,4]] as [[49,25,4]], so both were excluded rather than recorded wrongly. Codes now carry an optionalgaugematrix andgauge_qubitscount, andk = n − rank(S) − (rank(G) − rank(S)) / 2which for a stabilizer code has a zero gauge term and is the formula already in use — one implementation, not two.
hstill means the stabilizer group, for every code. Validators, dedup, the CSS split and the matrices view are untouched: a circuit is checked against the operators that are actually deterministic, which is what those checks want. Stabilizer codes gain no fields at all.- Logicals are the bare ones.
_compute_symplectic_logicalstakes the kernel of the gauge group rather than ofhwhen there is one, so they commute with everything a decoder may measure, while still quotienting by the stabilizers. - Identity is the gauge group's. Two subsystem codes can share a stabilizer group and
differ in gauge group, so
canonical_hashis computed over the gauge group when there is one — it determines the stabilizer group as its centre, so it is the more informative of the two. - Tagged
subsystem, so a reader who sees [[9,1,3]] over a rank-4his told why rather than left to think it a bug.
Verified against qLDPC's own
get_code_params()on Bacon-Shor(3), Bacon-Shor(4), SHYPS(3) and Steane. Importing the codes themselves is a follow-up.
-
Submitted check matrices are stored once instead of once per circuit. They are a property of the code and the labelling it was submitted in, so every circuit of one code wrote the same bytes — 479 files collapse to 68. They now live in
data_yaml/matrices/<digest>.yaml, content addressed, with the circuit YAML naming the one it uses viaoriginal_matrices. The original circuit stays beside the circuit, since that genuinely differs.db:createresolves the reference and inlines both halves, socircuit_originals, the API and the circuit page are unchanged. -
Large matrices are written sparsely. Above
SPARSE_MIN_ENTRIESa matrix is stored as the nonzero column indices of each row rather than dense 0/1 rows.his (n-k) x 2n, so a dense encoding costs O(n^2) characters however sparse the code is. Small codes stay dense deliberately — they are the ones a person reads, and the threshold sits above every code the library had before the qLDPC imports, so no existing small file is rewritten. Readers callmatrix_format.decode(Python) ordecodeMatrix(scripts/matrix-format.mjs) and cannot tell which was used.On the current data
data_yaml/goes 35.2 MB to 24.2 MB, all of it from sharing:circuits/originals/drops 15.7 MB to 0.3 MB and the sharedmatrices/costs 4.3 MB back. The sparse encoding saves nothing today and is meant to — no code the library has reaches the threshold, and rewriting the small files a person reads would be a cost with no benefit. It is what makes the qLDPC codes affordable when they land: on a single [[1428,184]] code it is worth more than this whole repository.db:createrefuses to build a circuit that still carries a per-circuitoriginals/<stem>.original.yamlwith nooriginal_matricesreference. Merging a branch written before this format produces no git conflict — the old file simply reappears — and the build would otherwise store null matrices and say nothing.scripts/migrate_matrix_storage.py --writeconverts such a branch.
- Importing a circuit no longer costs a full re-read of the library. Dedup compares a
submission against every stored code, and re-parsed all of them from YAML on every
call — so a bulk import paid O(library) per circuit and got slower as the library grew.
Measured on the 74-code, 43 MB library the qLDPC imports produce: 29.3 s per
circuit. Parsed codes are now cached across calls, keyed on each file's
(mtime, size) so a code written mid-import is still picked up —
add_circuitcreates code files as it goes, and a cache that could not see them would be worse than none. Every circuit after the first now pays astat. Sparse storage compounds it: the same first parse drops to 3.6 s. - The submitted-order logical operators are no longer recomputed from scratch.
canonical_formonly permutes columns and row-reduces, neither of which changes the code, so the canonical logicals permuted back are the originals — worth ~25 s per circuit on a [[544,80]] code, and it makes the two sets agree about which logical qubit is which, where a second independent computation would pick some other equally valid basis. The permuted operators are verified against the submitted matrices before use, with recomputation as the fallback. - A non-CSS circuit for an existing code is filed under that code's slug. On a dedup
match the CSS path has always used the stored slug; the non-CSS path only used it when
nothing else had set one, so
code_slug— or a slug derived fromcode_name— won instead. A five-qubit-code circuit submitted withcode_name="Five-Qubit Perfect Code"was written asfive-qubit-perfect-code--<circuit>.yamlwhile the code itself lives atfive-qubit-code; no code YAML was written under that name, because there was no new code, so the circuit referenced an entry that does not exist —annotate_circuits.pyreportedcode '...' not foundanddb:createrejected it.code_slugis documented as naming a new code, and on a match there is no new code to name.
-
Every qLDPC circuit records the one-liner that rebuilds it, at the end of its notes:
Reproduce with: qldpc.circuits.get_encoding_circuit(qldpc.codes.ToricCode(4), only_zero=True). Asked for by the library's author (qLDPCOrg/qLDPC#554) — a reader who wants to rebuild one should not have to reverse-engineer the constructor from[[n,k,d]]. The snippet cannot drift from the circuit, because the constructor string is what the importer evaluates:CodeSpec.constructorreplaced the lambda it used to hold, andCodeSpec.build()evaluates it. A snippet that stopped building the code would fail the import rather than mislead a reader. -
qLDPC circuits (58 of them), imported from qLDPC by data-imports/qldpc/: 28 syndrome-extraction rounds in two edge-colouring schedules (arXiv:2109.14609), plus a tableau encoder and a tableau |0> preparation for each of 15 codes. Six new codes come with them — the Iceberg codes [[4,2,2]] and [[6,2,2]], toric d=6, two hypergraph product codes [[58,16,3]] and [[241,121,3]], and a toric [[16,2,4]] that is not the stored one (its X row space has 16 weight-6 codewords where the stored code has none, and weight enumerators are permutation invariants).
- Upstream states both syndrome-extraction strategies are not guaranteed distance-preserving or fault-tolerant, and that its encoders are not fault-tolerant. That claim is reproduced in each circuit's notes rather than dropped: the library tags what its sources claim, and here the source claims the opposite.
- Subsystem codes are excluded, not broken.
BaconShorCodeandSHYPSCodevalidate fine against their stabilizer group, but the library derives k as n - rank(h), which is only correct for stabilizer codes — Bacon-Shor [[9,1,3]] would be stored as [[9,5,3]]. They need a gauge-group field and a k that is not derived.
round_check_matrixreads rounds that hold their ancillas in the X basis (RX...MX, with Z-checks asCZ), not only the Z-basis form. The two are the same construction in different frames; before this, every X-basis round returned no check map and silently lost its annotated memory experiment. It now tracks the basis each ancilla is prepared in, pulls back that operator rather than alwaysZ, and requires the measurement basis to match the reset basis — a round reset in X and read in Z has a random outcome however correct its check pattern looks.- QUITS syndrome-extraction schedules (72 of them), imported from
QUITS
(arXiv:2504.02673, Quantum 9, 1931 (2025)) by
data-imports/quits/. These are the library's first
circuits for balanced-product, hypergraph-product, lifted-product and
lift-connected-surface codes — 23 new codes, from [[36,8,4]] to
[[1428,184,<=24]] — and its first competing schedules for the same code from an
independent source: a bivariate bicycle round is 7 CX layers under the code's own
schedule and 12 under ZX-coloration.
- QUITS ships no circuits; it is a generator, so the importer calls it. One stored
circuit is one round, lifted out of the memory experiment's
REPEATblock. Thecustombivariate bicycle schedule needed two normalisations, both identities on a steady-state round: it renumbers the qubits (X-checks first) and ends onMR. - Source is the paper that defines the schedule, not the code: only
cardinaland its N/S-merged variant are QUITS' own, while ZX-coloration is arXiv:2308.08648 and the bivariate bicycle schedule is arXiv:2308.07915. - Schedules are tagged
schedule:interleavedorschedule:xz-separatedby measuring the emitted circuit — reading the check type off each two-qubit gate and asking whether any tick mixes them — rather than by trusting the strategy's name. The source's own vocabulary lives in the circuit name instead. - One extra seed is imported, and only one. QUITS' cardinal strategies take a
seedthat reorders the schedule at identical depth and gate count, and its author suggested varying it. Sweeping 12 seeds over the five cardinal-family codes whose circuit distance is measurable found the default already at the best distance available everywhere but one — and the two lift-connected-surface codes emit only two distinct schedules however it is set. So the import adds exactly the improvement: BPC [[36,8,4]] cardinal at seed 2, circuit distance 4 against seed 1's 3, same depth and same 216 two-qubit gates.EXTRA_SEEDSin the importer records it; a variant that measures the same as one already stored earns a reader nothing and costs them a row. - Three codes share [[n,k,d]] with an unrelated stored code and are refuted rather
than merged into it: BPC [[36,8,4]] against the hyperbolic surface code (row-space
ranks 14/14 against 11/17), BPC [[108,8,8]] against the bivariate bicycle
[[108,8,10]] (different distance), and BB [[90,8,10]] against the stored
90-8-10— enumerating every weight-4 vector against each X row space gives 0 codewords here and 90 there, and the weight enumerator is a permutation invariant. All three get distinct slugs. The last is filed as90-8-10-autqec, the slug autqec's import (#127) gives the same code, so whichever lands first creates the single shared entry; its name, Bivariate Bicycle Code (Bravyi Table 3), says what distinguishes it from the stored entry carrying the "(15,3) BB6 code" alias.
- QUITS ships no circuits; it is a generator, so the importer calls it. One stored
circuit is one round, lifted out of the memory experiment's
circuit-distance:<N>on syndrome-extraction circuits — the fewest faults anywhere in the round (gate, idle, reset or readout) that flip a logical while firing no detector, measured rather than cited. It sits next todistance:<N>, which is the code's distance and usually a larger number: 13 of the 87 circuits measured so far preserve it, the rest lose at least one step to hook errors. Filter on it like any other tag.scripts/measure_circuit_distance.pywrites it;scripts/add_circuit/circuit_distance.pyis the measurement. The search is stim'ssearch_for_undetectable_logical_errorsover ad-round memory experiment under uniform circuit-level depolarizing noise — every gate, every idle qubit each tick, every reset and every measurement. Cross-checked against QUITS'examples/circuit_distance_search.py, which computes the same number the same way: identical answers on BPC [[36,8,4]] across seeds.- Both memories are measured and the smaller wins. A CSS code's Z and X
experiments fail at different weights — the rotated surface code d=5 under the
depth-optimal schedule survives 5 faults in Z and 3 in X — and the Z number is not
even bounded by
d, since a Z observable is flipped by X errors. Measuring Z alone would have called that schedule distance-preserving and putcircuit-distance:6on adistance:5code.build_annotated_segained abasisargument for this; the stored Z bodies are byte-identical. - The search cost grows with
nand, harder, withd: 91 of the library's 159 rounds settle inside a 120 s budget.dis what hurts — [[241,121,3]] takes 3 s and [[49,1,7]] runs out. An absent tag means not measured, never "no faults found".
- A round built from two sequential sub-rounds gets its annotated view.
round_check_matrixanswers which operator each measurement reads, and it pulled every measurement back through the same whole-round unitary — so a round that resets and reads the Z-ancillas and only then the X-ancillas was refused outright, costing all 25 ZX-coloration circuits theirstim-annotatedbody and Crumble detector view. Each measurement is now pulled back through the gates that precede it. A later sub-round's operator picks up support on the earlier sub-round's ancillas unless it cancels — which it does exactly when the checks commute — so a schedule where they do not is still refused rather than given a wrong detector. Requiring each ancilla to be reset before any gate touches it is what makes the pull-back past the reset legitimate.
- A code too large to display offers its matrices as a download instead of
rendering them. The stored
his (n-k) x 2n, so the lifted product code [[1428,184,<=24]] is 3.6M entries and ~14 MB of JSON — neither readable as text nor worth sending to everyone who expands the section. BelowMATRIX_INLINE_ENTRY_LIMITnothing changes; matrices were already fetched lazily, so no page ever shipped them in its HTML.
- Search aliases (
data/migrations/017). Codes and tools gain optionalaliases:, and codesrelated:, hand-written with the Error Correction Zoo as reference.laflamme→ the five-qubit code,kitaev→ surface codes,bb codes→ 28 circuits instead of 824.relatednames a different, adjacent code (toric→ planar surface codes) and/searchsays so rather than silently equating them. See docs/superpowers/plans/2026-07-15-search-aliases.md. - Code tags reach
/search(data/migrations/019).circuit_search.tagswas built only fromtaggable_type = 'circuit', so every code-level tag was invisible:LDPC,topological,self-dual,surface-codeandcolor-codeeach matched zero circuits despite 4 codes being tagged LDPC and 10 surface-code. They now ride in their owncode_tagscolumn — separate fromtagsso a name used at both levels cannot double-count. It failed silently becausesearch_termsalready indexed code tags, so spelling correction consideredtopologicala real word and left the dead query alone. scripts/add_paper.py— fetches paper metadata from arXiv/Crossref intodata_yaml/papers/, so it is never hand-written or recalled from memory.npm run papers:add -- <arXiv id | DOI | link>for one;npm run papers:missingscans every circuitsourceand fetches whatever has no paper yet (idempotent — the one to run after a bulk import). Refuses to write a record with no authors rather than emit a half-citation. Stdlib only, no new dependency.- This is the only part of the repo that touches the network, and it is a
maintainer tool whose output is committed — the same shape as
annotate_circuits.py.db:createand the site read committed YAML only and build identically offline.
- This is the only part of the repo that touches the network, and it is a
maintainer tool whose output is committed — the same shape as
- Papers are searchable. A circuit taken from a paper can now be found on
/searchby the paper's title, authors and arXiv id — none of which existed anywhere in the library before, sincecircuits.sourceholds only a link. Searchingreinforcement learning,Forlivesiorboolean satisfiabilitynow finds the circuits from those papers.- New
data_yaml/papers/(7 files) and apaperstable, withcircuits.paper_id. - Circuits do not declare their paper.
db:createresolvessourceagainst each paper'surl/arxiv_id/doi, tolerating http/https, trailing slashes,dx.doi.org,/pdf/vs/abs/, and arXiv version suffixes. Adding a paper file enriches every circuit citing it; no circuit YAML changes. 724 of 833 circuits link; the remaining 109 carrysource: circuit-synth, a tool rather than a work. circuit_searchgains a weightedpapercolumn (migration 017 recreates the FTS5 table — it has noADD COLUMN), andsearch_termsindexes paper titles and authors so spelling correction knows author names.- Paper text is
/search-only. The header quick-search is for jumping to a known thing, and one paper backs up to 370 circuits.
- New
- BM25 weights and the strict column set are derived from the table
(
src/lib/queries/search-schema.ts), not restated by hand. Both drifted fromcircuit_searchwhile this branch was being built — a 7-entry weight array for 8 columns (silently scoringpaperasnotes), and a strict set missingpaper(which announced "no code goes by that name" for an exact author match). Weights are now keyed by column name and ordered fromPRAGMA table_info; a column with no weight throws and names itself instead of being silently scored 1.0.scripts/smoke.shadditionally asserts that a query reaching each ofaliases,code_tagsandpaperstill returns circuits. - A circuit's source now renders as a citation ("Zen et al. (2024) · arXiv:2402.17761")
instead of a bare URL, on circuit rows, detail pages and the cite toast. Detail pages
emit the paper as a schema.org
ScholarlyArticlewith its real authors, rather than a URL string. - The landing page's "Papers" stat counts the
paperstable instead of inferring papers from distinctsourcevalues that are not a tool's own links. Same number today, but it no longer guesses.
- The
Circuit-Synth Encoding (Gate-Optimized)circuit for the Gottesman [[8,3,3]] code (#134) — it did not encode the code it was filed under. PropagatingZthrough it landed only 1 of 8 single-qubit operators inside the code's stabilizer group where 5 (n−k) were expected, and 2 of its 5 stored stabilizers came out genuinely random on the|0…0⟩run — checked sign-free, so not a Pauli-frame difference. Its three sibling encoders are correct and remain.#134is retired, not reused. Surfaced by the detector work: the logical-input derivation is basis-independent, so it can check non-CSS codes thatvalidate:circuitsskips (see below).
- Qubit coordinates survive ingestion
(#107).
QUBIT_COORDSreached the stored body intact only when a circuit had TICKs; otherwise compaction kept the instruction and dropped its arguments, turningQUBIT_COORDS(3, 7) 0into a meaningless bareQUBIT_COORDS 0. That path runs for TICK-less circuits — most of the library. Nothing stored was corrupted, because no body carried coordinates yet.
-
Detectors and observables for state-preparation and encoding circuits (#108), shown with a Detectors toggle next to Coords and hidden by default. Switching it on swaps in a new
stim-annotatedbody — reset prologue, terminal readout, the deterministic stabilizer outcomes asDETECTORs, and the logical as anOBSERVABLE_INCLUDE— and repoints the Crumble link at the matching URL. The canonicalstimbody stays unitary and untouched, becauseto_tableau()and the derive/fit pipeline depend on it. 418 of 834 circuits are annotated (357 state prep, 61 encoders); the 62 five-qubit-code circuits are excluded, because non-CSS stabilizers mix X and Z on the same qubit and no single terminal readout basis reads them — but they still get the reset prologue, since being non-CSS blocks the readout, not the initialisation. -
State-prep and encoding circuits now state their
|0…0⟩input explicitly with a reset prologue, in both toggle states, instead of leaving it implied. Encoders reset only their non-input qubits, since those are the ones actually fixed in|0⟩. Circuits that act on an already-encoded state (syndrome extraction, gadgets) get no prologue — a reset there would be wrong, not merely unhelpful. -
Encoders are annotated differently from state preps: only their ancillas are reset, leaving the k logical inputs free, and they get detectors but no observable. Hz is deterministic for any input, so the detectors hold whatever the reader supplies; the logical is input-dependent. The input qubits are unrecorded and canonicalization moves them, so they are derived from the circuit.
-
Coordinates are now displayed with a Coords toggle on circuit bodies, hidden by default: a d=11 unrotated prep opens with 221
QUBIT_COORDSlines before its first gate. Copy and download follow the toggle, so what you take away is what you see. The toggle only appears on bodies that have coordinates. Crumble links always keep them — that is where the layout is the point. -
The 20 unrotated surface code circuits now carry their lattice layout, and their Crumble links render it. Only coordinates for qubits the circuit actually uses are kept: a
QUBIT_COORDStarget counts towards stim'snum_qubits, so importing every declared grid site would inflatequbit_count. -
Surface code unitary Pauli-state encodings from arXiv:2601.05113 — 40 circuits across 10 codes (
data-imports/surface-code-encodings/). Each code gets four circuits:Ancilla-mediated/Direct×zero/plus, the two variants differing in whether intra-plaquette gates are routed through bridge ancillas (nearest-neighbour) or run directly between data qubits (next-nearest-neighbour). -
Six new codes:
unrotated-surface-code-d-{3,5,7,9,11}— the unrotated (planar) surface code family,[[13,1,3]]through[[221,1,11]], new to the library — androtated-surface-code-d-11([[121,1,11]]), completing the rotated family from d=3 to d=11. -
New
partial-ftcircuit tag, for schemes that preserve the fault distance for only one error type. The surface code encodings preserve it for the error that flips the codeword (X for|0>_L, Z for|+>_L) but not the complementary one, whichft/non-ftcannot express. Which type is protected follows from thelogical-state:*tag. -
import_state_prep(ancilla_role=...)distinguishes flag qubits from routing ancillas. Previously theflagtag was inferred fromnum_qubits > nalone, which mis-tags circuits whose ancillas only bridge gates under a restricted connectivity and carry no fault-tolerance role. Defaults to"flag", so existing importers are unaffected; pass"routing"to opt out. -
Circuit notes render a long contiguous ancilla run as
221-440 (220 qubits)instead of spelling out every index. Ancilla indices are always contiguous (range(n, num_qubits)), so the list was pure noise once a code got large. Runs of 8 or fewer, and any non-contiguous set, are unchanged. -
Edge caching:
s-maxageraised from 10 minutes to 7 days (pages and API), paired with an automatic Cloudflare purge-everything on each deploy (src/lib/cache-purge.ts, activated by theCLOUDFLARE_ZONE_ID/CLOUDFLARE_API_TOKENenv vars; logged no-op without them). -
Social-share card: static
og:image/twitter:image(1200×630, generated byscripts/icons/generate.mjs) on all pages; Twitter card upgraded tosummary_large_image. -
Non-CSS stabilizer code support throughout the pipeline (symplectic representation). Submit codes via
add_circuit(H=..., n=...)for non-CSS, or the existingHx/Hzpath for CSS. -
New
handlogicalcolumns oncodes;original_handoriginal_logicaloncircuit_originals(migration011_symplectic_storage.sql). -
New
npm run backfill:symplecticscript for upgrading existing CSS YAMLs. -
Non-CSS code page renders stabilizers and logicals via a Pauli ↔ symplectic toggle; CSS pages keep the existing
Hx/Hz/Logical X/Logical Zview.
- Circuit results in the search dropdown now link to the circuit's own page
(
/circuits/<id>) instead of the anchored code list. - Filtering and sorting on the codes index and code pages now run client-side
over the rendered rows: filter changes apply instantly (no page reload) and
every visit shares one canonical cached document. The URL contract is
unchanged (old filter links still work, applied on load);
/api/downloadstill parses the same params server-side. - Code pages no longer embed circuit bodies in the initial HTML; bodies load
on first row expand via the new
/api/circuits/[qec_id]/bodiesendpoint. Largest page (/codes/flag-gadgets) drops from ~15 MB to ~3 MB HTML and from ~39k to ~20k DOM nodes. - BREAKING:
add_circuit()matrix arguments (Hx,Hz,H,n) are now keyword-only. Migration: replaceadd_circuit(Hx, Hz, circuit, name, d, ...)withadd_circuit(circuit=circuit, circuit_name=name, d=d, Hx=Hx, Hz=Hz, ...). - The non-CSS branch of
_compute_logicals(now_compute_logicals_css) was mathematically wrong and is replaced by the new_compute_symplectic_logicals. This affects nobody in practice (the library contained no non-CSS codes), but any downstream code calling the old_compute_logicals(_, _, code_is_css=False, _)must switch to_compute_symplectic_logicals(H, n, k).