# `Mutare.Project`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.2.1/lib/mutare/project.ex#L1)

Resolve the *shape* of the target into a copy-root and a mutate-scope.

Mutare copies one tree into the sandbox and mutates a set of source files in it.
For a plain Mix project those are the same directory. For an **umbrella** they
differ: the whole umbrella is copied (so `in_umbrella` sibling deps and the
shared `deps/`/`config/` keep resolving), while only a chosen subset of `apps/*`
gets metamutants.

`resolve/2` turns the user-supplied target path plus the `--app`/`--workspace`
flags into:

  * `copy_root` — the directory materialised into the sandbox (the umbrella
    root, or the project root for a single app). This is the `root` the rest of
    the pipeline (`Mutare.Schema`, `Mutare.Sandbox`, `Mutare.Runner`) treats as
    authoritative for every path operation; the struct duplicates it only so it is
    available to the entry points.
  * `mutate_scope` — the apps whose sources are mutated, as `%{app, dir}` entries
    with `dir` relative to `copy_root` (`"apps/foo"`, or `"."` for a single app).
  * `apps` — every app whose suite runs (all umbrella apps), which the per-app
    bootstrap injection and the dependency-graph scoping need.

Detection is **static** — Mutare runs outside the target's Mix, so the layout is
read from the filesystem (an `apps/` dir plus an `apps_path:` in the root
`mix.exs`) rather than from `Mix.Project`.

# `app`

```elixir
@type app() :: %{app: atom() | nil, dir: String.t()}
```

# `t`

```elixir
@type t() :: %Mutare.Project{
  apps: [app()],
  copy_root: Path.t(),
  mutate_scope: [app()],
  umbrella?: boolean()
}
```

# `app_test_scopes`

```elixir
@spec app_test_scopes(t(), Path.t(), %{required(atom()) =&gt; [atom()]}) :: %{
  required(atom()) =&gt; [String.t()]
}
```

Per mutate-scope app, the root-relative `test/` dirs a broad (whole-suite) run
may be narrowed to: the app itself plus every app that (transitively) depends on
it. A mutant in app A can only be killed by a test that executes A's code, and a
sibling executes A's code only through a declared dependency — so this set is a
safe superset of A's possible killers; narrowing below it would risk a false
survivor, so we never do.

`forward` is the **declared** inter-app graph, `%{app => [apps it depends on]}`,
as `Mutare.Runner.AppGraph.read/2` reads it from Mix (nodes and deps outside the
umbrella are ignored). Declared, not runtime: a `runtime: false` sibling dep is
absent from the compiled `.app`'s `applications` yet fully callable from the
dependent's tests. Only dirs that actually exist under `sandbox` are returned.
Returns `%{}` (⇒ no narrowing, run the whole umbrella) for a single app, or when
`forward` lacks an umbrella app — a missing node would hide that app's
dependents, so degrade safe rather than narrow on doubt.

# `project_dirs`

```elixir
@spec project_dirs(t() | nil) :: [String.t()]
```

Root-relative directories that hold a Mix project file Mutare may rewrite in the sandbox.

The root, plus each umbrella child. `Mutare.Sandbox` *attempts* to wrap exactly these
`mix.exs` files to force `infer_signatures: false`. Wrapping is not possible for an unreadable
file, source that Sourceror cannot faithfully round-trip, or a `mix.exs` defining no module
of its own. `Mutare.Sandbox.Seed` therefore realigns manifests
against the successfully applied wrappers — reported back by
`CompilerOptions.project_source/1` — not against this list. Realigning an app that
compiles with inference on invents a cache-key mismatch and cold-compiles it for nothing.

# `project_file`

```elixir
@spec project_file(String.t()) :: String.t()
```

The root-relative `mix.exs` path for a `project_dirs/1` entry.

The one derivation both halves of the inference override share: `Mutare.Sandbox` builds the
files it wraps from it, and `Mutare.Sandbox.Seed` rebuilds the same key to check whether a
given app's wrapper was applied. Two spellings of this join would silently stop matching.

# `resolve`

```elixir
@spec resolve(
  Path.t(),
  keyword()
) :: t()
```

Resolve `target` (+ scope flags) into a `t:t/0`.

`opts`:
  * `:apps` — app-name strings to mutate (from `--app`); `nil`/`[]` means "all".
  * `:workspace` — `true` mutates every app (from `--workspace`).

A single (non-umbrella) project resolves to `copy_root: target` with a lone
`%{app: nil, dir: "."}` scope, so the existing single-app pipeline is unchanged.

# `umbrella_root?`

```elixir
@spec umbrella_root?(Path.t()) :: boolean()
```

Is `dir` (an absolute path) the root of an umbrella project?

---

*Consult [api-reference.md](api-reference.md) for complete listing*
