Skip to content

D10 — Does a corpus replay re-cut the standing releases?

Status: PENDING OWNER DECISION. Nothing below has been executed. The three standing releases are committed as they were built, and stay that way until somebody with standing signs this off. §7 is the recommendation, §8 is the case against it and what would falsify it, §9 is what has to change either way so the next replay cannot leave the same gap silently.

The question. The Formex mapper’s TRANSFORM_VERSION moved to formex-akn/1.1.0 and the corpus was replayed. The three standing releases were cut before that and their bundles record formex-akn/1.0.0. Does a mapper bump oblige a re-cut of every release built under the old one — or is a release permanently a statement about its own commit, stamps included?

Who is affected. Anyone who reads a release’s provenance to decide whether to trust its text; whoever cuts the next release after the next mapper bump; and fixreg@2025.04, the one release tag that exists and cannot be moved.


1. Where the version actually is, and where it is not

Section titled “1. Where the version actually is, and where it is not”

release-meta.yaml does not carry a transform version. None of the three manifests names one, and neither does the schema. The stamp lives one level down, in corpus.sqlite, on every unit’s meta blob:

Terminal window
sqlite3 regimes/fixreg/releases/<tag>/corpus.sqlite \
"SELECT DISTINCT json_extract(meta,'\$.transform_version') FROM units;"
releasedistinct values
2025.02formex-akn/1.0.0
2025.04formex-akn/1.0.0, consolidate/1.0.0
2025.06formex-akn/1.0.0, consolidate/1.0.0

regimes/fixreg/canon/ on main carries transform="formex-akn/1.1.0".

This matters for the framing. The claim is not in a manifest a packager reads and not in a signed metadata field: it is a per-unit provenance annotation inside the bundle, whose own constant says what it is for — “replay compares stored bytes against re-emitted ones and this is how a corpus knows which transform produced what” (tooling/openregs/akn/formex.py). It is a record of what produced these bytes, not an assertion about which mapper is current.

The premise offered is: a consumer of fixreg@2025.06 is told the text came from mapper 1.0.0, and it did not.

Checked against the artifact, that is not what happened. 2025.06 records commit 8bb2939928388ebb09744cfe62efa7c1c2c21357. At that commit the canon carried formex-akn/1.0.0, and those units were produced by mapper 1.0.0. The bundle describes its own commit accurately. What changed is main, and the release is not a function of main — it is a function of the commit it records, which is the whole of the determinism rule.

The engine has already decided this exact question on another axis and written down the reason. tooling/openregs/release/build.py:

The spec version a release is built under is deliberately NOT a constant here. It is read out of spec/VERSION at the commit being built … so that bumping spec/ never restamps — and therefore never invalidates the bytes of — a release that was already published under an earlier one.

docs/schema-compatibility.md states the same rule as design: “a spec bump never restamps — and so never invalidates the digests and signatures of — a published release.” Nothing distinguishes a mapper version from a spec version here. Both are engine versions stamped into a release; both move; both describe the commit the release names.

There is a real residue, and it is smaller and different: a consumer cannot tell from the bundle whether the difference between 1.0.0 and 1.1.0 matters for this text. That is §9’s problem, and it does not need a byte of any release to move.

3. What a re-cut would actually be — measured, not assumed

Section titled “3. What a re-cut would actually be — measured, not assumed”

The measurement offered was taken on 2025.06 alone. Repeated on all three, it does not generalise. Each row below is openregs release build run twice for that tag — once at the commit the release records, once at main — and the two compared.

2025.06 — a re-stamp. The rebuild from 8bb2939… reproduces all five committed digests exactly (corpus.sqlite ac727cc3…, graph.idx 7d375c33…, embeddings.parquet fdf51b2c…, constants-src 4c6214ed…, the diff manifest 0232d864…). Against a build at main: graph.idx and embeddings.parquet are byte-identical; constants-src and the diff manifest differ only in the recorded commit sha they print; corpus.sqlite differs in units.meta.transform_version and nowhere else — the expressions, atoms, edges and modifications tables dump byte-identically and every unit’s text is unchanged.

2025.04 — not a re-stamp. graph.idx and embeddings.parquet are again byte-identical and every unit’s text is unchanged, but the atoms table is not: at main, FIXREG-Art5.2-Ob1 carries a language_divergence record the committed release does not. That comes from 32c61cf — “record the French modality divergence on the Article 5(2) atom” — a corpus commit made after 2025.04’s recorded commit and before 2025.06’s. Rebuilding 2025.04 at main does not restamp it; it back-dates a later corpus decision into an earlier release.

