Skip to content

chore: add kimera for mutation testing - #3

Merged
giacope merged 13 commits into
mainfrom
claude/kimera-mutation-testing-ep6q15
Sep 26, 2026
Merged

giacope merged 13 commits into
mainfrom
claude/kimera-mutation-testing-ep6q15

Conversation

@giacope

@giacope giacope commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Adds kimera to the test group with the config kimera init generates,
and makes the unreadable-file spec independent of the user running it:
it stubbed permissions with chmod 000, which root reads anyway.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF

Adds kimera to the test group with the config `kimera init` generates,
and makes the unreadable-file spec independent of the user running it:
it stubbed permissions with chmod 000, which root reads anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
…tion gaps

Two real bugs surfaced by surviving mutants:

- feature_envy counted an explicit `self.x` receiver twice (once for the
  SelfNode, once for the call), inflating the method's self references so
  genuine envy was hidden: `self.rate * order.net + order.tax` stayed quiet
  while the `@rate` spelling was flagged.
- assumed_state resolved `class Thing < Thing` (inside a namespace) to the
  class itself, so the real parent's assignments were never seen and ivars
  set there were reported as assumed. A type is never its own ancestor.

Also drops repeated_call's size > 1 group filter, which Branches#together?
already implies (a single call has no pair), and adds specs for untested
behaviour: underscore caches read without ||=, resolved superclasses,
mixin cycles, else branches and nested defs in control_parameter,
condition/ensure repeats in repeated_call, literal-built, rescued,
guarded-elsewhere and table-indexed names in feature_envy, constant-defined
error classes, non-hash constants, and `private :name` leaving later
methods public. Two vacuous specs (literal builds, constructor calls)
now actually exercise the rule they name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
Specs for behaviour the suite never pinned:
- anchor: deep compact paths hoist to a top-level root; single-segment
  definitions nest in the current scope
- boundary errors name the root, or "(unknown)" when it is missing
- usage credits a nested constant or unknown member to its holding type
- metrics order: stable to unstable, unconnected packages last
- a closed class body leaves the lexical scope (no stale nesting)
- every distinct cycle is reported; the lightest edge is found anywhere
  on the cycle
- namespace packaging skips wrappers above a deep prefix, even ones
  with methods
- Pipeline: verify checks boundaries without smells; structural findings
  only with coupling; hotspots only with complexity or duplication
- Trees: unparsed works before any tree is read; unreadable paths carry
  the system's reason (also covers the rescue without chmod, so root runs
  keep 100% coverage)

Dead code: Audiences#common's empty guard (an empty shared part is
dropped by the MIN filter anyway) and Census#refold's empty-map check
(re-tallying through an empty fold map is identical, and Roster is lazy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
…p withheld

The README promises that every capped list "says how many rows it withheld",
but only the package table and the findings list did. The methods table, the
per-class rollup and the hotspot rollup cut at 10 rows (or --top N) in silence,
so a reader had no way to know the list was truncated. They now print the same
"… and N more — raise the cap with --top, or read them all with --json" line,
worded once in Phrases.withheld.

Mutation triage for cli/, ci/ and report/ (kimera: 56 survivors/uncovered -> 0
survivors), the rest being test gaps and dead code:

- Baseline: drop keyed(); Array#to_h with a two-parameter block already reads
  a pre-magnitude list of keys as key => nil.
- Only.parse: drop the empty-string guard; "".split(",") is already [].
- Specs pin what the mutants erased: every smell message names its method,
  complexity sites and duplication advice, the ratchet refusals name the
  baseline path and packaging, a focused ratchet fails on a new finding without
  calling the rest resolved, no empty traces section, the crash notice's
  message and backtrace, terminal progress/churn/timing notices end to end, the
  help rows, the unwritable-baseline reason, a cell exactly at the clip cap,
  the leaf boundary and exact-fit metrics table, pretty/compact JSON, accepted
  and per-class JSON, hotspots and fold spacing in the text report.
- The unreadable-file spec used chmod 000, which root reads anyway; a dangling
  symlink fails for every user, so the suite is green (and 100% covered) in a
  root container too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
A client that references only a nested constant (Core::Walk::LIMIT) is
counted under its holder (Core::Walk), but mentions? matched only
evidence lines ending in the holder, so that audience printed no
evidence. Match the holder at any segment boundary instead.

Also restructures Refs#record: with the double-counted self branch gone,
Refs#named referred only to its argument and hashira's own ratchet
flagged it as feature envy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
…tation-testing-ep6q15

# Conflicts:
#	spec/cli/cli_spec.rb
… LCS in linear time

Mutation triage of complexity/, duplication/ and hotspots/ turned up two
real bugs and a set of test gaps.

Exact clones went missing. When one copy of a duplicated stretch ran even
one statement longer, union-find merged the exact pair with the longer
near-miss windows. Grouping kept only the largest non-overlapping site per
file, so the cluster became a near-miss that the 40-node floor rejected. The
fallback core was then drawn from those surviving sites, where the exact pair
no longer existed, and the finding disappeared. That is the burial the 0.3
fix was meant to prevent. The fallback now regroups the whole union group by
shape, so every exact class gets weighed on its own floor. It also stops two
unrelated clones that sit side by side in one method from being cut down to a
shorter shared window.

The LCS pre-check was neither linear nor a multiset bound. overlap re-tallied
the right-hand sequence for every token, so it cost O(n*m) and never
decremented. A lopsided pair therefore passed the bound and paid for the
quadratic comparison anyway. It now tallies once. Similarity#ratio also
drops a guard that only mattered when both sides are empty; normalized
returns 0.0 for a zero length instead.

New specs pin the cognitive-complexity rules: nesting inside
loops/case/else/rescue, an if's predicate, a ternary's operands,
begin/else/ensure bodies, the threshold at exactly 10, the finding's site and
dominant-label advice, and calls read before the score. They also pin the
duplication boundaries: the mass floor, the near-miss mass ratio, the
overlap/comparable guard, single-site and shared-statement clusters, string
literals, constant-receiver macros, 3-site :structure, the digest,
rare-type indexing, and the canonical shape. For hotspots they cover
Cluster#masses and FileCost#to_h.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
UnlessNode went through the generic nester, which descended the else
clause without its +1 and scored the condition one level deeper than an
if's. Route it through IfChain so both read the same.

Also records the fixes found by mutation testing in the CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
feature_envy already read TABLE[x.class] as dispatch through a table keyed
by foreign classes; fetch is the same lookup and now counts too. The key is
the first argument, so fetch(x.class, default) and fetch(x.class) { } work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
Cluster#masses charged every site the canonical copy's mass, so in a
near-miss cluster a longer copy was under-charged and a shorter one
over-charged in the hotspot Dup column. The README already said each
file pays for the clone sites it holds; charge each site its own mass
and say so explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MJdrcPfvw5DYY31WE1XJF
@giacope
giacope merged commit 3071134 into main Sep 26, 2026
3 checks passed
@giacope
giacope deleted the claude/kimera-mutation-testing-ep6q15 branch September 26, 2026 13:09
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.

2 participants