Skip to content

test(scripts): kill check_doc_claims.py's 25 surviving mutants for real (#235) - #304

Merged
cdeust merged 1 commit into
mainfrom
fix-doc-claims-survivors-235
Jul 30, 2026
Merged

test(scripts): kill check_doc_claims.py's 25 surviving mutants for real (#235)#304
cdeust merged 1 commit into
mainfrom
fix-doc-claims-survivors-235

Conversation

@cdeust

@cdeust cdeust commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #235.

Issue #235's originating mutation run found 25 survivors in
check_doc_claims.py's canonical-source helpers (canonical_reference_count
10, canonical_tool_counts 7, canonical_version 3, canonical_mechanism_count
3, read 2). Before it could be fixed, #294 merged and split the file into
check_doc_claims.py / doc_claim_sources.py / doc_claim_scan.py /
doc_claim_structural.py — moving all four canonical_* helpers into
doc_claim_sources.py.

Re-measuring against current main first (per this issue's own instruction):
a fresh scoped mutmut run reports 0 survived — but that number is
misleading, not a genuine pass. check_doc_claims.py reaches
doc_claim_sources.py through a bare import doc_claim_sources, which gives
every function in that file __module__ == "doc_claim_sources" — never the
dotted "scripts.doc_claim_sources" mutmut's trampoline expects. This is the
exact bare-import attribution defect issue #292 already tracks (and had
flagged doc_claim_sources.py as "not yet verified, assumed affected"). All
108 of doc_claim_sources.py's mutants were showing "no tests" — an
uncalled trampoline, not a verified kill — so #235's specific acceptance
criterion ("0 surviving non-equivalent mutants ... in the canonical-source
helpers") was unverifiable on main, not actually satisfied. Closing the
issue on the strength of "0 survived" alone would have been trusting a
measurement the tooling could not make.

Fix

tests_py/scripts/test_check_doc_claims.py now loads doc_claim_sources.py a
second time under its own dotted name ("scripts.doc_claim_sources") — the
same pattern already established in this file for
doc_claim_structural.check_badge_floor — and a new
CanonicalSourceDirectTests class calls through that reference with an
explicit read_fn (these functions already take one; no gate.read
monkeypatch needed).

Re-running the scoped mutation check after adding it: all 108 previously
invisible mutants become reachable, and every one is killed. Final:
260 killed / 125 no-tests / 0 survived across the four files (up from
152/233/0). The remaining 125 "no tests" are pre-existing, already-filed scope
doc_claim_scan.py's functions, doc_claim_structural.py's other functions
(check_badge, check_no_hotlinked_badges, check_no_conflict_markers,
check_scanned_json_parses), and check_doc_claims.py's main() — all named
in issue #292's "Remaining scope", untouched by this PR (no new issue opened;
#292 already covers it).

Several of the newly-reachable mutants needed an exact-message assertion
where the existing behavioural tests only checked a substring (assertIn) or
just the return value — missing-sentence, arithmetic-mismatch,
missing-pinned-test, missing-section, no-entries-found,
undeclared-mechanism-count, missing-version-declaration. These are the same
gap class #235's own table named (e.g. canonical_version()'s "not found" arm
had no test at all). No equivalent mutants were needed — every real gap was
killable once the message was pinned.