2025.02 — a different release entirely. Committed: 32 units, 1 atom, 3 expressions, 31 edges, 0 modifications. Built at main for the same tag: 55 units, 4 atoms, 4 expressions, 101 edges, 3 modifications — and the threshold atom closes at 2025-02-28 with a 10000 successor beside it. 2025.02 exists to be the pre-amendment release, and tooling/tests/test_release_build.py names that as one of the two properties “the whole design is for”: asked as of 2025-06-01 it must answer EUR 5000 with an open window, “which is only possible because it was built from a commit at which the amended Expression did not exist.” A re-cut at main destroys that.

So “re-cut the standing releases” is three operations, not one. Only the third is the thing the phrase suggests.

A faithful re-cut of the other two is possible and is a fabrication. Each release records a purpose-made snapshot commit — “the corpus as it stood for fixreg@<tag>” — and release_target.py requires the recorded commit to be an ancestor of the tag, so a faithful re-cut means creating a fourth and fifth such commit: the pre-consolidation canon, re-ingested under the new mapper, committed onto main for no reason except to move a version string. Their built_at would be today’s date, because built_at is the commit’s own committer date. A release dated 2025-02 would then be stamped as built in 2026, from a commit whose only content is a re-stamp.

Nothing. Checked against the remote rather than assumed:

  • openregs/openregs has zero GitHub releases. Nothing is downloadable.
  • Exactly one tag exists: fixreg@2025.04, an annotated tag over cfcc13c. Its pipeline ran build, sign and eval green; publish failed for an empty token and attach was skipped behind it, which is the graph that tag’s workflow file froze. Its two workflow artifacts have not expired.
  • No package reached PyPI or npm; the organisation has no secrets configured.

So the cost framed as “re-cutting rewrites published artifacts” is, today, almost entirely notional. The bytes have been distributed to nobody. If a re-cut is going to happen at all, this is the cheapest moment it will ever have — which is an argument for, and §7 still declines it, for §3’s and §5’s reasons rather than for this one.

One further point on harm, stated so it is not left implicit: fixreg is a fixture. Its releases sign with the published fixture key, which docs/runbooks/release-tag.md describes as vouching “for nothing, and says so”. There is no compliance consumer of fixreg@2025.06 to mislead. The decision matters because it is the precedent a real corpus’s first replay will follow.

Determinism is whole right now. All three releases rebuild byte-identically from the shas their own release-meta.yaml records — verified for 2025.06 by running the build, and asserted in CI for the other two. Re-cutting does not break that rule, because the re-cut releases would record their new commits. But note what it costs: the guarantee’s value is that a release is pinned to a commit somebody made for a reason. Three commits made to move a string weaken the claim without violating it.

