Repository navigation
Description
What
The Git is clean job (rainix-copy-artifacts) is the only thing checking that
this repo's committed generated sources are current. Its method is "re-run the
generator, then git diff --exit-code", which cannot detect a generator that has
stopped generating a file. The check reports green, and the generated file goes
on being whatever was last committed.
Verified repro
At 5c165a880ac105d6c038cee096500f76849a2190, with a control.
Control — regeneration normally rewrites the aggregate. Append a marker to
src/lib/LibReleasedSuites.sol, then run the generator:
$ printf '\n// CORRUPTION MARKER\n' >> src/lib/LibReleasedSuites.sol
$ forge script ./script/Build.sol
Script ran successfully.
$ grep -c 'CORRUPTION MARKER' src/lib/LibReleasedSuites.sol
0 # rewritten, marker gone
$ git status --porcelain
# clean
Mutant — remove the one call that emits it, in script/Build.sol:
- LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, LibRainDeploySnapshot.LIB_DIR, snapshotContractNames());
+ // aggregate emitter removedthen the same marker and the same generator run:
$ forge script ./script/Build.sol
Script ran successfully.
$ grep -c 'CORRUPTION MARKER' src/lib/LibReleasedSuites.sol
1 # NOT rewritten — the generator no longer emits this file
The script still exits 0. In a real PR the committed
src/lib/LibReleasedSuites.sol is already correct, so nothing is rewritten,
nothing differs, git diff --exit-code passes and the job is green — with the
aggregate generator dead.
Why it matters here
LibReleasedSuites.releasedSuites() is the whole released side of the
declaration: it is what CloneFactoryDeploySuites.releasedSuites() returns, and
therefore what the chain group and the frozen-record check are handed.
Its CONTENT does not change when a release is cut — it concatenates the
per-contract libs, and this repo has exactly one contract — so a dead emitter is
inert and invisible for as long as that stays true. It stops being inert the
moment a SECOND deployed contract is added: the aggregate would need a new entry,
would not get one, and the new contract's releases would be silently absent from
releasedSuites() with the currency check green throughout. That is the state
the file's own NatSpec says must be impossible:
a release missing from the declaration is a release every check quietly stops
asking about.
Scope
This is a property of the regenerate-and-diff pattern rather than of anything
written in this repo, so it applies to every repo using rainix-copy-artifacts
with committed generated sources. Filed here because this is where it was found
and where the consequence above is concrete.
Possible directions, for triage rather than as a recommendation: have the
generator write to a clean temporary tree and diff that against the committed one
(so a file never written shows as missing), or have script/Build.sol assert it
emitted every file its own declaration names, or delete the generated files
before regenerating in CI so absence is visible.
Found by adversarial mutation testing (skill 0.35.0).
Activity
audit: record the adversarial mutation-test scan of 5c165a8
thedavidmeister commentedon Sep 21, 2026
Correction on the fix direction
The three directions this issue originally listed all treat the gap as something
to detect from outside the generator. That framing is wrong, and the PR written
against it (rainlanguage/rainix#392) inherits the error: it leaves the currency
check defective and adds a SECOND check beside it, paid for by a hand-written
script/codegen-manifest.txtin every affected repo.Two problems with that shape.
The defective check stays defective. The job of
rainix-copy-artifactsis toanswer "are the committed generated files current". It answers with
regenerate-and-diff, which cannot see a file the generator no longer writes. A
separate witness compensating for that leaves the currency check wrong and adds a
thing that has to be kept in step with it.
A hand-written manifest is the same class of bug it is trying to catch. A
declaration maintained by hand in 15 repos goes stale. A stale manifest that
stops naming a file the generator still writes is silently inert — which is
exactly the dead-emitter failure, one level up.
What the check actually lacks
Not a manifest. It lacks the ability to tell these apart:
src/generated/<tag>/that was not rewritten because itis a FROZEN release snapshot and is never meant to be
That is why a blanket "delete generated files, regenerate, diff" false-reds every
deploy repo, and it is the one fact neither the diff nor the filesystem has.
Where that fact already lives
In the generator.
script/Build.solcallswriteSnapshot,writeAliasLib,writeReleasedSuitesLibandwriteReleasedSuitesAggregate— it knows preciselywhich paths it owns, and
LibRainDeploySnapshotalready computes them. Thefrozen record is excluded there by construction, because
cutRelease()is theonly thing that writes it.
So the generator can emit what it intended to write, and the check compares that
against what appeared. Intended-but-absent is the dead emitter. No repo declares
anything, nothing drifts, and the currency check becomes correct rather than
being worked around.
Consequences, relative to the manifest approach:
script/codegen-manifest.txtin any repo, so the 15 adoption issues(Adoption: 15 consumer repos need script/codegen-manifest.txt before #392 merges rainix#394 and children) fall away
Status
rainlanguage/rainix#392 implements the manifest approach and is open, not merged.
This supersedes the directions listed above; the PR should be reworked rather
than merged as it stands.
thedavidmeister commentedon Oct 21, 2026
The rainix half exists:
codegen-declarationruns inrainix-copy-artifacts. It is a no-op here until rain.deploy'sBuildScriptdeclares its outputs, which is tracked in rainlanguage/rain.deploy#329. This closes once this repo is on a rain-deploy release that has it.