Skip to content

Documentation versions

This site can serve more than one version of the documentation at once. latest, at the root, renders the engine’s main at a pinned commit and moves as core moves. Every other version is frozen: it renders one engine commit, for ever, at /v/<version>/.

Today there are none. Everything under /v/ is a 404, and the version switcher does not appear, because it hides itself when there is only one version to offer. Two versions have been cut and both were deleted — see Un-cutting one for what they were and why. This page describes the machinery that is in the repository and tested, ready for the next cut.

When there is more than one, which one you are reading is never left to inference. Every page of a frozen version carries a banner naming the version and linking to the current documentation, and the version switcher sits above the navigation on every page that has one. That is not decoration: this documentation describes obligations, and somebody reading a superseded one because the page did not say it was superseded is the failure the banner exists to prevent.

The engine documentation — everything rendered from the core repository: /overview/, /architecture/, the runbooks, the CLI reference. Those pages are a function of the commit the version was cut at, and that commit never changes.

Not the pages this repository authors: the guides, the landing page, the navigation, the theme. They are built from the current source into every version, so improving a guide improves it everywhere at once — and a guide edited today changes inside a version cut last year. That is the documented default rather than an oversight, and each frozen banner says which half it is talking about.

A commit is immutable, so it is tempting to treat the pin in versions.json as the whole of the freeze. It is not. What a frozen page says is also a function of the routing rules, the markdown transform and the reference renderers — all shared with latest, all changing regularly, all reaching back into every version that has already been cut.

So a cut records the bytes as well as the pin. versions.lock.json holds the sha256 of every core-derived page of every frozen version, and of every core-derived file the version publishes without rendering — the OpenAPI document is served verbatim beside the API reference, and something reads it automatically, so it is as much a promise as a page. Every build re-derives the lot and fails on any difference: a page whose bytes changed, a page that appeared, a page that went away, a document that now serves other bytes. A second check asserts over the built site that every version the switcher offers is a version the deploy actually serves, and that every frozen page is banner-marked — a menu entry leading to a 404, or a superseded page that looks current, are the two failures a passing build would otherwise hide.

Terminal window
pnpm cut-version --id 2026.09
pnpm check

The first command writes three files and builds nothing:

FileWhat it gains
versions.jsonthe pin, the label, and the banner every page of the version will carry
vercel.jsonthe version’s own 404 route, so a miss inside it does not answer with a search over latest
versions.lock.jsonthe sha256 of every core page, and every core document, the version froze

Recording the lock needs the pinned commit’s content, so the command runs the sync at the new pin. Offline, point it at a checkout you already have:

Terminal window
OPENREGS_CORE_SOURCE=/path/to/openregs pnpm cut-version --id 2026.09

If it cannot reach the pin it restores what it wrote and exits non-zero, because a version that is declared but unrecorded fails every later build with a true message about the wrong problem.

pnpm check then builds latest and every frozen version and runs the whole assertion suite over the result. Commit the three files; the cut is reviewable as a diff, which is the point of it not being a copy of a directory.

Two things the command does not do, because no build can:

  • Read the version before freezing it. See Freezing a statement of present fact below. This is the step both deleted versions needed and neither got.
  • Update the paragraph at the top of this page, which says how many versions are served. It is prose about the world, in a file that is not generated, and it is the first thing a reader here checks against what the switcher shows them.

--commit <sha> freezes something other than the current pin, and --label sets the name shown in the switcher and the banner. The default commit is whatever latest is serving at the moment you cut, which is almost always what you want.

The id is the URL segment, so it is permanent in the same way a route is. Today’s ids are dates, and that is a statement about the engine rather than a house style: the engine has published no release, so there is no version string for a snapshot to borrow. Core’s one tag, fixreg@2025.04, releases the fixture corpus — a test regime that stays in the engine repository permanently — and names a corpus as-of date, not a version of the software anybody runs. When the engine does release, its version string becomes the id and the dated snapshots stay as history.

Terminal window
pnpm cut-version --undo 2026.09

That removes the version from all three files, and the next build stops producing /v/2026.09/.

Whether you may is a different question from whether you can. A frozen version is never restructured — that is what freezing means — so the only way to stop serving one of its pages is to stop serving the version, and every URL under it becomes a 404 rather than a redirect. There is no 301 to latest on offer and there is not meant to be: redirects.json refuses any entry whose from begins /v/, because a frozen route is not an address that moved, and sending somebody from a page cut at one engine commit to a page describing another is a different answer to their question dressed up as the same one.

So un-cutting is uncontroversial only while the version’s URLs were never promised: a cut nobody has linked to yet, or one declared temporary at birth. It has a second, harder use, and this site has now made both.

preview-0 was the easy case. A synthetic snapshot cut deliberately at an old commit to exercise the machinery, it could not be repaired in place — its commit predated core’s scope correction, so it served a claim about the project’s subject that had been corrected everywhere else. Removing it cost nothing anybody had.

