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.
What “frozen” freezes
Section titled “What “frozen” freezes”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.
How the freeze is enforced
Section titled “How the freeze is enforced”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.
Cutting a version
Section titled “Cutting a version”pnpm cut-version --id 2026.09pnpm checkThe first command writes three files and builds nothing:
| File | What it gains |
|---|---|
versions.json | the pin, the label, and the banner every page of the version will carry |
vercel.json | the version’s own 404 route, so a miss inside it does not answer with a search over latest |
versions.lock.json | the 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:
OPENREGS_CORE_SOURCE=/path/to/openregs pnpm cut-version --id 2026.09If 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.
Choosing the id
Section titled “Choosing the id”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.
Un-cutting one
Section titled “Un-cutting one”pnpm cut-version --undo 2026.09That 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.
Freezing a statement of present fact
Section titled “Freezing a statement of present fact”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.jsonexists 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.
What a future cut should do about it
Section titled “What a future cut should do about it”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:
- 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.
- 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.
- Reduce its reach rather than its content.
noindexon frozen versions, or arobotsdisallow 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.txtargues against the disallow specifically, and that argument should be answered rather than stepped around. - 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’sexcludeis not that mechanism — it is documented as internal-only core docs that must not be published at all. - 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.
When a build fails on a frozen version
Section titled “When a build fails on a frozen version”FAIL — the frozen version 2026.09 has moved: /overview/ renders different bytes than it frozeThat 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
latestonly. 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.
Where this is written down
Section titled “Where this is written down”versions.json— the pins, one entry per frozen versionversions.lock.json— what each of them froze- URL scheme — what a
/v/<version>/path promises to anyone writing one into a link