defmodule Mix.Tasks.Frontier.Visualize do @shortdoc "Generates a visual dependency graph of Frontier boundaries." @moduledoc """ Generates a DOT or PNG graph of Frontier boundary relationships. The graph shows actual cross-context calls detected by the compiler tracer, colored by whether they're allowed or violations: * **Green edges** — allowed cross-context calls * **Red edges** — boundary violations * **Dashed orange edges** — known violations silenced via `skip_violations:` Violations are listed in a legend at the bottom of the graph. $ mix frontier.visualize # outputs frontier.dot $ mix frontier.visualize --format png # outputs frontier.png (requires graphviz) $ mix frontier.visualize --output ./docs # custom output directory ## Options * `--format` - Output format: `dot` (default) or `png` * `--output` - Output directory (default: current directory) """ use Mix.Task alias Frontier.Config alias Frontier.Graph alias Frontier.Visualization @impl Mix.Task def run(argv) do {opts, _rest, _errors} = OptionParser.parse(argv, strict: [format: :string, output: :string]) Mix.Task.run("compile") Config.init() format = Keyword.get(opts, :format, "dot") output_dir = Keyword.get(opts, :output, File.cwd!()) File.mkdir_p!(output_dir) dot_content = Visualization.build() |> Graph.to_dot() dot_path = Path.join(output_dir, "frontier.dot") File.write!(dot_path, dot_content) Mix.shell().info([:green, "Generated #{dot_path}"]) if format == "png" do png_path = Path.join(output_dir, "frontier.png") generate_png(dot_path, png_path) end end defp generate_png(dot_path, png_path) do case System.find_executable("dot") do nil -> Mix.shell().info([ :yellow, "Graphviz `dot` not found. Install it to generate PNG:\n", " brew install graphviz\n", "DOT file saved — you can render it manually:\n", " dot -Tpng #{dot_path} -o #{png_path}" ]) dot_cmd -> case System.cmd(dot_cmd, ["-Tpng", dot_path, "-o", png_path]) do {_output, 0} -> Mix.shell().info([:green, "Generated #{png_path}"]) {output, _code} -> Mix.shell().info([:red, "Failed to generate PNG: #{output}"]) end end end end