Repository navigation
Conversation
…0 lines `JsonWalk` carries two copies of `idx < 0 || idx >= arr.length`: one in `readPath` (:56), one in `modifyPath` (:81). `JsonIndexBoundsSpec` was written for :56 but drove only `.modify(...)`, so all six operator mutants on the read copy survived while the write copy was 11/11 killed. Replace the hard-coded example with one `forAll(Gen.chooseNum(-2, 5))` property over a fixed size-3 array whose expectations are DERIVED from the backing `Vector`, driving three surfaces per index: `get` (read → :56), `modify` (Ior write → :81) and `modifyUnsafe` (silent pass-through). The range straddles all five discriminating classes — i<0, i=0, 0<i<3, i=3, i>3 — which is what the two `>=` variants need (i=3 alone kills `>=`→`>`, i>=4 alone kills `>=`→`==`). Deletions: the subsumed `unsafeOOR` / `defaultOOR` / `negIndex` assertions in JsonPrismSpec. `defaultOOR` was already redundant with JsonFailureSpec:66-71; the other two are subsumed by the property's `silent` conjunct, which asserts pass-through across every out-of-range class rather than two constants. Measured: circe 38K/12S → 44K/6S (76.00% → 88.00%), net -10 test lines, suite test count unchanged. The remaining 6 survivors are provably equivalent mutants (`i >= n` → `i == n` on two `+ 1`-incremented loop heads, and four empty-path shortcuts whose general branch computes the same value), so circe is now at its killable ceiling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V
Contributor
Author
|
Superseded by #111, which cherry-picks this branch's commits unchanged and adds the core artifacts, a consolidation fold and the QA-page equivalent-mutant documentation. Close this one unmerged rather than merging both — #111 is based on the same |
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
One artifact, produced by the
improve-test-leverageskill (phase 4 Execute + phase 5 Eval) against the freshly measured circe survivor set.JsonWalkcarries two copies of the array bounds checkidx < 0 || idx >= arr.length— one inreadPath(JsonWalk.scala:56), one inmodifyPath(:81).JsonIndexBoundsSpecwas written for the:56copy (its comment says so) but exercised only.at(i).modify(identity)(...), which routes throughmodifyPath. Result::81was 11/11 killed while all six operator mutants on:56survived.The fix is an oracle replacement, not an addition. The hard-coded example becomes one
forAll(Gen.chooseNum(-2, 5))property over a fixed size-3 array, whose expected values are derived from the backingVectorrather than written down, and which drives three surfaces per index:p.get(basket)JsonWalk.readPath:56— the uncovered copyp.modify(identity)(basket)JsonWalk.modifyPath:81— preserves today's killsp.modifyUnsafe(identity)(basket).getOrElse(json)pass-throughWhy the range is
-2 .. 5and the array size is fixedThe five discriminating classes against a size-3 array are
i<0,i=0,0<i<3,i=3(exactly length),i>3(strictly past length). Each matters to a specific mutant:i = 0alone kills both<variants (<→<=,<→==) — they turn a legitimate Hit at index 0 into a Miss. The pre-existing read coverage inJsonPrismSpecreads at index 1, where1 <= 0and1 < 0agree.i = 3exactly kills>=→>;i >= 4does not (4 > 3fires correctly).i >= 4only kills>=→==;i = 3does not (3 == 3fires correctly).A generated array size was considered and rejected: size 0 discriminates nothing extra (at size 0 with i=0,
<=and==agree with the original), while a second generator dilutes each critical(size, i)cell to ~1/24 per trial and makes thei=3/i=4kills flaky. With a fixed size, each class lands with p = 1/8 per trial, so over ScalaCheck's default 100 trials the miss probability for any one class is < 2e-6.Deletions (part of the artifact, not a favour)
JsonIndexBoundsSpec— the"indices 0 and 1 read their elements; -1 and length fail IndexOutOfRange"example, replaced in place. Its three assertions are a strict subset of the property's index classes.JsonPrismSpec—basket1/json1plus theunsafeOOR,defaultOORandnegIndexassertions and theircovers:lines.defaultOORwas already redundant withJsonFailureSpec.scala:66-71, which asserts the sameIor.Bothand the same exactIndexOutOfRange(PathStep.Index(5), 1)on the same modify surface.unsafeOOR/negIndexare subsumed by the property'ssilentconjunct, which asserts pass-through across every out-of-range class rather than two constants.Measured (not predicted)
sbt circeIntegration/test— 42 passed, 0 failed. Stryker re-run diffed against the baseline by the full mutant key(file, line, column, mutator, replacement):Exactly the six predicted mutants flipped
Survived → Killed, with no regressions and no new survivors:Two distinct failure modes are exercised, both legitimate kills: a value mismatch for the
:24pair, and an escapingIndexOutOfBoundsExceptionfromVector.applyfor:20/:28/:35— sound specifically becausereadPathhas notry/catch, unlikemodifyPath. That asymmetry is exactly why:56survived while:81died.Objective terms:
net_test_lines_added= −10 (scalafmt-arbitrated, comments excluded)suite_test_countdelta = 0 (one example replaced by one property; theJsonPrismSpecremovals are assertions inside an existing block, not whole tests)suite_kill_densityrises on both terms: +6 detected mutants over 10 fewer test LOCcirce is now at its killable ceiling
The 6 remaining survivors are provably equivalent mutants and should be dropped from future sweep action lists:
JsonWalk.scala:42:12and:67:12—i >= n→i == non thereadPath/modifyPathloop heads. Both loops are entered at 0 and the only recursive calls arei + 1from inside thei < path.lengtharm, soican never exceedpath.length. This is verbatim the equivalent pattern the skill's triage table lists.JsonFocus.scala:77:10,:100:10,:114:10,:135:10—path.length == 0→falseon the four empty-path shortcuts inLeaf. The general branch with an empty path computes the identical value in every case (readPathreturnsRight(json)ati = 0;modifyPathapplies the terminal function ati = 0and cannot miss), differing only by one stack frame and one closure allocation.Gates
sbt circeIntegration/test✅sbt "scalafmtCheckAll; scalafixAll --check"✅🤖 Generated with Claude Code
https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V