Context
Surfaced while fixing the unbounded AST recursion in the extractor layer (branch fix/ast-walk-type-recursion-depth). A scoped mutation run over the five extractor modules shows the language walkers are largely unpinned by tests.
Measured 2026-08-06, same tool selection both sides:
scripts/mutation_check.sh \
"tests_py/core/test_ast_extractors.py tests_py/core/test_ast_parser_extra_langs.py tests_py/core/test_ast_extractor_depth.py" \
mcp_server/core/ast_extractors.py mcp_server/core/ast_extractors_jvm.py \
mcp_server/core/ast_extractors_clike.py mcp_server/core/ast_extractors_scripting.py \
mcp_server/core/ast_extractors_extra.py
|
mutants |
killed |
no tests |
survived |
| HEAD (before the recursion fix) |
1554 |
475 |
851 |
228 |
| after the recursion fix |
1486 |
571 |
492 |
423 |
The survived column rising while no tests falls is reclassification, not regression: the new depth tests execute code that no test reached before, so mutants moved from "never run" into "run but not detected". The honest metric is not-killed = survived + no tests, and it improved in every walker:
| function |
HEAD not-killed |
after |
_walk_for_calls |
40 |
13 |
_walk_java |
59 |
37 |
_walk_kotlin |
77 |
54 |
_walk_csharp |
59 |
36 |
_walk_ruby |
53 |
33 |
_walk_php |
61 |
51 |
_extract_swift_node |
19 |
17 |
_walk_type |
0 |
0 |
| total |
368 |
241 |
The gap
241 mutants remain unpinned in those seven walkers, and 423 across the five modules in total. Concretely, the Java, Kotlin, C#, Ruby, PHP and Swift definition extractors have almost no behavioural tests: tests_py/core/test_ast_extractors.py covers Python, JS and Go only, and test_ast_parser_extra_langs.py is thin.
This is pre-existing debt, not introduced by the recursion fix, which is why it was not treated as a blocker on that PR (rules/coding-standards.md §12.5: the per-commit tier does not punish a change for a pre-existing gap). It is filed here rather than left as prose, per §14.3.
Notable non-walker survivors in the same run, same cause: _extract_rust_node 51, extract_go_definitions 34, _extract_js_node 20, _extract_python_class 16, _extract_python_func 15, _collect_call_basenames 14.
Why it matters beyond the score
These extractors decide what enters the code graph. A silent behaviour change in one of them (a wrong qualified name, a dropped nested class, a method attributed to the wrong scope) degrades every downstream consumer — ingest_codebase, wiki reference pages, unified_search — with no failing test and no visible signal. The recursion fix had to lean on a purpose-built differential harness against the old implementation over 2229 sources precisely because the test suite could not have caught an ordering regression.
Acceptance criteria
- Behavioural tests for the six language extractors covering, per language: top-level function, class/type with methods, nested type, unnamed/anonymous construct, and the qualified-name shape (
Type.method).
- Not-killed count for the seven walkers is 0, or each remaining mutant is documented as equivalent with a written rationale at the use site (§12.1).
- The same scoped
mutation_check.sh invocation above is the verification command, and its output is quoted in the PR.
- Consider promoting these modules into
memory/critical-zones.conf so the sweep tracks them (§12.5).
Note on the tooling
scripts/mutation_check.sh reported none — 0 surviving mutants 🎉 on this run while its own progress counter ended at 🙁 423. The survivor list from mutmut results is 915 lines and non-empty. The script's grep-based survivor detection therefore produced a false green here; the cleanup trap deletes mutants/ and .mutmut-cache on exit, so the discrepancy cannot be investigated after the fact. Worth a separate look — a mutation gate that can report a false clean is worse than no gate.
Context
Surfaced while fixing the unbounded AST recursion in the extractor layer (branch
fix/ast-walk-type-recursion-depth). A scoped mutation run over the five extractor modules shows the language walkers are largely unpinned by tests.Measured 2026-08-06, same tool selection both sides:
The
survivedcolumn rising whileno testsfalls is reclassification, not regression: the new depth tests execute code that no test reached before, so mutants moved from "never run" into "run but not detected". The honest metric is not-killed = survived + no tests, and it improved in every walker:_walk_for_calls_walk_java_walk_kotlin_walk_csharp_walk_ruby_walk_php_extract_swift_node_walk_typeThe gap
241 mutants remain unpinned in those seven walkers, and 423 across the five modules in total. Concretely, the Java, Kotlin, C#, Ruby, PHP and Swift definition extractors have almost no behavioural tests:
tests_py/core/test_ast_extractors.pycovers Python, JS and Go only, andtest_ast_parser_extra_langs.pyis thin.This is pre-existing debt, not introduced by the recursion fix, which is why it was not treated as a blocker on that PR (rules/coding-standards.md §12.5: the per-commit tier does not punish a change for a pre-existing gap). It is filed here rather than left as prose, per §14.3.
Notable non-walker survivors in the same run, same cause:
_extract_rust_node51,extract_go_definitions34,_extract_js_node20,_extract_python_class16,_extract_python_func15,_collect_call_basenames14.Why it matters beyond the score
These extractors decide what enters the code graph. A silent behaviour change in one of them (a wrong qualified name, a dropped nested class, a method attributed to the wrong scope) degrades every downstream consumer —
ingest_codebase, wiki reference pages,unified_search— with no failing test and no visible signal. The recursion fix had to lean on a purpose-built differential harness against the old implementation over 2229 sources precisely because the test suite could not have caught an ordering regression.Acceptance criteria
Type.method).mutation_check.shinvocation above is the verification command, and its output is quoted in the PR.memory/critical-zones.confso the sweep tracks them (§12.5).Note on the tooling
scripts/mutation_check.shreportednone — 0 surviving mutants 🎉on this run while its own progress counter ended at🙁 423. The survivor list frommutmut resultsis 915 lines and non-empty. The script's grep-based survivor detection therefore produced a false green here; the cleanup trap deletesmutants/and.mutmut-cacheon exit, so the discrepancy cannot be investigated after the fact. Worth a separate look — a mutation gate that can report a false clean is worse than no gate.