All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.3.1 - 2026-09-19
Fixed
- Whole-call mutants on a pipe stage preserve a syntax-valued left operand. Routed
declarations, patterns, keyword fragments and interpolations reach the macro directly,
instead of being evaluated through a closure. This lets adapters mutate stages such as
(p in Post) |> from(order_by: …)without poisoning the single build. Mutations inside the left operand remain reachable without duplicating their selectors. - A generated interpolation on a pipe's left keeps its precedence when rendered, including when only the left operand carries mutations.
0.3.0 - 2026-09-19
Added
--verify-invariants(verify_invariants:) checks each transformed file before the run relies on it. Mutare reads the metamutant back and emits the file a second time. It aborts withMutare.InvariantErrorwhen a recorded mutant has no branch that runs while it alone is active, when a mutant has no coverage record, when generated code names an id that no mutant records, when a mutant renders identically to its original code, or when the second pass differs. These problems would otherwise distort the report silently. They come from custom mutators, hosts, and extensions (for example, a host splice that overwrites selectors core has already placed). The checks add roughly a sixth to scan time and are off by default.Mutare.Manifestlists every generated mention of a mutant id (:mentions).- A routed call written as a pipe stage is shown the pipe's left side.
Mutare.CallRouting.Callgainspipe_left—:unpiped, or{:piped, left}with the left side as written. The piped position is the call's effective first argument, and adapters used to route it without seeing it; aroute_arguments/2classifier can now route it by shape (Post |> from(…)held back from thealiasfamily,build(x) |> from(…)left mutable), and a host or mutator can read a declaration written there. It is the same value inroute_arguments/2, inhost/2, and fromMutare.Calls.resolved_routed_call/1. It is read-only, so the piped position still cannot be routed:hosted. Mutare.CallRouting.Call.new/5builds a call value from its independent facts and derivesarguments,pipe_mode, andeffective_arity.
Changed
- Breaking: a hand-built
%Mutare.CallRouting.Call{}must namepipe_left. The key is enforced, so a test that builds the struct literally stops compiling; build it withCall.new/5instead. Matching on the struct is unaffected. Mutare.Test's source helpers run the invariant checks by default.diffs/3,diffs_for/4,metamutant_source/3,assert_metamutant_compiles/3, andcompile_metamutant/3raiseMutare.InvariantErrorfor a mutator that breaks the metamutant; passverify_invariants: falseto opt out.- Hosts that target the same fragment share one selector. Inside a function
body, a later
Mutare.Mutator.MacroHost's mutants join the earlier host's selector instead of nesting a second selector around it, and one coverage record names every id. Each mutant keeps its own host'swrap, the first host's wrapped original stays the fallback, and every host'ssplicestill runs in order — a later one now receives the combined selector. Where no active-id binding is in scope (a default argument, a function of a module defined at runtime) the selectors nest as before. - A lifted function's guard mutants share one clause. Where a clause's
mutants change only its guard, they become
whenalternatives of a single generated clause instead of each copying the clause body. On fixtures of large-bodied clauses with several guard mutants this cut the generated source by up to 4× and the compile's CPU time by up to half. Mutants and their results are unchanged.
Fixed
- A
with/for/tryclause with alternative guards no longer crashes the metamutant compile. Mutating a guard such asx when x > 10 when x < 0produced a nestedwhenthat the compiler's type checker rejected with no file or line, so poison recovery could not drop the mutant and the whole run aborted.
0.2.1 - 2026-09-17
Fixed
- Mutant locations are correct again under Sourceror 1.12.3. Sourceror used to
size a bare
true/false/nilone column too wide, and Mutare subtracted that phantom column to compensate. Sourceror 1.12.3 fixed the over-count upstream, so the subtraction began cutting a real character instead: a survivor whose range ends at one of those three rendered its diff a character short (trim: tru, closing paren left behind), and the JSON/SARIF reporters emitted the shortendColumn. The compensation is gone, andsourceroris now floored at~> 1.12.3so there is one upstream behaviour rather than two. Mutation behaviour was never affected — a metamutant is built from the AST, never the range — and reports on Sourceror 1.12.2 and earlier were correct as they stood.
Changed
- Mutant runs execute far more of the target at its original speed. A function
holding two or more mutation sites now keeps its own source beside the instrumented
code, and a mutant elsewhere runs that source. This previously reached only lifted
functions of eight or more variants written in a narrow subset of Elixir; it now covers
functions that stay in place and ordinary code — local bindings, sibling calls, closures,
comprehensions, bitstrings and interpolation, structs,
raise,Logger— about 89–97% of generated mutants in the projects measured, up from under 10%. Benchmark kernels that previously missed it run at 0.11–0.76× their former time while another mutant is active. The one metamutant compile costs about 8% more CPU and 17% more BEAM size. - Cheaper entry into every instrumented function. The active file is stored and
compared as an atom instead of a path string, and the id projection tells the compiler
it holds an integer.
Mutare.Selector.put/1andactive/0are unchanged; manual selection in a generated sandbox still usesMUTARE_MUTANT_NAMESPACEandMUTARE_ACTIVE_MUTANT.
0.2.0 - 2026-09-14
Changed
Breaking: score API moved from
Mutare.ReporttoMutare.Score:score/1,percent/1,passes_gate?/2,gate_failures/2,harness_error_rate/1, andharness_errors_exceed?/2.Breaking:
Mutare.Sandbox.prepare/3now returns{sandbox, materialized}. Seed-reuse outcomes and declined inference overrides are returned as data; the runner delivers their progress events.Breaking: live-report text helpers moved to
Mutare.Report.Live.Lines. Callers of the rendering and time-formatting functions previously onMutare.Report.Liveshould use the new module.Breaking for manual sandbox selection: runtime mutant IDs are now local to each file. Manual selection in a generated sandbox needs both
MUTARE_MUTANT_NAMESPACE(the root-relative file) andMUTARE_ACTIVE_MUTANT(its local ID). Changing another file's candidate count no longer changes an otherwise unchanged metamutant, allowing retained builds to survive unrelated changes. Reports still use globally unique IDs; standalone transforms retain integer-only selection.Focused runs and ignore directives produce smaller builds.
--line,--since, and--max-mutantsnow limit generated branches as well as execution, and ignore directives suppress branches before compilation. IDs, ignore diagnostics, and mutant-cap accounting are preserved. Files with no emitted mutants keep their original bytes, allowing their compiled modules to be reused.Smaller metamutants and less processing overhead. Clause mutations share code in eligible
case, anonymous-function,receive, andtry/rescueconstructs, and exclusion guards compress consecutive IDs. Scanning and poison recovery avoid redundant parsing; coverage recording caches hits per file and writes a more compact dump.Custom string-pattern mutations follow the built-in redundancy rule. When one mutator offers both empty and non-empty string replacements for an exact pattern, Mutare drops the empty replacement.
Added
- Guard mutations in more clause positions:
withandforgenerators,withandtryelseclauses,trycatchclauses, andfor … reduce:bodies. These are supported inside functions where the runtime selector is bound; module-level and default-argument positions remain excluded.
Fixed
- Mutations activate before project evaluation, runtime configuration, and application startup. Timeouts and owner-death watchers also start before target project code, containing mutations that hang during startup. Closures and workers created before test helpers load retain the correct selection and can record coverage when they run after recording begins; execution confined to startup is still not recorded as coverage.
- Mutations that prevent startup count as kills. Failures during project
evaluation, runtime configuration, or
Application.start/2use the startup retry budget before being scored as killed. Errors, exits, and throws retain the evidence needed for this classification even when deep stacktraces lose the project or configuration frames. Infrastructure failures remain harness errors. - An empty coverage result no longer manufactures survivors. A successful
probe that records no hits marks the selected mutants
:no_coverage, including runs focused entirely on untested code. Missing or malformed capture data still falls back to running the suite. - Signature inference stays disabled in sandbox Mix projects on Elixir 1.20.
The override now reaches effective compiler options, including umbrella
children and custom configuration paths, and overrides an explicit
infer_signatures: trueto prevent pathological metamutant compile times. Other compiler options are preserved. If a project cannot be safely rewritten,--verbosereports the file and reason, and its seeded compiler cache is left consistent with the options it actually uses. - Generated operators respect Mutare's semantics under restricted or replaced
Kernelimports. Selectors, guards, and coverage code no longer resolve operators through the target's imports. Coverage short-circuits also work on Elixir 1.21 development builds, where:erlang.andalsois guard-only. - Protocol implementations receive lifted mutations. Functions in
module-level
defimplblocks now receive guard, head-pattern, and clause mutations, with:skip_liftingresolving to the implementation module. - Immediately invoked anonymous functions are mutated. Mutare now analyzes
the callee in
callee.(args), including the guards, patterns, and bodies of(fn … end).(args). - Binding conditions preserve custom and return-value mutations. Custom
condition_replacementscallbacks now receiveif/unlessconditions whose bindings must escape into the body, even withIfConditiondisabled. Rewriting those conditions also preserves per-branch return mutations and the exclusion of unit-returning tails. - Bitstring generators no longer produce invalid value mutations. The
generator wrapper in
for <<… <- binary>>is treated as syntax, avoiding spurious compile-poison recovery. - Macro poison recovery associates each expansion with its own file. Nested expansion stacks no longer cross-match macro names and unrelated call sites; findings without a corresponding mutant are discarded instead of crashing recovery.
- Configuration rejects inconsistent extension and routing declarations.
Mutator modules cannot be registered as non-mutating extensions merely by
omitting
@behaviour; declarative routes reject the internal{:hosted, hosts}form; and:partition_envcannot overwriteERL_COMPILER_OPTIONSor other environment variables managed by Mutare. - The sandbox ownership-marker writer no longer follows symlinks.
- Self-hosted fixture coverage no longer overwrites the outer probe's state or dump.
Security
- Updated locked Igniter and Mint dependencies to 0.8.4 and 1.10.0 respectively
to address advisories reported by
mix hex.audit.
0.1.2 - 2026-09-07
Fixed
- The kept sandbox preserves empty directories. The incremental
materialisation that the default kept sandbox uses mirrored files and
symlinks only, so a directory with nothing in it never reached the sandbox —
and a shallow git checkout under
deps/keeps.git/refs/headsand.git/refs/tagsempty. git then no longer recognised the checkout and Mix reported every git dependency as a lock mismatch before the metamutant could compile; a freshly generated Phoenix 1.8 app (heroicons,daisyui) could not run Mutare at all. - Rendering no longer consults the target project's
.formatter.exs. Sourceror readslocals_without_parensfrom it throughMix.Tasks.Formaton every render — evaluating itsimport_depsand plugins inside Mutare's own process — and Mix could refuse the lookup mid-scan ("Unknown dependency:ecto_sqlgiven to:import_deps"). Every render now pins the option (Mutare.AST.render_opts/1); a parsed call keeps the spelling its metadata records, and a node built without metadata renders with parentheses. mix igniter.install mutarefetches the companion packages it adds. The companions are chosen from the project's own deps at run time, and Igniter writes a dep added that way tomix.exswithout fetching it — so the generated.mutare.exsnamed modules of packages that were never fetched or locked. The installer now applies themix.exschange and runsdeps.getbefore writing.mutare.exs.
0.1.1 - 2026-09-07
Fixed
- Unit-return classification exempts behaviour callbacks. A function that is a
callback of one of its module's declared behaviours (direct
@behaviouroruse-injected, read through the behaviour'sbehaviour_info/1when it is loadable), or whose first clause carries an@implother than@impl false, is never classified unit-returning, however its body reads. Its caller is the behaviour's runtime, which the source never shows and which may treat a lone:okas one contract outcome among several —Oban.Worker.perform/1's:okis one of six. 0.1.0 silenced the:okreturn mutants of every such callback, includingmutare_oban's worker-return family.
0.1.0 - 2026-09-07
Initial release.
Added
- Compile-once metamutant. Source under
lib/is rewritten into a single program that embeds every mutant behind a:persistent_termruntime switch, compiled once; the suite then runs once per mutant by flippingMUTARE_ACTIVE_MUTANT— no per-mutant recompilation. - A broad built-in mutator set (all on by default): arithmetic/operator and
operand swaps, relational and logical swaps, strict-equality relaxation,
literals of every kind (integer/float/string/charlist/atom/sigil/regex/
bitstring/date-time), collection/string/map/keyword call rewrites, pattern and
clause restructurings, guard/default/call drops, and more. See
Mutare.Mutators. - Unit-return classification — a function (or anonymous function) whose every
return path is literally
:okornilreturns no data, so its tails draw no return-value constant and no:ok → :errorswap. Syntactic, and one-sided: it can miss a unit function, never silence a data-returning one. - Coverage-guided test selection — coverage is self-recorded by the metamutant
at runtime and keyed by mutant id; each mutant runs only the test cases that
cover it (
--per-filewidens that to the covering test files, for statefulasync: falsesuites;--fullruns the whole suite every time). Uncovered mutants are skipped and excluded from the score. - Parallel workers with per-mutant timeouts — mutants run
:workersat a time, each capped by a wall-clock deadline; a mutation that hangs halts itself and counts as a kill (no process-tree killing). - Compile-poison recovery — a mutant that won't compile is identified from the
compile error, dropped (reported as poisoned, excluded from the score), and
the build retried, for a bounded number of rounds; only a compile error that
can't be attributed to any mutant (or that outlasts the bound) aborts the run,
with a copy-pasteable
:skipsnippet. # mutare:ignoredirective — suppress a known-equivalent mutant per line, per span (-start/-end), or per file (-file), with an optional free-text reason and an optional[family]/[family:label]filter; ineffective directives are surfaced (--strict-ignoresescalates).- Reporters — human (default, survivor diffs + score), plus machine-readable
json(Stryker / mutation-testing-elements schema),html(interactive viewer), andsarif(GitHub code scanning) via--report FORMAT[:PATH](repeatable) or:reporters. - Live progress on stderr; the detailed report and score are printed to stdout.
- CI integration —
--since <ref>to scope to the lines changed against a git ref,--min-scoreto gate,--lineto target lines, and a kept sandbox (the default) so the compiled build carries across runs;--sandbox <path>pins it at a CI cache,--no-keep-sandboxopts back into a throwaway copy. - Umbrella-aware — target one app, several, or the whole workspace.
- Extension surface —
Mutare.Mutator(custom mutators), independentMutare.CallRoutingandMutare.UseExpansioncapabilities,:extensionsfor non-mutating integrations, declarative:call_routesconfiguration (user-tier treatments only; the adapter-grade treatments must come from a module implementingMutare.CallRouting), theMutare.ASTnode constructors that discharge Sourceror's emission invariants for plugins, and theMutare.Callscall-resolution readers (resolved_call_to/3,module_key/1) so plugins match calls without building core's key representation, and a declarative environment guard (required_modules/0, on mutators and extensions): the modules a DSL plugin routes are checked loadable once at startup, aborting withMutare.EnvironmentErrorinstead of silently registering routes against nothing on an external-source run. mix igniter.install mutareinstaller that detects frameworks and wires up the matching companion packages and.mutare.exs.- Call routes (
call_routes:/--skip-call Module.fun/arity) — leave a call alone::skipmakes a whole call an inert leaf (functions, macros, and the construct special forms alike —Kernel.if/2orcaseincluded; a piped receiver and the enclosing function's return-value mutants are unaffected),:rawleaves an argument as written,:interiormutates an argument's contents but never its own node, and a keyed refinement ([:expression, timeout: :raw]) reaches one option of a literal keyword argument. The forms Mutare analyzes structurally (if,case, the boolean operators, …) take:skiponly, and definitions (def,defmodule, …) and literal syntax ({},%{},=, …) take no route. Routes match qualified, aliased, imported, and piped forms, and an entry that matches no call in a full scan is warned about. - Argument marks (
argument_marks:) — extend the built-in timeout table (or any label a mutator declares) to your own functions, with the mutators' value-aware reaction:{MyApp.Http, :get, 2, [{:keyword, :recv_timeout}], :timeout}.