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:
sqlite3 regimes/fixreg/releases/<tag>/corpus.sqlite \ "SELECT DISTINCT json_extract(meta,'\$.transform_version') FROM units;"| release | distinct values |
|---|---|
2025.02 | formex-akn/1.0.0 |
2025.04 | formex-akn/1.0.0, consolidate/1.0.0 |
2025.06 | formex-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.
2. The premise, checked
Section titled “2. The premise, checked”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/VERSIONat the commit being built … so that bumpingspec/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.
4. What is published today
Section titled “4. What is published today”Nothing. Checked against the remote rather than assumed:
openregs/openregshas zero GitHub releases. Nothing is downloadable.- Exactly one tag exists:
fixreg@2025.04, an annotated tag overcfcc13c. Its pipeline ranbuild,signandevalgreen;publishfailed for an empty token andattachwas 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.
5. Determinism, and tag immutability
Section titled “5. Determinism, and tag immutability”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.
6. What not re-cutting costs
Section titled “6. What not re-cutting costs”Stated plainly, because it is the price of §7:
- Every standing release will fall further behind the engine, permanently. The stamp gap does not heal; the next mapper bump widens it.
- A reader who compares a release to
mainwill see a contradiction and has to be told the rule before the artifact stops looking wrong. That is a documentation burden that never ends. - 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.
7. Recommendation
Section titled “7. Recommendation”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:
- 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.
- 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.
- The operation is not what it is called. For two of three releases a re-cut
at
mainchanges content, and a faithful re-cut means manufacturing commits. fixreg@2025.04cannot 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.
10. Trigger to revisit
Section titled “10. Trigger to revisit”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