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)

DO
9h ago
+369-26

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

FindingThe fixHeld by
Any call in the mutant lists was data, so an entry that read the world, such as os.getenv('X', 'old'), escaped the checkAn 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 belowtest_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 nothingThe operator is found between the comparison's two sides. A comparison it cannot swap, or one on another line of the statement, is passed overtest_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 aloneA trailer in an earlier commit of the change is never read, and the commit asked for is the one read, not the checkout's HEADtest_a_trailer_is_read_from_the_head_commit_alone, 2 mutants
Nothing held that the change is read from the merge baseMain moving on after the branch left it adds no hunk to the changetest_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, setattr or delattr makes every name code.
  • Calls:
    • a call is data only of such a helper, or of replace on a text;
    • a helper's return counts as a text only if the helper says -> str;
    • a dunder attribute is code.
  • The real lists read exactly as before: data_lines of tools/mutants.py is the same under main's untested.py and 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 verify on an export of the head: 1716 tests OK, at 2b47efa.
  • Every mutant of tools/untested.py, run by tools.mutants on an export of the head: 83 of 83 killed at 2b47efa. 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.

ReviewerHeadUnread partsBlockingMajorMinorInfo
agyeb184e4none0200
museeb184e4none0230
claude-revieweb184e4none0030
claude-securityeb184e4none0010
claude-review-r2fe00bf1none0220
claude-review-r3c0065cenone0130
agy-r42424df1none0800
claude-review-r42424df1none0014
FindingSeverityCriterionWhereWhatDisposition
agy#1majorPR-3tools/untested.py:200The receiver guard _literal(func.value, helpers) on .replace calls has no negative test and no mutantFixed in fae6b69: replace is data only on a text; ROOT.replace is code.
agy#2majorPR-3tools/untested.py:200_literal treats os.replace(...) and any module's replace function as literal dataFixed in fae6b69: os is a name an import binds, so os.replace is code.
muse#1majorPR-3tools/untested.py:162Helper detection ignores binding order, so a shadowed name calling os.getenv is dataFixed 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#2majorPR-3tools/untested.py:200Any object's replace, including os.replace, is treated as mutant-list dataFixed in fae6b69, as agy#2.
muse#3minorPR-3tools/untested.py:200No test holds the replace receiver check against a deciding receiverFixed in fae6b69 and fe00bf1: open('x').read().replace and os.replace are held as code.
muse#4minorPR-3tools/untested.py:484No test holds the > and >= entries of the comparison SWAP tableFixed in fe00bf1: > and >= are swapped in the test, with a mutant each.
muse#5minorPR-3tools/untested.py:521Trailer test never calls trailers() for a commit other than the current HEADFixed in fe00bf1: the earlier commit is asked for after the fix round too, with a mutant.
claude-review#1minorPR-3tools/untested.py:191An entry of the mutant lists that reads the world through a subscript or attribute is still dataFixed in fae6b69: a comparison it cannot swap is passed over, with a case and a mutant.
claude-review#2minorPR-3tools/untested.py:498No test holds the one-line guard on the Compare: dropping inner.lineno == inner.end_lineno == node.lineno survivesFixed in fae6b69: a comparison on another line is passed over, with a case and a mutant.
claude-review#3minorPR-3tools/untested.py:499No test holds the SWAP guard on the comparison's operator: a mutant that removes it survives, and the change has no mutant for itFixed in fae6b69 and c0065ce: what is read from a name an import binds is code.
claude-security#1minorPR-3tools/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 modulesFixed in fae6b69, as claude-review#3 and agy#1.
claude-review-r2#1majorPR-3tools/untested.py:192An import nested in if/try/with, or a star import, binds nothing in _bind, so what it gives is data in the mutant listsFixed 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#2majorPR-3tools/untested.py:210No 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#3minorPR-3tools/untested.py:193No test holds that a dotted import binds its first name as codeFixed in c0065ce: a dotted import binds its first name, with a case and a mutant.
claude-review-r2#4minorPR-3tools/untested.py:207No test holds that an unpacking other than A, B = x, y binds its names as codeFixed in c0065ce: another unpacking binds its names as code, with a case and a mutant.
claude-review-r3#1majorPR-3tools/untested.py:193A 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 dataFixed 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#2minorPR-3tools/untested.py:193No test notices that an annotated assignment is read as an assignmentFixed in 2424df1: a dunder attribute and a decorated def are code.
claude-review-r3#3minorPR-3tools/untested.py:231An attribute chain off a helper or a text reaches os without a call, and is dataFixed in 2424df1: replace on a helper's return is data only where the helper says -> str.
claude-review-r3#4minorPR-3tools/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 codeFixed in 2424df1: an annotated text is held, with a mutant.
agy-r4#1majorPR-3tests/test_untested.py:354Import and rebinding cases in test_a_call_in_the_mutant_lists_is_data_only_of_the_modules_own_helpers do not test what they claimDeclined: 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#2majorPR-3tools/mutants.py:15389Calls to helpers after globals() or eval() bypass bound["*"] == "code"Fixed in 5626728: a helper's call is code after globals() or eval.
agy-r4#3majorPR-3tools/mutants.py:15406Mutant "a helper defined after the mutant lists is theirs" disables binding before the lists insteadFixed in dd25df3: the mutant is renamed "no statement before the mutant lists binds a name", for what it does.
agy-r4#4majorPR-3tools/untested.py:167Listed chained assignment in data_lines skips _bind and leaves prior bound names settledDeclined: 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#5majorPR-3tools/untested.py:201_bind pairs tuple unpacking 1-to-1 even when the RHS tuple contains starred itemsFixed in dd25df3: an unpacking with a starred item is no pairing, with a case and a mutant.
agy-r4#6majorPR-3tools/untested.py:214_bind and _unsettle ignore del statements and in-place subscript mutations in functionsFixed in dd25df3: del, and a store through a subscript or attribute, make a name code, each with a case and a mutant.
agy-r4#7majorPR-3tools/untested.py:247_unsettle misses method mutations when a function precedes the variable or shares a nested nameFixed 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#8majorPR-3tools/untested.py:273_literal allows calling a helper after globals() or exec() unsettles all namesFixed in 5626728, as agy-r4#2.
claude-review-r4#1minorPR-3tools/untested.py:286_text's check that a call is itself literal is held by no test: a mutant that drops it survivesFixed in 5626728: the literal check on a call read as a text is held, with a case and a mutant.
claude-review-r4#2infononetools/untested.py:224Most of REFLECTS, and the Store-local half of own, are held by no testFixed in 5626728, as agy-r4#2.
claude-review-r4#3infononetools/untested.py:247A helper that changes a module list before the list is bound, or through a subscript, leaves the name dataFixed in dd25df3 (a helper filling a list early, a subscript store), as agy-r4#6 and #7.
claude-review-r4#4infononetools/untested.py:273After a call of globals() or eval, a helper's call is still data, though decision 0166 says such a name is codeLeft: 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#5infononetools/untested.py:406Text methods other than replace, and replace on a helper's tuple item, are now code where main took them for dataDeliberate: only replace on a text is data; no entry of the real lists changes.

🤖 Generated with Claude Code