Acceptance criteria (from #235)

  • 0 surviving non-equivalent mutants in scripts/check_doc_claims.py
    (+ its extracted siblings doc_claim_sources.py/doc_claim_scan.py/
    doc_claim_structural.py, which now carry the logic check_doc_claims.py: 25 surviving mutants in the canonical-source helpers #235's table
    named) — verified, not just measured: 0 survived, and the 108
    previously-unattributed mutants are now demonstrably reachable and
    killed, not silently uncounted.
  • Any mutant judged equivalent is documented with a written rationale —
    N/A, no equivalent mutants remained after fixing attribution; all real
    gaps were killed outright.
  • canonical_reference_count's entry filter is pinned against a
    bibliography containing headings, separators and blank lines — already
    covered by test_reference_entries_exclude_headings_and_separators
    (headings ###, separator ---, and blank lines between entries all
    present in that fixture), now also exercised directly (mutation-level,
    not just behaviourally) via CanonicalSourceDirectTests.

Completion Ledger

Path introduced Test asserting it
Dotted-load of doc_claim_sources.py under "scripts.doc_claim_sources" Module-level fixture; exercised by every test in CanonicalSourceDirectTests
canonical_tool_counts(): catalogue read + happy path (direct) CanonicalSourceDirectTests.test_tool_counts_come_from_the_catalogue
canonical_tool_counts(): missing header sentence, exact message test_missing_standalone_total_sentence_is_an_error
canonical_tool_counts(): catalogue/pinned-test drift test_catalogue_drifting_from_the_pinned_registry_test_is_an_error
canonical_tool_counts(): arithmetic mismatch, exact message test_inconsistent_arithmetic_in_the_catalogue_is_an_error
canonical_tool_counts(): missing pinned test, exact message test_missing_pinned_registry_test_is_an_error
canonical_reference_count(): happy path (direct) test_reference_count_is_the_number_of_entries_not_the_advertised_number
canonical_reference_count(): headings/separators excluded (direct) test_reference_entries_exclude_headings_and_separators
canonical_reference_count(): split-first-not-last semantics test_the_first_references_heading_is_the_split_point
canonical_reference_count(): missing section, exact message test_bibliography_without_a_references_section_is_an_error
canonical_reference_count(): no entries found, exact message test_a_references_section_with_no_entries_is_an_error
canonical_mechanism_count(): happy path (direct) test_mechanism_count_is_read_from_the_bibliography_header
canonical_mechanism_count(): undeclared count, exact message test_undeclared_mechanism_count_is_an_error
canonical_version(): happy path (direct) test_version_comes_from_pyproject
canonical_version(): missing declaration, exact message test_missing_version_declaration_is_an_error

Test plan

  • pytest tests_py/scripts/test_check_doc_claims.py — 70 passed, 9 subtests
  • pytest tests_py/scripts/ (full sibling scope) — 458 passed, 116 subtests
  • Full suite: 6835 passed, 5 skipped, 0 failed
  • ruff check + ruff format --check on the touched file — clean
  • python scripts/check_doc_claims.py --test-count 6840doc claims OK (1 declared not-a-claim exemption(s))
  • python scripts/generate_repo_badges.py --check --test-count 6840badges OK (5 checked) (the tests badge is a monotone floor — 6788 <= 6840 — so no regeneration is needed for a tests-only PR)
  • pyrightconfig.json excludes tests_py/scripts from the zero-diagnostic gate — N/A for this change
  • No conflict markers introduced; .bestpractices.json still parses
  • Scoped mutmut re-run (tests_py/scripts/test_check_doc_claims.py against scripts/check_doc_claims.py + doc_claim_sources.py + doc_claim_scan.py + doc_claim_structural.py): 260 killed / 125 no-tests (pre-existing mutmut: sibling-imported scripts/ modules show every mutant as 'no tests' (bare-import __module__ mismatch) #292 scope) / 0 survived, 0 unexplained
  • pyproject.toml's committed [tool.mutmut] scope (mcp_server/shared/json_native.py) untouched — every scoped run restored it via scripts/mutation_check.sh's own trap/cleanup

AST size gate (coding-standards.md §4, this repo's 300/40 tightening)

$ python3 - tests_py/scripts/test_check_doc_claims.py <<'PY'
import ast, pathlib, sys
for f in sys.argv[1:]:
    src = pathlib.Path(f).read_text(); n = len(src.splitlines())
    if n > 300: print(f"FILE OVER CAP: {f} = {n}")
    for node in ast.walk(ast.parse(src)):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            ln = (node.end_lineno or node.lineno) - node.lineno + 1
            if ln > 40: print(f"FN OVER CAP: {f}::{node.name} = {ln}")
PY
FILE OVER CAP: tests_py/scripts/test_check_doc_claims.py = 920

File-level cap: this test file was already at 721 lines before this PR (organized
by one unittest.TestCase class per function/concern — the repo's own
test_generate_pip_constraints.py (944), test_launcher_deps.py (1500), and
test_generate_repo_badges.py (613) are the same, pre-existing pattern for
this test tree). No new method is over the 40-line cap (the AST walk above
reports zero FN OVER CAP lines).

Boy-scout items (coding-standards.md §14)

None seen beyond this issue's own subject in the touched file. The
doc_claim_scan.py/doc_claim_structural.py/main() attribution gaps this
PR's investigation surfaced are pre-existing and already tracked by issue
#292 (filed, with acceptance criteria) — left untouched per §14.3 (outside
this change's blast radius; #235's acceptance criteria name only the
canonical-source helpers, not the whole file's mutation coverage).

🤖 Generated with Claude Code

https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u

…al (#235)

Issue #235's originating mutation run found 25 survivors in check_doc_claims.py's
canonical-source helpers. Before this could be fixed, #294 split the file into
check_doc_claims.py/doc_claim_sources.py/doc_claim_scan.py/doc_claim_structural.py,
moving canonical_reference_count/canonical_tool_counts/canonical_version/
canonical_mechanism_count into doc_claim_sources.py.

A fresh scoped mutmut run on current main reports 0 survived — but that number
was misleading: check_doc_claims.py reaches doc_claim_sources.py through a bare
`import doc_claim_sources`, which gives every function in it
`__module__ == "doc_claim_sources"`, never the dotted
`"scripts.doc_claim_sources"` mutmut's trampoline expects (the same bare-import
attribution defect issue #292 tracks). All 108 of doc_claim_sources.py's mutants
were reported "no tests" — an uncalled trampoline, not a verified kill — so
#235's specific acceptance criterion ("0 surviving non-equivalent mutants ...
in the canonical-source helpers") was unverifiable on main, not satisfied.

Fixed by loading doc_claim_sources.py a second time under its own dotted name
(the same pattern already used for doc_claim_structural.check_badge_floor) and
adding CanonicalSourceDirectTests, which calls through that reference with an
explicit read_fn. Re-running the scoped mutation check: all 108 previously
invisible mutants are now reachable, and mutmut kills every one of them
(260 killed / 125 no-tests / 0 survived across the four files, up from
152/233/0). The remaining 125 "no tests" are pre-existing, already-filed scope
(doc_claim_scan.py + doc_claim_structural.py's other functions + main() itself
— issue #292), untouched here.

Several of the newly-reachable mutants needed an exact-message assertion
(missing-sentence, arithmetic-mismatch, missing-pinned-test, missing-section,
no-entries, undeclared-mechanism-count, missing-version) where the existing
`assertIn`/behavioural tests only checked a substring or the return value —
the same gap class #235's table originally named.

Verified:
- pytest tests_py/scripts/test_check_doc_claims.py: 70 passed, 9 subtests
- pytest tests_py/scripts/ (sibling scope): 458 passed, 116 subtests
- Full suite: 6835 passed, 5 skipped, 116 subtests passed
- ruff check / ruff format --check on the touched file: clean
- scripts/check_doc_claims.py --test-count 6840: doc claims OK
- scripts/generate_repo_badges.py --check --test-count 6840: badges OK
  (assets/badge-tests.svg's floor of 6788 <= 6840 holds; no regeneration needed)
- Scoped mutmut re-run (tests_py/scripts/test_check_doc_claims.py against
  scripts/check_doc_claims.py + doc_claim_sources.py + doc_claim_scan.py +
  doc_claim_structural.py): 260 killed / 125 no-tests (pre-existing #292
  scope) / 0 survived, 0 unexplained

Closes #235

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
@cdeust
cdeust merged commit 1bef0f5 into main Jul 30, 2026
19 checks passed
@cdeust
cdeust deleted the fix-doc-claims-survivors-235 branch July 30, 2026 18:17
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
…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.

check_doc_claims.py: 25 surviving mutants in the canonical-source helpers

1 participant