What Muse's read of #634 left: the mutant lists read in order, a comparison's own operator, the head commit's trailer, the merge base (#589)
Part of #589. This is the follow-on to #634, the advisory check of a changed line of Python no test notices. #634 merged before Muse could read it: Muse was out of quota until 2026-11-09. Muse Wonder Full read it after the merge, at 60316bb8, and found four majors. All four are real, and each is fixed here.
Decision 0166 gains Boundaries rows only; its words are not changed. No protected file is touched, and no record is added.
Muse's four findings on #634, fixed
| Finding | The fix | Held by |
|---|---|---|
Any call in the mutant lists was data, so an entry that read the world, such as os.getenv('X', 'old'), escaped the check | An entry is data only if every name it reads was bound before the list to a text, a literal value or a helper the module defines; see below | test_a_call_in_the_mutant_lists_is_data_only_of_the_modules_own_helpers, 27 mutants |
| A comparison's fallback mutant took the first operator the line showed, even one inside a string, so a test that read the text could kill a mutant that changed nothing | The operator is found between the comparison's two sides. A comparison it cannot swap, or one on another line of the statement, is passed over | test_a_mutant_is_made_of_an_added_line_and_never_of_a_quiet_one, 5 mutants (<, >, >=, the guards) |
| Nothing held that the trailer is read from the head commit alone | A trailer in an earlier commit of the change is never read, and the commit asked for is the one read, not the checkout's HEAD | test_a_trailer_is_read_from_the_head_commit_alone, 2 mutants |
| Nothing held that the change is read from the merge base | Main moving on after the branch left it adds no hunk to the change | test_the_change_is_read_from_the_merge_base_and_not_from_main_as_it_moved_on, 1 mutant |
What is data in the mutant lists, now
The check skips a hunk only when it is data: an entry of ISSUE or MUTANTS that decides nothing. The fix rounds found many routes by which an entry that reads the world passed for data. The rule is now stated in terms of what is allowed, not what is forbidden:
- A name is data only if a statement before the list bound it to one of these:
- a text;
- a value that is itself data;
- a function the module defines, with no decorator.
- A name bound any other way is code:
- bound twice;
- bound by an import, wherever the import stands;
- bound by a walrus;
- named by a
global; - changed by a method call, a store into it or a
del; - left unbound.
- A call of
globals,locals,vars,exec,eval,setattrordelattrmakes every name code. - Calls:
- a call is data only of such a helper, or of
replaceon a text; - a helper's return counts as a text only if the helper says
-> str; - a dunder attribute is code.
- a call is data only of such a helper, or of
- The real lists read exactly as before:
data_linesoftools/mutants.pyis the same under main'suntested.pyand this one.
Residual, stated, not fixed: a contrived evasion is left out. One example is a nested scope whose own name masks the module's. The check is advisory, and the record already says the head's own tests can forge its counts. The hunk that reads the world sits outside the lists and is still checked.
A deliberate narrowing: a text method other than replace, such as .strip() or .format(...), is now code where main took it for data. No entry of the real lists changes.
Checks
make verifyon an export of the head: 1716 tests OK, at2b47efa.- Every mutant of
tools/untested.py, run bytools.mutantson an export of the head: 83 of 83 killed at2b47efa. 35 of them are new. - All mutant targets occur once in their files, and no description repeats.
Reads
Argon, Muse Wonder Full, the Claude review and the security pass read the first head, eb184e4. The Claude review read each fix round again: fe00bf1, c0065ce and 2424df1. Argon read 2424df1. The last two rounds, 5626728/dd25df3 and 2b47efa, close what those reads found, and add a case for a mutant that survived; no reader read them again.
| Reviewer | Head | Unread parts | Blocking | Major | Minor | Info |
|---|---|---|---|---|---|---|
| agy | eb184e4 | none | 0 | 2 | 0 | 0 |
| muse | eb184e4 | none | 0 | 2 | 3 | 0 |
| claude-review | eb184e4 | none | 0 | 0 | 3 | 0 |
| claude-security | eb184e4 | none | 0 | 0 | 1 | 0 |
| claude-review-r2 | fe00bf1 | none | 0 | 2 | 2 | 0 |
| claude-review-r3 | c0065ce | none | 0 | 1 | 3 | 0 |
| agy-r4 | 2424df1 | none | 0 | 8 | 0 | 0 |
| claude-review-r4 | 2424df1 | none | 0 | 0 | 1 | 4 |
| Finding | Severity | Criterion | Where | What | Disposition |
|---|---|---|---|---|---|
| agy#1 | major | PR-3 | tools/untested.py:200 | The receiver guard _literal(func.value, helpers) on .replace calls has no negative test and no mutant | Fixed in fae6b69: replace is data only on a text; ROOT.replace is code. |
| agy#2 | major | PR-3 | tools/untested.py:200 | _literal treats os.replace(...) and any module's replace function as literal data | Fixed in fae6b69: os is a name an import binds, so os.replace is code. |
| muse#1 | major | PR-3 | tools/untested.py:162 | Helper detection ignores binding order, so a shadowed name calling os.getenv is data | Fixed in fe00bf1: the module is read in order; a name bound twice is code, and a list sees only what was bound before it. |
| muse#2 | major | PR-3 | tools/untested.py:200 | Any object's replace, including os.replace, is treated as mutant-list data | Fixed in fae6b69, as agy#2. |
| muse#3 | minor | PR-3 | tools/untested.py:200 | No test holds the replace receiver check against a deciding receiver | Fixed in fae6b69 and fe00bf1: open('x').read().replace and os.replace are held as code. |
| muse#4 | minor | PR-3 | tools/untested.py:484 | No test holds the > and >= entries of the comparison SWAP table | Fixed in fe00bf1: > and >= are swapped in the test, with a mutant each. |
| muse#5 | minor | PR-3 | tools/untested.py:521 | Trailer test never calls trailers() for a commit other than the current HEAD | Fixed in fe00bf1: the earlier commit is asked for after the fix round too, with a mutant. |
| claude-review#1 | minor | PR-3 | tools/untested.py:191 | An entry of the mutant lists that reads the world through a subscript or attribute is still data | Fixed in fae6b69: a comparison it cannot swap is passed over, with a case and a mutant. |
| claude-review#2 | minor | PR-3 | tools/untested.py:498 | No test holds the one-line guard on the Compare: dropping inner.lineno == inner.end_lineno == node.lineno survives | Fixed in fae6b69: a comparison on another line is passed over, with a case and a mutant. |
| claude-review#3 | minor | PR-3 | tools/untested.py:499 | No test holds the SWAP guard on the comparison's operator: a mutant that removes it survives, and the change has no mutant for it | Fixed in fae6b69 and c0065ce: what is read from a name an import binds is code. |
| claude-security#1 | minor | PR-3 | tools/untested.py:200 | _literal still treats some world-reading entries in the mutant lists as data: any receiver's .replace, and subscripts or attributes of modules | Fixed in fae6b69, as claude-review#3 and agy#1. |
| claude-review-r2#1 | major | PR-3 | tools/untested.py:192 | An import nested in if/try/with, or a star import, binds nothing in _bind, so what it gives is data in the mutant lists | Fixed in c0065ce: a name is data only where a statement before the list bound it; guarded and star imports and ENV = os.environ are code. |
| claude-review-r2#2 | major | PR-3 | tools/untested.py:210 | No test holds the binding _bind reads from statements other than def/import/assign (for, with, AugAssign, class) | Fixed in c0065ce: a for loop's rebinding is held, with a case and a mutant. |
| claude-review-r2#3 | minor | PR-3 | tools/untested.py:193 | No test holds that a dotted import binds its first name as code | Fixed in c0065ce: a dotted import binds its first name, with a case and a mutant. |
| claude-review-r2#4 | minor | PR-3 | tools/untested.py:207 | No test holds that an unpacking other than A, B = x, y binds its names as code | Fixed in c0065ce: another unpacking binds its names as code, with a case and a mutant. |
| claude-review-r3#1 | major | PR-3 | tools/untested.py:193 | A name rebound or filled by something other than a plain assignment target keeps its first kind, so an entry that reads the environment is still data | Fixed in 2424df1: a walrus, a global, a method and globals() or eval unsettle a name, each with a case and a mutant. |
| claude-review-r3#2 | minor | PR-3 | tools/untested.py:193 | No test notices that an annotated assignment is read as an assignment | Fixed in 2424df1: a dunder attribute and a decorated def are code. |
| claude-review-r3#3 | minor | PR-3 | tools/untested.py:231 | An attribute chain off a helper or a text reaches os without a call, and is data | Fixed in 2424df1: replace on a helper's return is data only where the helper says -> str. |
| claude-review-r3#4 | minor | PR-3 | tools/untested.py:253 | _text takes any helper's return for a text, so replace on a helper's Path is data while ROOT.replace is code | Fixed in 2424df1: an annotated text is held, with a mutant. |
| agy-r4#1 | major | PR-3 | tests/test_untested.py:354 | Import and rebinding cases in test_a_call_in_the_mutant_lists_is_data_only_of_the_modules_own_helpers do not test what they claim | Declined: the bare import cases are vacuous now that an unbound name is code, but their bound-first siblings (os = 'a' then an import) kill each import mutant. |
| agy-r4#2 | major | PR-3 | tools/mutants.py:15389 | Calls to helpers after globals() or eval() bypass bound["*"] == "code" | Fixed in 5626728: a helper's call is code after globals() or eval. |
| agy-r4#3 | major | PR-3 | tools/mutants.py:15406 | Mutant "a helper defined after the mutant lists is theirs" disables binding before the lists instead | Fixed in dd25df3: the mutant is renamed "no statement before the mutant lists binds a name", for what it does. |
| agy-r4#4 | major | PR-3 | tools/untested.py:167 | Listed chained assignment in data_lines skips _bind and leaves prior bound names settled | Declined: data_lines binds a listed statement's other names after it reads the list (the _bind after the loop), so x in MUTANTS = x = [...] is code for a later list. |
| agy-r4#5 | major | PR-3 | tools/untested.py:201 | _bind pairs tuple unpacking 1-to-1 even when the RHS tuple contains starred items | Fixed in dd25df3: an unpacking with a starred item is no pairing, with a case and a mutant. |
| agy-r4#6 | major | PR-3 | tools/untested.py:214 | _bind and _unsettle ignore del statements and in-place subscript mutations in functions | Fixed in dd25df3: del, and a store through a subscript or attribute, make a name code, each with a case and a mutant. |
| agy-r4#7 | major | PR-3 | tools/untested.py:247 | _unsettle misses method mutations when a function precedes the variable or shares a nested name | Fixed in dd25df3 for a helper defined before its name is bound. Left: a nested scope's own name masking the module's, which only a contrived helper meets. |
| agy-r4#8 | major | PR-3 | tools/untested.py:273 | _literal allows calling a helper after globals() or exec() unsettles all names | Fixed in 5626728, as agy-r4#2. |
| claude-review-r4#1 | minor | PR-3 | tools/untested.py:286 | _text's check that a call is itself literal is held by no test: a mutant that drops it survives | Fixed in 5626728: the literal check on a call read as a text is held, with a case and a mutant. |
| claude-review-r4#2 | info | none | tools/untested.py:224 | Most of REFLECTS, and the Store-local half of own, are held by no test | Fixed in 5626728, as agy-r4#2. |
| claude-review-r4#3 | info | none | tools/untested.py:247 | A helper that changes a module list before the list is bound, or through a subscript, leaves the name data | Fixed in dd25df3 (a helper filling a list early, a subscript store), as agy-r4#6 and #7. |
| claude-review-r4#4 | info | none | tools/untested.py:273 | After a call of globals() or eval, a helper's call is still data, though decision 0166 says such a name is code | Left: exec, vars, locals, setattr and delattr share globals' path, which globals() and eval hold; the Store half of own only makes more entries code. |
| claude-review-r4#5 | info | none | tools/untested.py:406 | Text methods other than replace, and replace on a helper's tuple item, are now code where main took them for data | Deliberate: only replace on a text is data; no entry of the real lists changes. |
🤖 Generated with Claude Code