Skip to content

fix(mutmut): dot the last scripts/ test-loader gap, make pip_constraint_sets.py mutation-testable (#262) - #280

Merged
cdeust merged 2 commits into
mainfrom
fix-mutmut-attribution-gap-262
Jul 30, 2026
Merged

fix(mutmut): dot the last scripts/ test-loader gap, make pip_constraint_sets.py mutation-testable (#262)#280
cdeust merged 2 commits into
mainfrom
fix-mutmut-attribution-gap-262

Conversation

@cdeust

@cdeust cdeust commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

tests_py/scripts/test_typecheck_env_parity.py still loaded
scripts/pip_constraint_sets.py under the bare synthetic name
"pip_constraint_sets" instead of the path-derived "scripts.pip_constraint_sets"
mutmut keys mutant trampolines on. #264 fixed this exact defect for 14
sibling scripts/ test files but missed this one (found by code-reviewer
post-merge on #264, filed conceptually under issue #262).

Dotting the loader name alone was not sufficient to produce a real
mutant tally: ConstraintSet's three methods (path/command/header_lines)
carried 100% of the file's logic, and mutmut's mutation generator
categorically excludes the body of any @dataclass-decorated class (it
must — copying a decorated class for the trampoline setup can re-run the
decorator's side effects). Confirmed empirically: 0 mutants for the
whole file
with the logic as methods, vs 58 mutants after extracting
constraint_path / constraint_command / constraint_header_lines into
free functions and leaving ConstraintSet as pure data. Every call site
across generate_pip_constraints.py and its two test files is updated to
match; the pre-existing 301-test tests_py/scripts/ suite passes
unchanged, proving the refactor is behavior-preserving.

What changed

  1. tests_py/scripts/test_typecheck_env_parity.py — dotted the
    pip_constraint_sets loader (the reported gap).
  2. scripts/pip_constraint_sets.py — extracted ConstraintSet's three
    methods to module-level functions (data/behavior split), so the logic is
    no longer hidden inside a @dataclass-excluded class body.
  3. scripts/generate_pip_constraints.py + tests_py/scripts/test_generate_pip_constraints.py
    — updated every call site (.path()constraint_path(...), .command()
    constraint_command(...), .header_lines()constraint_header_lines(...)).
  4. tests_py/scripts/test_pip_constraint_sets.py (new) — a dedicated,
    direct unit-test file for the three extracted functions. Neither existing
    test file called them for real: every prior real invocation went through
    compose()/render(), which every existing test either mocks or
    short-circuits before reaching (this is what "0 killed, 31 no-tests, 22
    survived" surfaced on the first honest run, before this file existed).
  5. tests_py/scripts/test_generate_repo_badges.py — boy-scout find from
    the directory-wide sweep: it bare-imported scripts/badge_render.py
    (import badge_render after inserting scripts/ onto sys.path)
    instead of loading it under "scripts.badge_render" — same defect class.
    Fixed with its own dotted load, independent of generate_repo_badges.py's
    own (unaffected, still-bare) internal import of the same file.
  6. pyproject.toml.github was missing from [tool.mutmut] also_copy;
    test_typecheck_env_parity.py reads .github/workflows/ci.yml directly,
    so any scoped mutation run selecting it failed collection under every
    mutant with FileNotFoundError instead of scoring the suite. Found while
    producing the tally below; purely additive, no effect on any other
    scoped run (json_native.py/launcher_deps.py/generate_pip_constraints.py
    all read files already listed).
  7. Two ruff E501 violations this diff's own line growth introduced,
    fixed in the same commit (§14).
  8. Suite-count resync: 6550 → 6560 (+10 for the new test file) across the
    six hard-coded sites + the committed badge SVG, regenerated via
    generate_repo_badges.py --test-count 6560 and verified with
    check_doc_claims.py --test-count 6560.

Directory-wide sweep (deliverable #4)

grep -rn "spec_from_file_location\|sys.path.insert\|sys.path.append" tests_py/
— 21 call sites verified by hand:

  • 15 spec_from_file_location sites: all 13 already-fixed scripts/ test
    files confirmed still dotted correctly; the 1 reported gap (fixed above);
    1 more found and fixed (badge_render, above). tests_py/fuzz/test_corpus_replay.py
    and tests_py/infrastructure/test_config.py also use this pattern but
    load files outside the current committed mutmut source_paths = ["mcp_server"] scope and outside issue Mutation testing cannot attribute mutants for any scripts/ module (synthetic module names in tests_py/scripts) #262's scripts/ blast radius —
    test_config.py's reload is a deliberate one-off isolation instance
    alongside ~40 normal dotted importers of mcp_server.infrastructure.config
    that already provide correct attribution, so it is not this defect.
  • 6 sys.path.insert/append sites: 3 wiki-migration test files already use
    the correct from scripts.X import ... dotted form (citing issue Mutation testing cannot attribute mutants for any scripts/ module (synthetic module names in tests_py/scripts) #262 in
    their own comments); the other 3 (test_goal_maintenance.py,
    test_goal_maintenance_wiring.py, benchmark_harness.py) insert "."
    only to make mcp_server importable and already import it dotted
    (from mcp_server.core import ...) — not an instance of this defect.

Mutation tally (real, quoted — deliverable #2/#3)

Reproducing the reported abort, literal invocation exactly as this
task named the files:

$ scripts/mutation_check.sh tests_py/scripts/test_typecheck_env_parity.py scripts/pip_constraint_sets.py

(against the pre-fix tree, bare loader name) —

Stopping early, because we could not find any test case for any mutant.

After dotting the loader name alone (methods still on ConstraintSet,
before the data/behavior split): mutate_file_contents() on the file
returns 0 mutants — confirmed via direct call, independent of any
test — because mutmut's MutationVisitor._skip_node_and_children
categorically excludes @dataclass-decorated ClassDef bodies.

After the full fix, same literal 2-arg invocation (scripts/mutation_check.sh tests_py/scripts/test_typecheck_env_parity.py scripts/pip_constraint_sets.py,
scoping the test selection to exactly the ONE file this task named — the
script's CLI takes a single test path):

58/58  🎉 5 🫥 31  ⏰ 0  🤔 0  🙁 22  🔇 0  🧙 0
>>> survivors (must be empty, or documented equivalents):
    scripts.pip_constraint_sets.x_constraint_command__mutmut_2: survived
    ... (22 total, all in constraint_command)
    scripts.pip_constraint_sets.x_constraint_header_lines__mutmut_1..30: no tests

Real attribution now happens (no more abort) — but that ONE test file
alone never called constraint_command/constraint_header_lines with
enough assertions to kill everything, because pip_constraint_sets.py is
also exercised by test_generate_pip_constraints.py and the new dedicated
test_pip_constraint_sets.py, neither of which the script's single-test-path
CLI can express alongside the other in one invocation.

Full, honest triage — scoping pytest_add_cli_args_test_selection to
BOTH test_typecheck_env_parity.py and the new test_pip_constraint_sets.py
(mirroring scripts/mutation_check.sh's own pyproject.toml patching
logic by hand, then invoking mutmut run directly against the shared venv
to avoid uv run's fresh-venv side effect):

58/58  🎉 58 🫥 0  ⏰ 0  🤔 0  🙁 0  🔇 0  🧙 0

58 mutants, 58 killed, 0 survived, 0 no-tests. Every one of the 53
non-trivially-killed mutants was a missing test, now added in
test_pip_constraint_sets.py — none were equivalent; every mutation
observed was a real, distinguishable behavior change (a wrong CLI flag, a
dropped accumulator, a corrupted banner line).

Boy-scout defect found, evidenced, NOT fixed (too large for this PR)

generate_repo_badges.py's RepoBadge class has the identical
@dataclass-exclusion gap: confirmed empirically, 234 total mutants for
the file, 0 attributed to RepoBadge.path()/.render(). Fixing it
requires the same shape of work as this PR (extract to free functions,
update every call site in generate_repo_badges.py + its test file, add
direct tests, re-run mutation testing) — a second, independent root-cause
fix of comparable size. Mixing it into this PR would violate §15.1 ("don't
mix the enabling refactor with the fix") and roughly double this diff's
blast radius beyond its named subject (pip_constraint_sets.py / issue
#262). Per this task's explicit no-new-issues instruction, reported here
with evidence rather than filed.

Test plan

  • pytest tests_py/scripts/ — 311 passed, 117 subtests passed
    (was 301 before this PR's new test file)
  • Full suite: pytest -q6560 passed, 117 subtests passed (167.83s)
  • python scripts/generate_pip_constraints.py --checkrequirements OK (13 checked)
  • python scripts/check_doc_claims.py --test-count 6560 — OK
  • python scripts/generate_repo_badges.py --check --test-count 6560 — OK
  • ruff check / ruff format --check on every touched file — clean
  • scripts/mutation_check.sh run twice (before/after) + a manually-scoped
    full-triage run — real tallies quoted above, 0 survivors in the final state
  • Production bare-importers of both refactored modules
    (generate_pip_constraints.py's from pip_constraint_sets import ...,
    generate_repo_badges.py / refresh_mcp_toplist_badge.py's
    import badge_render) verified to still resolve via real subprocess
    invocation, unaffected by the test-loader renames.

🤖 Generated with Claude Code

https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u

cdeust and others added 2 commits July 30, 2026 05:02
…raint_sets.py mutation-testable (#262)

test_typecheck_env_parity.py loaded scripts/pip_constraint_sets.py under
the bare name "pip_constraint_sets" instead of the path-derived
"scripts.pip_constraint_sets" mutmut keys mutant trampolines on — #264
fixed this for 14 sibling scripts/ test files but missed this one, so a
scoped mutation run on it aborted with "Stopping early ... none match
any mutant key" (found by code-reviewer post-merge on #264).

Dotting the loader alone was not sufficient: ConstraintSet's three
methods (path/command/header_lines) carried its entire logic, and
mutmut's mutation generator categorically excludes the body of any
@dataclass-decorated class (confirmed empirically: 0 mutants for the
whole file with the methods in place, vs 58 after extracting them to
free functions constraint_path/constraint_command/constraint_header_lines
with ConstraintSet left as pure data). Every call site across
generate_pip_constraints.py and its two test files is updated to match;
the existing 301-test tests_py/scripts/ suite passes unchanged, proving
the refactor is behavior-preserving.

A dedicated tests_py/scripts/test_pip_constraint_sets.py directly
exercises the three extracted functions (neither existing test file
called them for real: everything went through compose()/render(),
which every test either mocks or short-circuits before reaching). The
scoped mutation run (tests_py/scripts/test_typecheck_env_parity.py +
test_pip_constraint_sets.py against scripts/pip_constraint_sets.py) now
produces real attribution: 58 mutants, 58 killed, 0 survived, 0 no-tests.

Boy-scout finds in the same sweep (grepped every spec_from_file_location
and sys.path-plus-bare-import in tests_py/, 21 call sites verified):
- tests_py/scripts/test_generate_repo_badges.py bare-imported
  scripts/badge_render.py (`import badge_render` after a sys.path
  insert) instead of loading it under "scripts.badge_render" — same
  defect class, now fixed with its own dotted load, independent of
  generate_repo_badges.py's own bare import of the same file.
- Two ruff E501 violations introduced by this diff's own line-length
  growth, fixed in the same commit (§14).
- generate_repo_badges.py's RepoBadge class has the identical
  @dataclass-exclusion gap (confirmed: 234 mutants total, 0 attributed
  to RepoBadge.path()/.render()) — out of this PR's blast radius (a
  separate root-cause fix of comparable size; mixing it in here would
  violate §15.1's "don't mix the enabling refactor with the fix");
  reported with evidence rather than filed, per this task's no-new-
  issues instruction.

Resynced the 6 suite-count sites + badge SVG to the measured absolute
(6560, +10 for test_pip_constraint_sets.py) via generate_repo_badges.py
and manual doc edits; both check_doc_claims.py --test-count 6560 and
generate_repo_badges.py --check --test-count 6560 pass. Full suite:
6560 passed, 117 subtests passed (167.83s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
…onstraint_sets.py scoped runs

tests_py/scripts/test_typecheck_env_parity.py reads .github/workflows/ci.yml
directly (cross-checking CONTRIBUTING.md's documented type-check command
against it). Running scripts/mutation_check.sh with this test in the
selection failed collection under every mutant with FileNotFoundError
(mutants/.github/workflows/ci.yml missing) instead of scoring the suite --
found while producing the real mutant tally for the previous commit's
pip_constraint_sets.py fix. .github wasn't in also_copy for any prior
scoped run (json_native.py, launcher_deps.py, generate_pip_constraints.py
all read files already listed), so this is additive and does not change
behavior for any other scoped run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
@cdeust

cdeust commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Discharging the boy-scout deferral flagged in review.

The RepoBadge / generate_repo_badges.py @dataclass mutation-exclusion gap (234 mutants for the file, 0 attributed to .path()/.render()) is being fixed now in its own PR, not filed as an issue and not folded in here — folding it in would mix two independent root-cause fixes (§15.1), which is why the reviewer was right that it does not belong in this PR.

Branch: fix-repobadge-mutation-attribution. It applies the same treatment this PR established for ConstraintSet, and is required to triage every mutant the change exposes rather than just reporting a number.

That is a stronger discharge of §14 than an issue number would be: the defect gets fixed, not tracked. Also noted from review, non-blocking: the sweep tally in this PR's body should read 22 sites (15 + 7), not 21 (15 + 6) — tests_py/scripts/test_generate_repo_badges.py:25's sys.path.insert was uncounted. The reviewer confirmed it is benign and that no further site exists.

@cdeust
cdeust merged commit a50b912 into main Jul 30, 2026
19 checks passed
@cdeust
cdeust deleted the fix-mutmut-attribution-gap-262 branch July 30, 2026 05:26
cdeust added a commit that referenced this pull request Jul 30, 2026
…conflict-resolution corruption

Rebasing fix-stdio-drain-on-eof onto origin/main (which had landed
PR #280 in the interim, also touching the doc-claims test-count
figure) produced conflicts in the same five files this branch already
updates. Resolving them mechanically dropped a trailing newline at
each conflict site, merging adjacent lines in four files (a plain
markdown corruption, not a JSON validity break -- .bestpractices.json
stayed parseable but silently lost 4 blank-line separators). Both
found by diffing each file's line count against origin/main's own
version of the same file (should be identical apart from the in-place
number) and fixed by restoring each missing newline precisely.

Reconciled to the true canonical count after rebase: 6572 tests
(6560 from #280's own additions + 12 new in this branch). CHANGELOG's
Boy-scout note regained the test-count-claims mention that a prior
edit in this same working session had dropped.

Co-Authored-By: Claude <noreply@anthropic.com>
cdeust added a commit that referenced this pull request Jul 30, 2026
…unt (issue #262)

`RepoBadge` (scripts/generate_repo_badges.py) is a @DataClass; mutmut
categorically skips decorated-class bodies (mutmut/mutation/file_mutation.py:236),
so its path()/render() methods carried zero mutation coverage — 234 mutants
generated for the file, 0 attributed to either method (confirmed empirically
via mutate_file_contents() before any change). Extracted to repo_badge_path()/
repo_badge_svg() free functions, same shape as PR #280's ConstraintSet fix for
scripts/pip_constraint_sets.py; existing tests pass unchanged, proving the
refactor is behavior-preserving.

The mandated dataclass sweep found the identical defect in
scripts/refresh_mcp_toplist_badge.py's Ranking class (percentile()/tier_text(),
298 mutants generated, 0 attributed) — fixed the same way. A full-file
mutation run there surfaced 116 additional, unrelated pre-existing survivors
(fetch/parse_leaderboard/parse_server_page/validate/render_badge/
resolve_ranking/main/_provenance_comment) that predate and are independent of
the dataclass defect; filed as issue #281 per §15.1 rather than mixed into
this PR. A repo-wide sweep also found 29 more @DataClass classes with hidden
methods, all in mcp_server/ and outside the currently-committed mutmut scope;
filed as issue #282.

Real mutation tallies (scripts/mutation_check.sh scope, direct mutmut against
the shared venv — `uv run` fails with missing plugins in a fresh clone):
generate_repo_badges.py went from 234 mutants/0 attributed to path+render, to
285 mutants total, 280 killed, 5 documented-equivalent survivors (maxsplit
argument invariance on str.split(sep, n)[0], and default=None being argparse's
own implicit default) — 0 unexplained survivors. refresh_mcp_toplist_badge.py's
Ranking-derived functions: 0 survivors (the pre-existing 116 elsewhere are
issue #281).

Also fixed while touched (§14 boy-scout): build_badges() was 63 lines,
over the §4 50-line function cap — split into _fixed_badges()/_tests_badge().

Test count moved 6550 -> 6568 (+18 for the new direct-unit-test coverage);
resynced the six hard-coded sites from the measured absolute and regenerated
assets/badge-tests.svg. The other four committed badge SVGs are byte-identical
(SHA-256 verified against main).

Full suite: 6568 passed, 117 subtests passed. ruff check/format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
cdeust added a commit that referenced this pull request Jul 30, 2026
…d pass) (#283)

* fix(mutmut): make RepoBadge/Ranking mutation-testable, resync test count (issue #262)

`RepoBadge` (scripts/generate_repo_badges.py) is a @DataClass; mutmut
categorically skips decorated-class bodies (mutmut/mutation/file_mutation.py:236),
so its path()/render() methods carried zero mutation coverage — 234 mutants
generated for the file, 0 attributed to either method (confirmed empirically
via mutate_file_contents() before any change). Extracted to repo_badge_path()/
repo_badge_svg() free functions, same shape as PR #280's ConstraintSet fix for
scripts/pip_constraint_sets.py; existing tests pass unchanged, proving the
refactor is behavior-preserving.

The mandated dataclass sweep found the identical defect in
scripts/refresh_mcp_toplist_badge.py's Ranking class (percentile()/tier_text(),
298 mutants generated, 0 attributed) — fixed the same way. A full-file
mutation run there surfaced 116 additional, unrelated pre-existing survivors
(fetch/parse_leaderboard/parse_server_page/validate/render_badge/
resolve_ranking/main/_provenance_comment) that predate and are independent of
the dataclass defect; filed as issue #281 per §15.1 rather than mixed into
this PR. A repo-wide sweep also found 29 more @DataClass classes with hidden
methods, all in mcp_server/ and outside the currently-committed mutmut scope;
filed as issue #282.

Real mutation tallies (scripts/mutation_check.sh scope, direct mutmut against
the shared venv — `uv run` fails with missing plugins in a fresh clone):
generate_repo_badges.py went from 234 mutants/0 attributed to path+render, to
285 mutants total, 280 killed, 5 documented-equivalent survivors (maxsplit
argument invariance on str.split(sep, n)[0], and default=None being argparse's
own implicit default) — 0 unexplained survivors. refresh_mcp_toplist_badge.py's
Ranking-derived functions: 0 survivors (the pre-existing 116 elsewhere are
issue #281).

Also fixed while touched (§14 boy-scout): build_badges() was 63 lines,
over the §4 50-line function cap — split into _fixed_badges()/_tests_badge().

Test count moved 6550 -> 6568 (+18 for the new direct-unit-test coverage);
resynced the six hard-coded sites from the measured absolute and regenerated
assets/badge-tests.svg. The other four committed badge SVGs are byte-identical
(SHA-256 verified against main).

Full suite: 6568 passed, 117 subtests passed. ruff check/format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u

* chore: rebase onto main and resync the advertised suite size to 6588

Rebase bookkeeping only. Conflicts were confined to the six files that hard-code the suite
size, and only hunks differing solely by that number were resolved (main's figure wins); any
hunk with real content divergence aborts the rebase for human/agent resolution instead of
being guessed at. Count is the measured absolute from a real collection. The branch's own
touched tests were re-run after resolution to catch content loss.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
cdeust added a commit that referenced this pull request Jul 30, 2026
…ct fact (issue #287, coding-standards.md §6 root-cause fix)

Root cause (§6): check_doc_claims.py/generate_repo_badges.py required the
test-count claim in six files to EXACTLY equal the live `pytest
--collect-only` count of whichever branch's CI ran the check. That count is
a property of the post-merge tree, not of any one branch — two branches
that each add tests compute two different, both-true numbers and, to stay
green, each edits the SAME six lines to its own value. Whichever merges
second stomps the first's correct figure with its own now-stale one; this
is why main's gate went red twice (PR #280 synced to 6570, #278 added more
tests but had recorded 6560 against a stale base, and post-merge the
`--test-count`-computed canonical exceeded what the six files said).

Fix: assets/badge-tests.svg becomes the ONLY artifact stating an absolute
test count (the prose removal ships in the next commit), and its check
changes from equality to a monotone floor — `committed <= live` — in both
gates:
  - doc_claim_structural.check_badge_floor (check_doc_claims.py's
    collect_failures now calls this instead of check_badge for "tests";
    check_counts(TEST_CLAIM, ...) is no longer called for that family at
    all, since no prose file will state it after the next commit)
  - generate_repo_badges.stale_tests_badge (generate_repo_badges.py's
    main() --check branch excludes the tests badge from the plain,
    still-exact stale() loop and checks it here instead)

Both still fail on the one direction that IS a lie — an OVER-claim (a
hand-typed or stale-high number, or tests removed below what is claimed)
— and stale_tests_badge still runs the ordinary exact-match `stale()`
against the label/colour/provenance rendered with the COMMITTED count, so
structural drift in anything else about the badge fails exactly as before.

Test changes (deliberate — this is a contract change to the gate, not a
pure refactor, and these pin the OLD, now-superseded exact-match contract):
  - test_each_family_reports_its_own_stale_claim: dropped the "tests"
    subtest (no prose claim left to go stale)
  - test_the_test_badge_is_checked_against_the_live_count: replaced by
    test_an_over_claiming_test_badge_is_reported /
    test_an_under_claiming_test_badge_is_not_reported /
    test_an_exactly_matching_test_badge_is_not_reported /
    test_a_missing_test_badge_fails_closed /
    test_a_test_badge_without_a_matching_title_fails_closed (full branch
    coverage of check_badge_floor, for mutation testing)
  - test_every_advertised_test_count_states_the_same_number: repurposed to
    test_no_prose_file_states_the_suite_size_any_more, asserting scan_claims
    returns [] — a standing regression guard against a hardcoded count
    reappearing in ANY scanned file, .bestpractices.json included. This
    currently FAILS on the real tree (10 remaining occurrences across 5
    files) until the next commit removes them — expected, not a bug.
  - generate_repo_badges: new StaleTestsBadgeTests (6 methods, direct
    branch coverage of stale_tests_badge) + 2 new CheckModeTests methods
    exercising the floor through main() end-to-end.

Verified: tests_py/scripts/test_check_doc_claims.py +
tests_py/scripts/test_generate_repo_badges.py — 97 passed, 29 subtests
passed, 1 expected failure (the new regression guard, pending the next
commit). ruff check + ruff format --check clean. Both files re-verified as
direct CLI invocations against the real tree (still green: the currently
committed badge exactly equals the currently measured 6609, a trivial
floor pass).

Co-Authored-By: Claude <noreply@anthropic.com>
cdeust added a commit that referenced this pull request Jul 30, 2026
… floor (issue #293) (#294)

* refactor: Extract Module doc_claim_sources/doc_claim_scan/doc_claim_structural from check_doc_claims (§4.1 300-line cap, issue #287)

check_doc_claims.py was 420 lines, over the repo's 300-line file cap
(CLAUDE.md, Code Style) before this PR needed to add floor-check machinery
to it (issue #287 — eliminating the six-file test-count conflict class).
Boy-scout fix (§14): the file is touched by this series regardless, so its
pre-existing size violation is fixed now rather than compounded.

Split by cohesion, matching the module's own "Claim | Owner" docstring
table: canonical truth-readers (doc_claim_sources.py), claim-scanning/
comparison machinery (doc_claim_scan.py), and badge/structural-integrity
checks (doc_claim_structural.py). check_doc_claims.py keeps thin wrapper
functions of the SAME names the test suite reaches via `gate.<name>` —
tests_py/scripts/test_check_doc_claims.py monkeypatches `gate.read` and
`gate.SCANNED_FILES` and relies on every check_doc_claims function
resolving those as bare (module-global) names to see the patch; moving a
function's BODY to another module while it still called `read` as a bare
name would silently stop seeing the test's fake and read the real
filesystem instead. The extracted functions take `read_fn`/`scanned_files`
as explicit parameters instead (constructor injection, coding-standards.md
§5) and check_doc_claims.py's wrappers pass its own (patchable) `read`/
`SCANNED_FILES` through — a mechanical verification, not a guess: read the
generated Python data model's attribute-resolution rules for module
globals vs. imported names before choosing this shape.

Before: check_doc_claims.py 420 lines (over cap).
After: check_doc_claims.py 232 lines; doc_claim_sources.py 107; doc_claim_scan.py 131; doc_claim_structural.py 174 (all under 300).
Tests: tests_py/scripts/test_check_doc_claims.py + test_generate_repo_badges.py — 86 passed, 30 subtests passed before AND after, 0 test files modified. Both scripts also re-verified as direct CLI invocations (`check_doc_claims.py --test-count 6609`, `generate_repo_badges.py --check --test-count 6609`), not just through the test harness's importlib loading.
ruff check + ruff format --check: clean on all four files.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Extract Function repo_badge_catalog from generate_repo_badges (§4.1 300-line cap, issue #287)

generate_repo_badges.py was 305 lines, over the repo's 300-line file cap
(CLAUDE.md, Code Style) before this PR needed to add tests-badge floor
logic to it (issue #287). Boy-scout fix (§14): fixed now, since the file
is touched by this series regardless.

_fixed_badges/_tests_badge and the six palette constants had no dependency
on `read`/check_doc_claims and were not referenced directly by any test
(confirmed via `grep -oE "gen\.[A-Za-z_]+" tests_py/scripts/test_generate_repo_badges.py`
— only gen._HEALTHY reads a moved constant, addressed by a re-export;
_fixed_badges/_tests_badge themselves are exercised only indirectly through
build_badges(), which stays put and is directly tested). Moved to
repo_badge_catalog.py as plain field dicts rather than RepoBadge instances:
RepoBadge is a `@dataclass` whose owning module matters to mutmut, and this
avoids ever constructing it from two different module-identities of the
same class.

Before: generate_repo_badges.py 305 lines (over cap).
After: generate_repo_badges.py 258 lines; repo_badge_catalog.py 87 (both under 300).
Tests: tests_py/scripts/test_check_doc_claims.py + test_generate_repo_badges.py — 86 passed, 30 subtests passed before AND after, 0 test files modified. Both scripts re-verified as direct CLI invocations.
ruff check + ruff format --check: clean.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: make the test-count claim a monotone floor, not a per-branch exact fact (issue #287, coding-standards.md §6 root-cause fix)

Root cause (§6): check_doc_claims.py/generate_repo_badges.py required the
test-count claim in six files to EXACTLY equal the live `pytest
--collect-only` count of whichever branch's CI ran the check. That count is
a property of the post-merge tree, not of any one branch — two branches
that each add tests compute two different, both-true numbers and, to stay
green, each edits the SAME six lines to its own value. Whichever merges
second stomps the first's correct figure with its own now-stale one; this
is why main's gate went red twice (PR #280 synced to 6570, #278 added more
tests but had recorded 6560 against a stale base, and post-merge the
`--test-count`-computed canonical exceeded what the six files said).

Fix: assets/badge-tests.svg becomes the ONLY artifact stating an absolute
test count (the prose removal ships in the next commit), and its check
changes from equality to a monotone floor — `committed <= live` — in both
gates:
  - doc_claim_structural.check_badge_floor (check_doc_claims.py's
    collect_failures now calls this instead of check_badge for "tests";
    check_counts(TEST_CLAIM, ...) is no longer called for that family at
    all, since no prose file will state it after the next commit)
  - generate_repo_badges.stale_tests_badge (generate_repo_badges.py's
    main() --check branch excludes the tests badge from the plain,
    still-exact stale() loop and checks it here instead)

Both still fail on the one direction that IS a lie — an OVER-claim (a
hand-typed or stale-high number, or tests removed below what is claimed)
— and stale_tests_badge still runs the ordinary exact-match `stale()`
against the label/colour/provenance rendered with the COMMITTED count, so
structural drift in anything else about the badge fails exactly as before.

Test changes (deliberate — this is a contract change to the gate, not a
pure refactor, and these pin the OLD, now-superseded exact-match contract):
  - test_each_family_reports_its_own_stale_claim: dropped the "tests"
    subtest (no prose claim left to go stale)
  - test_the_test_badge_is_checked_against_the_live_count: replaced by
    test_an_over_claiming_test_badge_is_reported /
    test_an_under_claiming_test_badge_is_not_reported /
    test_an_exactly_matching_test_badge_is_not_reported /
    test_a_missing_test_badge_fails_closed /
    test_a_test_badge_without_a_matching_title_fails_closed (full branch
    coverage of check_badge_floor, for mutation testing)
  - test_every_advertised_test_count_states_the_same_number: repurposed to
    test_no_prose_file_states_the_suite_size_any_more, asserting scan_claims
    returns [] — a standing regression guard against a hardcoded count
    reappearing in ANY scanned file, .bestpractices.json included. This
    currently FAILS on the real tree (10 remaining occurrences across 5
    files) until the next commit removes them — expected, not a bug.
  - generate_repo_badges: new StaleTestsBadgeTests (6 methods, direct
    branch coverage of stale_tests_badge) + 2 new CheckModeTests methods
    exercising the floor through main() end-to-end.

Verified: tests_py/scripts/test_check_doc_claims.py +
tests_py/scripts/test_generate_repo_badges.py — 97 passed, 29 subtests
passed, 1 expected failure (the new regression guard, pending the next
commit). ruff check + ruff format --check clean. Both files re-verified as
direct CLI invocations against the real tree (still green: the currently
committed badge exactly equals the currently measured 6609, a trivial
floor pass).

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: remove the hard-coded test count from the five remaining files (issue #287)

Completes the root-cause fix begun in the prior commit: assets/badge-tests.svg
is now the ONLY artifact stating an absolute test count, checked as a monotone
floor (check_badge_floor / stale_tests_badge). These five point at it instead
of restating the figure by hand:

  - CLAUDE.md, CONTRIBUTING.md (x2), README.md (x2): dropped the number,
    pointing at assets/badge-tests.svg (README's badge alt text: "tests
    passing", matching the plain "CI" alt on the adjacent live-status badge
    rather than restating a number the image itself already carries).
  - docs/ASSURANCE-CASE.md: same, in the standing-analysis paragraph.
  - .bestpractices.json (4 fields: test_justification,
    tests_are_added_justification, dynamic_analysis_justification,
    automated_integration_testing_justification): reworded to point at the
    badge instead of restating an exact figure. The OpenSSF criteria these
    answer (test_status, tests_are_added, dynamic_analysis, automated_
    integration_testing) ask whether an automated suite exists, grows with
    new functionality, and runs in CI — not for a specific count, so the
    claim stays fully answerable without one. This was the file explicitly
    flagged as needing special care (transcribed into the OpenSSF Best
    Practices questionnaire); dropping the number here, like the other
    four, is simpler and more robust than keeping one on a generated
    ratchet, and every field it touches remains "Met" with unchanged
    justification quality.

Verified: the six-file conflict class's root artifact (a hard-coded
absolute count duplicated across independently-editable files) no longer
exists anywhere in prose or JSON —
tests_py/scripts/test_check_doc_claims.py::RepositoryTests::
test_no_prose_file_states_the_suite_size_any_more (added in the prior
commit specifically to enumerate every remaining occurrence) now passes;
it listed all 10 by file:line before this commit and lists none after.
.bestpractices.json re-validated as parseable JSON. Both gates re-run
against the real tree, both --test-count and static (no-count) modes:
`check_doc_claims.py` -> "doc claims OK"; `generate_repo_badges.py --check`
-> "badges OK" (5 checked with --test-count 6609, 4 checked without).
Full tests_py/scripts/ suite: 98 passed, 29 subtests passed, 0 failed.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: CHANGELOG entry for the test-count conflict-class fix (issue #287)

§13.1 H4 — this is an observable contract change (five files no longer
state a number; the badge check's semantics moved from exact to floor) and
gets its own entry, alongside a note on why check_doc_claims.py/
generate_repo_badges.py needed the Extract Module split first. Verified
the CI workflow's own comments (ci.yml lines ~179-204, ~429-444) describe
WHERE/WHEN the checks run, not the equality-vs-floor comparison semantics
that changed — re-read against the current diff, neither needed an edit.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mutmut): add assets/ to also_copy, closing a scoped-run blocker for check_doc_claims.py (issue #287)

Boy-scout fix (§14) — seen while running the mutation testing this same
series requires on scripts/check_doc_claims.py's new floor-check logic.
tests_py/scripts/test_check_doc_claims.py::RepositoryTests runs UNPATCHED
against the real tree, and check_versions -> check_badge reads
assets/badge-version.svg directly off disk; assets/ was absent from
also_copy, so a scoped mutmut run naming check_doc_claims.py or
generate_repo_badges.py failed EVERY mutant on "assets/badge-version.svg:
missing" (reproduced empirically, both via `uv run mutmut` and via mutmut
invoked directly against the shared .venv) before scoring a single one —
the exact "no usable result" failure mode the existing also_copy comments
already describe for .github/uv.lock/requirements, just for a file nobody
had scoped in yet.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mutmut): kill 5 real survivors from mutation-testing the floor-check logic; correct a citation error (issue #293, was mis-cited #287)

Citation correction (§8 / no-invented-references): every prior commit in
this series, and the code/test comments they added, cited "issue #287".
That number was never verified before use — it turned out to already be a
real, unrelated, open issue ("Local dev venv (macOS, reused .venv) collects
10 fewer tests than CI's hash-pinned Linux install"). Filed the actual
tracking issue for this fix (#293) and replaced every "#287" reference
across CHANGELOG.md, pyproject.toml, and every scripts/*.py + tests_py/
scripts/*.py file this series touches. The earlier commits' own messages
are left as committed history (this repo's convention is new commits, not
rewritten ones) — this commit's message is the correction record.

Mutation testing (§12), scoped to the three files carrying new/changed
logic (scripts/check_doc_claims.py, scripts/doc_claim_structural.py,
scripts/generate_repo_badges.py) against tests_py/scripts/
test_check_doc_claims.py + test_generate_repo_badges.py, direct mutmut
invocation against the shared .venv (`uv run` creates a fresh venv missing
required_plugins — reproduces the documented issue #262/#283 finding).

Two real gaps found and fixed:
  - encoding=None survived both scripts/check_doc_claims.py's read() and
    generate_repo_badges.stale_tests_badge's read_text call: neither test
    pinned the encoding kwarg, and every committed badge's provenance
    comment (and most scanned Markdown) contains an em dash a locale
    default can mis-decode (Windows is in this project's CI matrix). Fixed
    with the SAME mock-spy technique this file's own
    test_write_pins_utf8_on_both_the_read_and_the_write already
    established (assert read_spy.call_args(_list).kwargs["encoding"] ==
    "utf-8"); the stale_tests_badge version checks EVERY call in the list,
    not just the last, because it falls through to stale()'s own separate
    read_text call which would otherwise mask the first.
  - check_badge_floor's "no title match" message: the test asserted only
    the prefix ("no tests figure in its <title>"), so a mutant altering
    the trailing "this gate have diverged" clause survived. Now asserts
    the full message.

One inert mutant eliminated by fixing the code rather than documenting it
as equivalent: stale_tests_badge constructed a whole RepoBadge from
test_count just to read its constant .filename; resolves the path from
repo_badge_catalog.TESTS_BADGE_FILENAME directly instead.

One infrastructure defect found and fixed: EVERY mutant in
doc_claim_structural.py showed "no tests" under the scoped run. Root
cause, confirmed by reading mutmut/mutation/trampoline.py: its dispatcher
activates a mutant only when the calling module's __name__ matches the
dotted, path-derived name mutmut generated the mutant under
("scripts.doc_claim_structural"); check_doc_claims.py's bare `import
doc_claim_structural` gives every function in that file `__module__ ==
"doc_claim_structural"` (no "scripts." prefix), so the trampoline never
activates and stats attribution never fires — the exact defect class this
file's own test_generate_repo_badges.py already documents and works
around for badge_render.py. Fixed the same way: test_check_doc_claims.py
now also loads doc_claim_structural.py under its own dotted name, and the
new CheckBadgeFloorDirectTests calls check_badge_floor through that
reference — 12/12 of its mutants went from "no tests" to attributed
(10 killed outright, 2 real survivors above, both now fixed). The other
three functions in doc_claim_structural.py, plus doc_claim_scan.py/
doc_claim_sources.py/repo_badge_catalog.py (not in this run's scope),
likely have the same gap — filed as its own issue (#292) per §15.1 rather
than folded in here, since fixing it needs no change to this PR's own
logic.

Final scoped tally: 453 mutants, 361 killed, 87 not-covered (documented
above, tracked as #292), 5 survivors — all 5 pre-existing, in
canonical_licence/canonical_python_floor/_build_parser (unmodified by this
series), and already self-documented as equivalent by comments predating
this change (maxsplit=1 vs 0/2/-1 equivalence; default=None as argparse's
own implicit default) — re-verified, not re-asserted from scratch.

Tests: tests_py/scripts/test_check_doc_claims.py +
test_generate_repo_badges.py — 105 passed, 29 subtests passed (was 100/29
before this commit's 5 new direct tests + 1 dotted-load addition). ruff
check + ruff format --check clean. Both scripts re-verified as direct CLI
invocations against the real tree.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
cdeust added a commit that referenced this pull request Jul 30, 2026
…ions (issue #282) (#303)

* fix(mutmut): extract 30 @dataclass-with-methods classes to free functions (issue #282)

mutmut categorically skips the body of any @dataclass-decorated ClassDef
(mutmut/mutation/file_mutation.py:236), including @staticmethod members and
__post_init__ dunders, so logic placed on methods carries zero mutation
coverage regardless of test-loader naming. Issues #262/#280/#283 established
the fix (extract methods to free functions, keep the dataclass pure data) for
ConstraintSet, RepoBadge, and Ranking; this closes the systemic sweep across
the remaining 30 classes the issue's own AST scan found (26 files, spanning
mcp_server/core, mcp_server/handlers/consolidation, mcp_server/shared,
fuzz/, and benchmarks/lib/).

Every extracted function now attributes a real, scoped mutation tally
(0 attributed -> a number, matching the RepoBadge precedent) — verified via
per-file `mutmut run "<module>.x_<func>__mutmut_*"` isolation runs (a
26-file combined run gave unreliable results and was discarded; see the
per-file tallies below). Surviving mutants were killed by strengthening
assertions (precision-insensitive test fixtures, message-substring
assertions matching the padded/typo'd input, missing negative-case
coverage) or documented as equivalent at the use site.

Also found and fixed one real regression risk while sweeping: a
`getattr(goal, "is_active", False)` call site in recall_pipeline.py
silently defaulted to False once the `is_active` property was removed,
which would have disabled A3 goal-maintenance recall re-ranking with no
signal — grep for `.attr` patterns alone does not catch getattr() defaults.

Rebased onto a fast-moving origin/main mid-session (issue #276's
headless_authoring.py split moved CycleBudget to a new cycle_types.py);
the CycleBudget extraction was re-applied against the new file location.

Closes #282

* fix(mutmut): close 3 real survivors + 1 psycopg import bug in issue #282's sweep

Verified per-file mutant tallies for all 26 files/61 extracted functions
from the #282 sweep via isolated `mutmut run "<pattern>"` invocations (a
combined 26-file run gave unreliable "survived"/"not checked" counts —
discarded per this branch's own prior checkpoint).

Two methodology bugs found while producing trustworthy numbers, both
fixed at the source rather than worked around:

- tests_py/fuzz/test_corpus_replay.py imported replay_corpus.py via
  importlib.util.spec_from_file_location under the bare name
  "replay_corpus", not the package-qualified "fuzz.replay_corpus" mutmut's
  trampoline dispatch expects -- every mutant in that file was invisible
  to mutation testing regardless of what the test asserted. Switched to a
  normal `from fuzz import replay_corpus` import (behavior unchanged,
  confirmed by the existing tests still passing); mutants went from
  29/35 "not checked" to real killed/survived numbers.
- tests_py/test_doctor_mcp.py's launcher smoke test needs scripts/ on
  disk; mutation_scope_run.sh (scratchpad tooling, not shipped) now adds
  it to also_copy so the scoped run's baseline-stats phase doesn't fail
  before scoring a single mutant (same class of gap as issue #293).

Real surviving mutants found and killed with negative-case tests:
- harness_consume (fuzz/replay_corpus.py): missing-consume() and
  module-preregistration paths were untested; added two tests pinning
  the exact RuntimeError message and the sys.modules[spec.name] ordering
  the function's own docstring documents as load-bearing.
- replay() (same file, touched by this diff's call-site update): its
  AssertionError-wrapping path had no test that made consume() itself
  raise; added one.

Remaining survivors in backpressure_pipeline_run (6) and
exp_result_verdict (2) are the SAME equivalent mutants their source
comments already document (thread `name=` kwargs; `cmp.get(op, False)`'s
falsy default) -- verified by reading the actual mutant diffs, not
re-litigated.

Also fixes a real bug the sweep surfaced: benchmarks/lib/__init__.py
eagerly imported BenchmarkDB (-> psycopg/psycopg_pool/pgvector) at
package-init time, so importing the Postgres-independent
benchmarks.lib.verification_report (one of the 26 extracted files) on a
SQLite-only install raised ModuleNotFoundError before a single mutant
could run. Every other consumer in benchmarks/lib/ already defers this
import inside a function for exactly this reason; __init__.py did not
follow its own package's convention. Fixed via a PEP 562 module
__getattr__, with a subprocess-isolated regression test poisoning
sys.modules for psycopg/psycopg_pool/pgvector.

Full suite: 6864 passed, 5 skipped (was 6706 passed before this commit).

Closes #282

* fix(tests): skip the psycopg-present sanity check when Postgres extras are absent

test_benchmark_db_resolves_with_psycopg_present asserted the real
BenchmarkDB resolves via benchmarks.lib's lazy __getattr__, but assumed
psycopg is always importable in the test environment. CI's "Test
(SQLite backend)" job installs without the Postgres extras, so this
sanity check would fail there for the same reason BenchmarkDB is
lazy in the first place. Guarded with pytest.importorskip("psycopg")
-- the negative case (no psycopg -> loud ImportError, not silent) is
already pinned by test_benchmark_db_still_reachable_lazily_and_fails_
loudly_without_psycopg above, so this only skips the redundant
positive-case check on an install that cannot exercise it.

Full suite: 6868 passed, 5 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
cdeust added a commit that referenced this pull request Jul 30, 2026
…ge_catalog for real mutation coverage (#292)

check_doc_claims.py and generate_repo_badges.py bare-import their sibling
modules (doc_claim_scan, doc_claim_sources, doc_claim_structural,
repo_badge_catalog). A function's __module__ is fixed at definition time to
whatever name it was imported under, and mutmut's trampoline only activates
a mutant when that matches the dotted, path-derived name it generated the
mutant under -- so every mutant in these bare-imported siblings reported
"no tests" under a scoped run, even though the functions are exercised by
real passing tests through the bare-imported path (same defect class as
badge_render.py/doc_claim_structural.py's check_badge_floor, issues #262/
#280/#293).

Fix: dotted-load each sibling under its own "scripts.<name>" spec (mirroring
the existing check_badge_floor precedent) and add direct test classes that
call through those dotted references instead of through gate's/gen's
bare-imported copies. Exact assertEqual on every message string and dict
field (not assertIn) to kill the underlying string-literal mutants directly,
plus targeted continue-vs-break and split/rsplit/maxsplit tests for the
loop- and parsing-shaped mutants exact-match assertions alone don't reach.

Verified with a real tally, not the absence of an error: a scoped
mutmut run (scripts/mutation_check.sh) against all four production files --
doc_claim_structural.py, doc_claim_scan.py, doc_claim_sources.py,
repo_badge_catalog.py -- and both test files reports "0 surviving mutants"
(previously 62 genuine survivors once the "no tests" misattribution was
fixed, all now killed). Full suite green (6872 passed, 5 skipped, 121
subtests), doc-claims and badge-floor gates green, ruff clean.

Closes #292

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
cdeust added a commit that referenced this pull request Jul 30, 2026
Every prior fix in this defect family (badge_render/check_badge_floor
#262/#280, and #293 itself) has a matching CHANGELOG entry under
[Unreleased] ### Fixed; this PR's own commit lacked one. Boy-scout
(coding-standards.md §14): the gap was visible the moment the touched
diff was compared against the repo's own established convention for
this exact class of change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
cdeust added a commit that referenced this pull request Jul 30, 2026
…ining functions + repo_badge_catalog for real mutation coverage (#292)

check_doc_claims.py bare-imports its siblings doc_claim_scan and
doc_claim_structural; generate_repo_badges.py bare-imports
repo_badge_catalog. A function's __module__ is fixed at definition time
to whatever name it was imported under, and mutmut's trampoline only
activates a mutant when that matches the dotted, path-derived name it
generated the mutant under -- so every mutant in these bare-imported
siblings reported "no tests" under a scoped run, even though the
functions are exercised by real passing tests through the bare-imported
path (same defect class as badge_render.py/check_badge_floor, issues
#262/#280/#293, and doc_claim_sources.py's own instance of it, already
fixed on main by #304/issue #235).

Fix: dotted-load each sibling under its own "scripts.<name>" spec
(mirroring the existing check_badge_floor/doc_claim_sources precedent)
and add direct test classes that call through those dotted references
instead of through gate's/gen's bare-imported copies. Exact assertEqual
on every message string and dict field (not assertIn) to kill the
underlying string-literal mutants directly, plus targeted continue-vs-
break and split/rsplit/maxsplit tests for the loop- and parsing-shaped
mutants exact-match assertions alone don't reach.

Verified with a real tally, not the absence of an error: a scoped
mutmut run (scripts/mutation_check.sh) against doc_claim_structural.py,
doc_claim_scan.py, doc_claim_sources.py, repo_badge_catalog.py and both
test files reports "0 surviving mutants" (301 mutants, 301 killed, 0
"no tests", 0 survived). Full suite green (6919 passed, 5 skipped, 121
subtests), doc-claims and badge-floor gates green, ruff clean.

Closes #292

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
cdeust added a commit that referenced this pull request Jul 30, 2026
Every prior fix in this defect family (badge_render/check_badge_floor
#262/#280, doc_claim_sources #235, and #293 itself) has a matching
CHANGELOG entry under [Unreleased] ### Fixed; this fix's own commit
lacked one. Boy-scout (coding-standards.md §14): the gap was visible
the moment the touched diff was compared against the repo's own
established convention for this exact class of change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
cdeust added a commit that referenced this pull request Jul 30, 2026
…ge_catalog for real mutation coverage (#292) (#305)

* fix(mutmut): dotted-load doc_claim_scan + doc_claim_structural's remaining functions + repo_badge_catalog for real mutation coverage (#292)

check_doc_claims.py bare-imports its siblings doc_claim_scan and
doc_claim_structural; generate_repo_badges.py bare-imports
repo_badge_catalog. A function's __module__ is fixed at definition time
to whatever name it was imported under, and mutmut's trampoline only
activates a mutant when that matches the dotted, path-derived name it
generated the mutant under -- so every mutant in these bare-imported
siblings reported "no tests" under a scoped run, even though the
functions are exercised by real passing tests through the bare-imported
path (same defect class as badge_render.py/check_badge_floor, issues
#262/#280/#293, and doc_claim_sources.py's own instance of it, already
fixed on main by #304/issue #235).

Fix: dotted-load each sibling under its own "scripts.<name>" spec
(mirroring the existing check_badge_floor/doc_claim_sources precedent)
and add direct test classes that call through those dotted references
instead of through gate's/gen's bare-imported copies. Exact assertEqual
on every message string and dict field (not assertIn) to kill the
underlying string-literal mutants directly, plus targeted continue-vs-
break and split/rsplit/maxsplit tests for the loop- and parsing-shaped
mutants exact-match assertions alone don't reach.

Verified with a real tally, not the absence of an error: a scoped
mutmut run (scripts/mutation_check.sh) against doc_claim_structural.py,
doc_claim_scan.py, doc_claim_sources.py, repo_badge_catalog.py and both
test files reports "0 surviving mutants" (301 mutants, 301 killed, 0
"no tests", 0 survived). Full suite green (6919 passed, 5 skipped, 121
subtests), doc-claims and badge-floor gates green, ruff clean.

Closes #292

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u

* docs(changelog): add Unreleased entry for issue #292's mutmut fix

Every prior fix in this defect family (badge_render/check_badge_floor
#262/#280, doc_claim_sources #235, and #293 itself) has a matching
CHANGELOG entry under [Unreleased] ### Fixed; this fix's own commit
lacked one. Boy-scout (coding-standards.md §14): the gap was visible
the moment the touched diff was compared against the repo's own
established convention for this exact class of change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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