The runtime wiring for a mutation run: the validated Mutare.Options
(configuration) bundled with the things that are not configuration — the
resolved Mutare.Project (copy-root + mutate-scope, derived from the target
path and --app/--workspace) and the four optional live-progress hooks
(reporter, on_phase, on_start, on_scan).
on_phase receives both the phase-transition atoms (:compiling → :baseline
→ :coverage_probe → {:running, total}) and structured detail events fired
alongside them — {:compiled, ms}, {:baseline_done, ms}, {:coverage_done, summary}, {:run_config, cfg} (from Mutare.Runner) and {:seed_app_build, summary} (from Mutare.Sandbox, during :compiling, reporting what the app-build
_build seed did) — carrying the behind-the-scenes numbers --verbose renders. A
custom hook should ignore events it doesn't know (the in-tree consumer,
Mutare.Report.Live, has a catch-all).
Splitting these off Mutare.Options keeps that struct pure configuration. The
pipeline (Mutare.Schema, Mutare.Sandbox, Mutare.Runner) threads a
Run.Context, reading config from context.options and wiring from the
context's own fields.
Wrap-at-boundary
new/1 is the normalization boundary, mirroring Mutare.Options.new/1: it
accepts an existing Run.Context (idempotent), a bare Mutare.Options (no
wiring), or a keyword list. For a keyword list it splits the wiring keys
(:project + the hooks) from the configuration keys, validating the former here
and routing the latter through Mutare.Options.new/1. So an existing
keyword-list call site — Schema.build(root, project: p, mutators: m) — keeps
working unchanged: the wiring rides into the context, the rest into options.
new/2 is the convenience the Mix task uses to attach wiring to an already
resolved Options (Run.Context.new(options, project: project)).
Summary
Functions
Ensure the context carries a Mutare.Project, resolving one from root if it is
unset. The entry points (Mutare.Runner.run/2, the Mix task) resolve the project
from the target path + scope flags; the direct Mutare.run/2 API leaves it nil,
so it is resolved here as a single-app project at root.
The 1-arity hook bound to field (:reporter/:on_phase/:on_start/:on_scan),
or a no-op when unset — so Mutare.Runner and Mutare.Schema invoke it
unconditionally without each re-stating the || fn _ -> :ok end default. The
single home for that default.
Normalize an input into a Run.Context.
Build a Run.Context from already-resolved options (an Options or a keyword
list) plus a keyword list of wiring (:project and/or the hooks).
Types
@type t() :: %Mutare.Run.Context{ defer_site_code: boolean(), on_phase: hook(), on_scan: hook(), on_start: hook(), options: Mutare.Options.t(), project: Mutare.Project.t() | nil, reporter: hook(), summarize_sites: boolean() }
Functions
Ensure the context carries a Mutare.Project, resolving one from root if it is
unset. The entry points (Mutare.Runner.run/2, the Mix task) resolve the project
from the target path + scope flags; the direct Mutare.run/2 API leaves it nil,
so it is resolved here as a single-app project at root.
The 1-arity hook bound to field (:reporter/:on_phase/:on_start/:on_scan),
or a no-op when unset — so Mutare.Runner and Mutare.Schema invoke it
unconditionally without each re-stating the || fn _ -> :ok end default. The
single home for that default.
@spec new(t() | Mutare.Options.t() | keyword()) :: t()
Normalize an input into a Run.Context.
Accepts an existing Run.Context (returned as-is), a Mutare.Options (wrapped
with no wiring), or a keyword list (wiring keys split out and validated, the
rest validated through Mutare.Options.new/1).
iex> ctx = Mutare.Run.Context.new(mutators: [:arithmetic], on_scan: fn _ -> :ok end)
iex> {Enum.map(ctx.options.mutators, & &1.name), is_function(ctx.on_scan, 1)}
{[:arithmetic], true}
iex> ctx = Mutare.Run.Context.new(workers: 2)
iex> Mutare.Run.Context.new(ctx) == ctx
true
@spec new( Mutare.Options.t() | keyword(), keyword() ) :: t()
Build a Run.Context from already-resolved options (an Options or a keyword
list) plus a keyword list of wiring (:project and/or the hooks).