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.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 print 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}.