Skip to content

test(semantic): raise experts.py mutation kill rate 44.4% -> 63.2% - #492

Merged
kevincostner17 merged 1 commit into
mainfrom
test/experts-mutants
Sep 20, 2026
Merged

kevincostner17 merged 1 commit into
mainfrom
test/experts-mutants

Conversation

@kevincostner17

Copy link
Copy Markdown
Contributor

What

semantic/experts.py is the module where semantic repairs are proposed, and it killed only 111 of 250 mutants. This adds 60 tests across four themes.

Module Before After Survivors after
semantic/experts.py 44.4% (111/250) 63.2% (158/250) 92 (untargeted)
semantic/scoring.py 96.6% (56/58) 98.3% (57/58) 1, proven equivalent

Of the 37 targeted mutants: 32 killed, 5 proven equivalent, 0 unexplained.

All runs cover every mutation site with not applied: 0.

Why the survivors mattered

Expert applicability guards. if info.free_text or info.identifier_like or info.boolean_like: could become and, and info.numeric_like and not info.free_text could become or, with nothing failing. These guards are what keep an expert off identifier and free-text columns — the brief's "ID-protection removed" class. An expert that runs on an identifier column rewrites "007" to 7, and the damage is unrecoverable from the output alone.

Date day/month disambiguation. The a > 12 and b <= 12 family decides whether 05/12 is May 12th or 5th December. Every boundary at 12 was movable — a silent wrong date written into user data.

Currency and number parsing, including the ambiguity flag for "1,000" with no currency code — the corpus trap this parser exists to handle — and the decimal-separator selection.

Allowed values and the category tie-break, plus the frozen-ness of _DateResolution and dropna in the value counter.

The five equivalent mutants are proven, not asserted

  • cmp#7 — reachable only inside if dots and commas:, so both rfind calls return ≥ 0 and, being different characters, can never be equal; > and >= agree on every reachable input. Confirmed over 727,831 parser calls.
  • const_bool#5_valid_grouping line 134 is dead: an early return guarantees rest is non-empty. Confirmed by sys.settrace line coverage over 610,436 calls: zero executions.
  • cmp#20, cmp#21, bool#16 — the three branch-3 date mutants differ from the original only in states already claimed by branches 1 and 2, which return first. Confirmed by differential over the complete input domain (a, b ∈ 0..99, both separators, 3 years, 3 dayfirst values) = 180,000 inputs per mutant, zero differences.

This also corrects PR #491

#491 reported 98.3% for scoring.py. That figure was wrong. Its verdicts came from a harness run affected by a stale-bytecode defect, and the true value at that commit was 96.6%.

The masked survivor was json.dumps(features, sort_keys=True)False in features_hash — the property that makes the digest a function of feature content rather than of the order the dict literal happens to be written in. Every existing test compared hashes built in a single order, so none of them noticed. A test for it is included here, and scoring.py now genuinely measures 98.3%.

Scope

Tests only — no src/ change. No behaviour changes, so no changelog entry and no compatibility impact.

Verification

  • The four new files + test_scoring_boundaries.py — all pass
  • Full suite, py3.12 — 7309 passed, 22 skipped, 0 failed, coverage 95.01%
  • ruff check + ruff format --check clean
  • Mutation re-runs as tabled above

Two defects found in the library (reported, not fixed here)

Neither is fixed in this PR, because both touch src/ and warrant their own change with its own regression test.

1. The currency parser's ambiguous flag is dead on every path. experts.py:110-114 documents that "a currency absent from it and not settled by structure is reported ambiguous rather than guessed". It cannot happen: _split_amount's only caller always resolves a currency code first, so the ambiguous branch is unreachable (verified: 0 of 98 public inputs), and parse_currency — the entry point all six consumers use — discards the flag anyway. No recognised currency produces a wrong value today, since all eight codes outside {EUR, CHF} are dot-decimal locales. The hazard is latent: adding SEK/BRL/DKK to _CURRENCY_CODES without also adding it to _COMMA_DECIMAL_CURRENCIES would read SEK 1.200 as 1.2 instead of 1200, and the documented safety net would not catch it.

2. The category tie-break is decided by row order — a metamorphic violation. On an exact tie, count > best[1] keeps whichever spelling value_counts saw first, which is a property of row ordering, not of the data:

input order proposal at defaults data at semantic_auto_threshold=0.90
["USA"]*3 + ["usa"]*3 'usa' → 'USA' all six become USA
["usa"]*3 + ["USA"]*3 'USA' → 'usa' all six become usa

At defaults the data is unchanged (confidence 0.93 < 0.95) but the proposal direction still flips, so the report is order-dependent even at defaults. semantic_auto_threshold is a documented public kwarg, so the data flip needs no private API. An exact tie means there is no column-local evidence for either spelling, so the contract says route to review rather than pick one. The tie-break tests in this PR are characterization tests whose docstrings say plainly that they pin a suspected defect, so a deliberate fix has to come past them.

`semantic/experts.py` is where semantic repairs are proposed, and it killed
only 111 of 250 mutants. This adds 60 tests across four themes, killing 32
of 37 targeted mutants; the other 5 are proven equivalent with executable
proofs, not assertions.

Expert applicability guards. `info.free_text or info.identifier_like or
info.boolean_like` could become `and`, and `numeric_like and not free_text`
could become `or`, with nothing failing. These are the guards that keep an
expert off identifier and free-text columns, so flipping them is the
"ID-protection removed" class: an expert that runs on an identifier column
rewrites "007" to 7, and the damage is unrecoverable from the output alone.

Date day/month disambiguation. The `a > 12 and b <= 12` family decides
whether 05/12 is May 12th or 5th December. Every boundary at 12 was
movable. Three of the branch-3 mutants cannot be killed: they differ only
in states already claimed by the earlier branches, proven by differential
over the complete input domain (180,000 inputs each, zero differences).

Currency and number parsing, including the ambiguity flag for "1,000" with
no currency code -- the corpus trap this parser exists to handle. Two
mutants here are equivalent: one comparison is unreachable outside a branch
that guarantees both operands differ, and one `return True` is dead code,
confirmed by line-level trace over 610,436 calls recording zero executions.

Allowed values and the category tie-break, plus the frozen-ness of
_DateResolution and dropna in the value counter.

Also corrects PR #491. That PR reported 98.3% for scoring.py, but its
verdicts came from a harness run with a stale-bytecode defect; the true
figure at that commit was 96.6%. The masked survivor was `sort_keys=True`
in features_hash -- the property that makes the digest a function of
feature content rather than of dict literal order. A test for it is
included here, and scoring.py now genuinely measures 98.3%.

Tests only; no src change.
@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 516b00c8-dd8f-48af-9d24-1bf4bafdbe34


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kevincostner17
kevincostner17 merged commit d74e6cd into main Sep 20, 2026
19 checks passed
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