Tag immutability is enforced and has no bypass. Ruleset 20202372, “release tags are immutable”, is active over refs/tags/*@* with deletion and non_fast_forward and an empty bypass-actor list; the API reports current_user_can_bypass: never. Removing a tag means disabling the ruleset, and release-tag.md records that while it is disabled every release tag in the repository is deletable and movable.

The sharp consequence is not that the tag would have to move. It is that it cannot, and does not need to, and that is worse:

GitHub runs the workflow file from the ref that triggered it.

fixreg@2025.04’s pipeline reads tooling/ci/release_target.py at cfcc13c, which reads regimes/fixreg/releases/2025.04/release-meta.yaml at cfcc13c, which records 0f466185…. That tag will build the 1.0.0 bytes on every run it will ever have. Re-cut 2025.04 on main and the repository permanently holds, under that name, bytes its own tag cannot produce — and the pending manual attach described in release-tag.md becomes unanswerable, because whichever bytes go up, one of the two claims is false. That is a permanent, ruleset-enforced inconsistency bought for a version string.

Stated plainly, because it is the price of §7:

  1. Every standing release will fall further behind the engine, permanently. The stamp gap does not heal; the next mapper bump widens it.
  2. A reader who compares a release to main will see a contradiction and has to be told the rule before the artifact stops looking wrong. That is a documentation burden that never ends.
  3. The bundles say nothing about whether the difference matters. Today the answer is “it does not, for this corpus”, and the only place that is written down is a commit message.

Do not re-cut. State the non-restamping rule for the transform axis the way it is already stated for the spec axis, and make the next replay prove — rather than assume — that the bump did not move any standing release’s bytes.

The reasons, in the order they do work:

  1. The claim in the bundles is true. Each records the mapper that produced its text at the commit it names. Re-cutting to make a true statement look current is not a provenance fix.
  2. The rule already exists on the neighbouring axis, with its reason written down. A mapper bump and a spec bump are the same kind of event. Deciding them differently would need an argument nobody has made.
  3. The operation is not what it is called. For two of three releases a re-cut at main changes content, and a faithful re-cut means manufacturing commits.
  4. fixreg@2025.04 cannot follow. Its pipeline is frozen at bytes the re-cut would abandon, and the ruleset means that is forever.

Consequences accepted knowingly: §6, all three.

8. The strongest argument against, and what would falsify this

Section titled “8. The strongest argument against, and what would falsify this”

The strongest argument against is §4’s: nothing has shipped, so this is the only moment a re-cut is nearly free, and declining it now means declining it forever under worse conditions. Add that a compliance product’s whole proposition is that provenance is exact, and “exact relative to the commit it names” is a distinction a buyer will not make.

Two things would falsify the recommendation, and only two.

A mapper bump that moves the text. formex-akn/1.1.0 was measured: no fixture act carries a footnote, so 1.1.0 emits the bytes 1.0.0 emitted, and §3 confirms it per release. If a future bump does change what a standing release’s canon produces, the release and the current mapper genuinely disagree about the law’s text, and the stamp stops being a bookkeeping difference. Even then the answer is not a re-cut of the old tag — it is a new dated release, built the ordinary way, with the diff manifest saying what moved. release-tag.md already reaches that answer for the analogous case: “there is no second tag for the same version.”

Evidence that a consumer reads the stamp as prescriptive. The recommendation rests on transform_version meaning “what produced this”. If an SDK, a downstream tool or a customer reads it as “the mapper to use”, it is a currency claim, currency claims go stale, and staleness is a defect rather than a fact. This is not established offline: no consumer of the field exists outside this repository today, and no SDK has shipped.

9. How the next replay is stopped from leaving this gap silently

Section titled “9. How the next replay is stopped from leaving this gap silently”

make bump does not exist. There is no such Makefile target. openregs bump is a different thing: it moves a consuming repository’s pinned release forward and opens the pull request a human reads. It has no view of mapper versions and is the wrong lever. Two things are the right ones.

1. Sweep the rebuild test over every committed release, rather than naming two. test_rebuilding_from_the_same_commit_reproduces_the_bytes is parametrized over [PRE_TAG, POST_TAG], so 2025.06 is unchecked — the release the replay was measured against is the one nothing asserts. Discover the releases the way tooling/tests/test_combinations.py discovers combinations: over the directory, failing closed on an empty sweep, so a release is checked by having been added. That turns “did the replay leave a release unable to rebuild from its own commit?” from a thing somebody remembers to ask into a thing CI answers. It is the cheap half and it is owed whichever way this decision goes.

2. Make the drift declared rather than discovered. The sweep proves each release still rebuilds; it cannot say whether the current mapper would produce the same text. Require that when a TRANSFORM_VERSION moves, the same commit records which committed releases carry the superseded version and whether the new one changes their bytes — and assert that every distinct transform version present in any committed bundle has a row. Two precedents for the shape: BLESS-SUMMARY.md, which is what a reviewer reads to see what a deliberate golden change moved; and openregs.conformance.NO_GOLDEN, whose declarations are “re-proved on each run, so it cannot quietly become a skip list.”

Re-proving a row costs one replay against that release’s own canon commit, which is what was already run by hand for formex-akn/1.1.0. Gate it on the mapper version having changed rather than running it every time; a row whose version is unchanged is proved by the sweep alone.

Where this is written down matters as much as the check. docs/runbooks/goldens.md § “What blessing does not do” is where a mapper change’s reach is described, and it currently names one boundary — blessing does not re-ingest, only replay reaches the canon. It should name the second: replay does not reach a release. A release is a function of a commit, and a commit already made does not change.

Not a date. Revisit D10 when a mapper bump is measured to change the text a standing release’s canon produces, or when a consumer outside this repository is found reading transform_version to decide which mapper to run. The first makes §8’s first falsifier real; the second makes its second real. The steady accumulation of version gaps is not a trigger — it is the rule working.

Rendered from openregs/openregs@f3a2d10:docs/decisions/d10-replay-and-standing-releases.md