Read an umbrella's declared inter-app dependency graph out of the sandbox.
Mutare.Project.app_test_scopes/3 narrows a broad (whole-umbrella) mutant run
to the mutant's owning app plus its transitive dependents, which needs the
forward graph %{app => [sibling apps it depends on]}. This module gets it from
Mix: one mix eval in the sandbox (no compile, no deps check) merges
Mix.Project.deps_tree/0 from the umbrella root with sibling applications named
by each child's application/0. Every child's deps/0 and application/0 are
evaluated under MIX_ENV=test exactly as the test runs see them. Consequently,
a runtime: false sibling and one named only through :extra_applications or
explicit :applications all count as dependencies, while an only: that
excludes the test env does not. Why the graph is read this way rather than only
off the build: NOTES "Umbrella narrowing must follow the declared graph".
Output contract
The evaluated snippet prints one mutare-dep <app> <dep> <dep>… line per node
of the tree and a closing mutare-dep-end line. parse/2 keeps the lines
naming an umbrella app, drops deps that aren't umbrella apps (Hex packages),
matches names as strings against the known app list (never String.to_atom/1
on subprocess output), and is :error unless every umbrella app was reported
and the end marker arrived — a missing node would silently lose that app's
edges, so any incompleteness means "unknown graph" and the caller runs the
whole umbrella.
Summary
Types
Functions
Decode mix eval output (see the module doc's output contract) into the forward
graph over names, the known umbrella apps.
@spec read(Mutare.Project.t(), Path.t()) :: {:ok, forward()} | :error
The declared graph of project's umbrella apps, read from the sandbox.
A single (non-umbrella) project is {:ok, %{}} without running anything. For an
umbrella, a failed mix eval or an incomplete tree (see parse/2) is :error
— logged, since the fallback (every broad run covers the whole umbrella) is
safe but slow.