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.Report to Mutare.Score: score/1, percent/1, passes_gate?/2, gate_failures/2, harness_error_rate/1, and harness_errors_exceed?/2.

  • Breaking: Mutare.Sandbox.prepare/3 now 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 on Mutare.Report.Live should 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) and MUTARE_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-mutants now 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, and try/rescue constructs, 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: with and for generators, with and try else clauses, try catch clauses, and for … 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/2 use 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: true to prevent pathological metamutant compile times. Other compiler options are preserved. If a project cannot be safely rewritten, --verbose reports 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 Kernel imports. 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.andalso is guard-only.
  • Protocol implementations receive lifted mutations. Functions in module-level defimpl blocks now receive guard, head-pattern, and clause mutations, with :skip_lifting resolving 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_replacements callbacks now receive if/unless conditions whose bindings must escape into the body, even with IfCondition disabled. 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_env cannot overwrite ERL_COMPILER_OPTIONS or 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/heads and .git/refs/tags empty. 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 reads locals_without_parens from it through Mix.Tasks.Format on every render — evaluating its import_deps and plugins inside Mutare's own process — and Mix could refuse the lookup mid-scan ("Unknown dependency :ecto_sql given 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 mutare fetches 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 to mix.exs without fetching it — so the generated .mutare.exs named modules of packages that were never fetched or locked. The installer now applies the mix.exs change and runs deps.get before 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 @behaviour or use-injected, read through the behaviour's behaviour_info/1 when it is loadable), or whose first clause carries an @impl other 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 :ok as one contract outcome among several — Oban.Worker.perform/1's :ok is one of six. 0.1.0 silenced the :ok return mutants of every such callback, including mutare_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_term runtime switch, compiled once; the suite then runs once per mutant by flipping MUTARE_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 :ok or nil returns no data, so its tails draw no return-value constant and no :ok → :error swap. 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-file widens that to the covering test files, for stateful async: false suites; --full runs the whole suite every time). Uncovered mutants are skipped and excluded from the score.
  • Parallel workers with per-mutant timeouts — mutants run :workers at 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 :skip snippet.
  • # mutare:ignore directive — 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-ignores escalates).
  • Reporters — human (default, survivor diffs + score), plus machine-readable json (Stryker / mutation-testing-elements schema), html (interactive viewer), and sarif (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-score to gate, --line to 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-sandbox opts back into a throwaway copy.
  • Umbrella-aware — target one app, several, or the whole workspace.
  • Extension surface — Mutare.Mutator (custom mutators), independent Mutare.CallRouting and Mutare.UseExpansion capabilities, :extensions for non-mutating integrations, declarative :call_routes configuration (user-tier treatments only; the adapter-grade treatments must come from a module implementing Mutare.CallRouting), the Mutare.AST node constructors that discharge Sourceror's emission invariants for plugins, and the Mutare.Calls call-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 with Mutare.EnvironmentError instead of silently registering routes against nothing on an external-source run.
  • mix igniter.install mutare installer 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: :skip makes a whole call an inert leaf (functions, macros, and the construct special forms alike — Kernel.if/2 or case included; a piped receiver and the enclosing function's return-value mutants are unaffected), :raw leaves an argument as written, :interior mutates 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 :skip only, 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}.