2026.08 was the hard one. A real dated snapshot at core 7370148, publicly linked and indexable, whose /contributing/security/ page said the security reporting address “is planned and does not route mail yet” — about an address that routes, and had been proven to. That is false in the direction that suppresses vulnerability reports. It was demoted first, by pointing every frozen page’s canonical at its counterpart under latest, and demotion is not removal: a canonical moves a search ranking and changes nothing for a reader holding the URL. The version was deleted on 2026-08-06, with the cost stated rather than discovered afterwards — anyone pinned to that engine commit lost the documentation matching it, and every /v/2026.08/… link now 404s into a search over latest.

Both ids are spent. Cutting a new version at preview-0 or 2026.08 would serve different content at an address those URLs already had, which is the one thing the version scheme promises not to do.

For a version that has been announced and is merely old, the answer is still to cut forward: a new version, and the old one left where it is with its banner doing its job. Deletion is for a version that is wrong in a way nothing can reach.

Two versions have been cut here and two have been deleted, for the same underlying reason both times. That is a fact about this procedure, not about either version, and the procedure does not currently account for it.

The freeze is a promise about bytes. It is silent about tense. A page that says “the atomizer emits one obligation per sentence” describes the engine at the commit it was cut from, and stays true of that engine for ever. A page that says “that address does not route yet”, “this is not implemented”, “no release has been published”, “the hosting provider has not been chosen” describes the world on the day it was written — and the world moves. The moment such a sentence is frozen it becomes a public, permanent, uncorrectable claim, still carrying the site’s authority and still answering a reader who arrived from a search engine.

Three properties make it worse than an ordinary stale page:

  • It cannot be fixed. Not by a pin bump, not by an edit in core, not by anything short of deleting the version. versions.lock.json exists precisely to stop the bytes moving, and it works.
  • It has no symptom. Every check here stays green: the page renders, the links resolve, the banner is present, the digest matches. Being wrong about the world is not a property any build can compute.
  • The banner does not cover it. It says the engine documentation is fixed at a commit. A reader takes that as “this may describe an older release” — not as “this may state, as current fact, something that has since stopped being true.”

The direction of the error is what decides how much this matters. A frozen page understating what the engine can do wastes somebody’s time. A frozen page understating where to report a vulnerability suppresses the report. Those are not the same defect and a procedure that treats them alike will keep producing the second.

Undecided, deliberately. What follows is the shape of the choice, so that whoever next cuts a version makes it on purpose rather than by not thinking about it. The options are not exclusive and none of them has been adopted:

  1. Read before you freeze. Add a step to the cut: sweep the version’s pages for present-tense claims about the world — addresses, availability, “not yet”, “planned”, “no … has been” — and either fix them in core and move the pin first, or write down, in the version’s entry, which ones you are knowingly freezing. This is the cheapest option and the only one that acts before the URL is public.
  2. Give the version an expiry. Record a date at which somebody re-reads it and decides to keep or delete it. This admits the version has a lifetime instead of discovering that it did.
  3. Reduce its reach rather than its content. noindex on frozen versions, or a robots disallow of /v/. Cheap, reversible, and only partial: it moves the page out of search and leaves it serving to anyone with the link. public/robots.txt argues against the disallow specifically, and that argument should be answered rather than stepped around.
  4. Do not freeze the pages that make such claims. Serve latest’s copy of the security policy, the contributing guide and anything else about contacting or participating in the project, at every version’s routes. It is the only option that removes the hazard at the source, and it costs the most: “frozen” becomes partial, the lock has to know which half it covers, and somebody has to draw a line that the next document will sit on. core-sync.config.json’s exclude is not that mechanism — it is documented as internal-only core docs that must not be published at all.
  5. Accept it, in writing. State on the version’s own entry that its non-engine prose is a snapshot of a moving world, and that removal is the only remedy if one of those sentences becomes dangerous. This is what the site has been doing without saying so, which is the part worth changing either way.

Whichever is chosen, one thing already holds and is worth not losing: deleting a version is a decision with a stated cost, made by whoever owns the project — never a cleanup, and never something a build does on its own.

FAIL — the frozen version 2026.09 has moved:
/overview/
renders different bytes than it froze

That is the check working. Something changed what an already-cut version renders — usually a change to the shared machinery, occasionally an edited pin. Two ways out, and they are not equivalent:

  • Leave the frozen version alone. Make the change apply to latest only. This is the default, and it is right whenever the frozen version was correct as cut.
  • Re-record it, with node scripts/check-frozen.mjs --id <id> --record, when the new rendering is genuinely the right one for every version — a fix to link rewriting, say, that every version should have. The diff is the review: it lists exactly which frozen pages changed, and somebody has to look at that before it merges.

What is not on the list is deleting the record. A frozen version with nothing recorded fails the build too, and for the same reason: it is free to drift and nothing would notice.