Skip to content

Add a mutation helper that refuses when the edit did not land - #371

Merged
haksungjang merged 2 commits into
mainfrom
tools-mutate
Sep 4, 2026
Merged

Add a mutation helper that refuses when the edit did not land#371
haksungjang merged 2 commits into
mainfrom
tools-mutate

Conversation

@haksungjang

Copy link
Copy Markdown
Contributor

Hardening rules 7 and 8 ask contributors to break what an assertion guards and to confirm that a surviving mutation actually applied. The ordinary way to do that is a shell loop that edits a file, runs the tests, and restores it, and that loop has a failure mode with no symptom. When the edit does not match, most spellings carry on, the tests run against unmodified code, and the green result reads as "the assertion does not catch this" when the assertion was never handed anything to catch.

It happened five times across three sessions in one day. Every time somebody noticed, which is the part that does not scale.

The tool edits one file and refuses three things, exiting non-zero so a && chain stops before the tests run: an anchor that is missing, an anchor that appears more than once, and a replacement that leaves the file byte-identical. The third is the one worth having on its own. An anchor can match while the edit changes nothing, and that is the same false green by a different route; it is also the case a count == 1 assertion in the calling script does not cover.

--restore reads a backup the tool wrote. Two sessions lost uncommitted edits to git checkout -- <file> on the same day this was written, so having a restore path that is not that command removes one reason to reach for it.

The guide points at it as one safe way and not as the way. Rule 7 says to break what you guard, not how, and a mutation that needs a different shape should use a different shape. A required tool that did not fit would be worked around, and the work-around is the thing with no guard.

selftest.py drives each refusal and each success, because a tool that refused everything would pass a file of refusal tests and be useless. It runs in the existing lint job for the reason ai-review gives there: standard library only, about a second, and a new job is a new required check that docs-only pull requests stall on.

Verified by mutating the tool with itself: deleting the unchanged-file check makes the selftest fail on exactly that case and nothing else.

Rules 7 and 8 ask contributors to break what an assertion guards and to check
that a surviving mutation applied. The usual shell loop does that with a
silent failure mode: when the edit does not match, it carries on, the tests
run against unmodified code, and the green result reads as "the assertion does
not catch this".

That happened five times across three sessions in one day, and a person caught
it every time. This is the check written down.

It refuses a missing anchor, an ambiguous one, and a replacement that leaves
the file byte-identical, exiting non-zero so a chained test command does not
run. Restoring reads its own backup rather than git, which would take
uncommitted work in that file with it.

The guide points at it as one safe way rather than the way: a mutation that
needs a different shape should use one, and a tool that had to be bent around
would be worked around instead.
CI runs it and tools/static-checks/run.py did not know about it, which its
own contract test refuses: a local runner that reports success without having
run a step is worse than one that does not claim to cover it.

Found by that test rather than by review, which is the test working.
@haksungjang
haksungjang merged commit 49af568 into main Sep 4, 2026
30 checks passed
@haksungjang
haksungjang deleted the tools-mutate branch September 4, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant