Repository navigation
Currency check cannot detect a generator that has stopped emitting a file #35
Description
Activity
- addedauditFinding from an audit or mutation-test scanFinding from an audit or mutation-test scanadversarialFound by the adversarial half of a mutation-test scanFound by the adversarial half of a mutation-test scan
on Sep 20, 2026 - added a commit that references this issue
on Sep 20, 2026 thedavidmeister commented
on Sep 21, 2026 ContributorAuthorMore actionsCorrection 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 to
answer "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:
- a committed generated file that was not rewritten because the generator is dead
- a committed file under
src/generated/<tag>/that was not rewritten because it
is 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 precisely
which paths it owns, andLibRainDeploySnapshotalready computes them. The
frozen record is excluded there by construction, becausecutRelease()is the
only 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:
- no
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 - no repo goes red on merge — the rollout stops being a coordinated migration
- one check rather than two, with nothing to keep in step
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.
What
The
Git is cleanjob (rainix-copy-artifacts) is the only thing checking thatthis 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 hasstopped 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:Mutant — remove the one call that emits it, in
script/Build.sol:then the same marker and the same generator run:
The script still exits 0. In a real PR the committed
src/lib/LibReleasedSuites.solis already correct, so nothing is rewritten,nothing differs,
git diff --exit-codepasses and the job is green — with theaggregate generator dead.
Why it matters here
LibReleasedSuites.releasedSuites()is the whole released side of thedeclaration: it is what
CloneFactoryDeploySuites.releasedSuites()returns, andtherefore 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 statethe file's own NatSpec says must be impossible:
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-artifactswith 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.solassert itemitted 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).