Current section

Files

Jump to
ragex lib mix tasks ragex.audit.ex
Raw

lib/mix/tasks/ragex.audit.ex

defmodule Mix.Tasks.Ragex.Audit do
@moduledoc """
Generates an AI-powered code audit report as JSON.
Combines comprehensive static analysis with an AI-generated professional
code audit report. The output is a JSON document containing all structured
analysis results plus an `"audit"` field with the AI-generated Markdown report.
## Usage
mix ragex.audit [options]
## Options
* `--path PATH` - Directory to analyze (default: current directory)
* `--output FILE` - Output file (default: stdout)
* `--dead-code` - Include dead code analysis (disabled by default, can be slow)
* `--provider PROVIDER` - AI provider: deepseek_r1, openai, anthropic, ollama
* `--model MODEL` - Model name override
* `--verbose` - Show progress on stderr
* `--with-empty` - Include empty result categories in output (default: false)
* `--help` - Show this help
## Examples
# Audit current directory
mix ragex.audit
# Audit specific directory, save to file
mix ragex.audit --path lib/ --output audit.json
# Include dead code analysis with progress
mix ragex.audit --dead-code --verbose --output report.json
## Output Format
JSON with the following top-level keys:
* `timestamp` - ISO 8601 audit timestamp
* `path` - Analyzed directory path
* `audit` - AI-generated Markdown audit report (string)
* `graph` - Knowledge graph statistics (nodes, edges, modules, functions, embeddings)
* `results` - Structured analysis results (compatible with `mix ragex.analyze --format json`)
* `summary` - Issue counts by category
* `config` - Analysis configuration used
"""
@shortdoc "Generates AI-powered code audit report as JSON"
use Mix.Task
alias Ragex.Agent.Core
alias Ragex.Analysis.{BusinessLogic, DependencyGraph, Quality}
alias Ragex.Graph.Store
@impl Mix.Task
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
strict: [
path: :string,
output: :string,
dead_code: :boolean,
provider: :string,
model: :string,
verbose: :boolean,
with_empty: :boolean,
help: :boolean
],
aliases: [p: :path, o: :output, m: :model, h: :help]
)
if opts[:help] do
Mix.shell().info(@moduledoc)
else
run_audit(opts)
end
end
# Private functions
defp run_audit(opts) do
verbose = Keyword.get(opts, :verbose, false)
path = Keyword.get(opts, :path, File.cwd!()) |> Path.expand()
output_file = Keyword.get(opts, :output)
# Disable MCP server for non-interactive JSON output
Application.put_env(:ragex, :start_server, false)
# Suppress logger for clean output unless verbose
unless verbose, do: Logger.configure(level: :emergency)
Mix.Task.run("app.start")
if verbose do
Logger.configure(level: :info)
progress("Starting audit: #{path}")
end
# Always suppress Executor's stdout printing to keep JSON output clean.
# Progress is reported via Logger (stderr) when --verbose.
core_opts =
[
include_dead_code: Keyword.get(opts, :dead_code, false),
skip_embeddings: false,
verbose: false
]
|> maybe_put(:provider, parse_provider(opts[:provider]))
|> maybe_put(:model, opts[:model])
if verbose, do: progress("Running analysis pipeline...")
case Core.analyze_project(path, core_opts) do
{:ok, result} ->
if verbose, do: progress("AI report generated. Running supplementary analyses...")
supplementary = run_supplementary(path)
graph_stats = Store.stats()
modules = Store.list_nodes(:module, :infinity)
functions = Store.list_nodes(:function, :infinity)
json_report =
build_json(path, result, supplementary, graph_stats, modules, functions, opts)
encoded = Jason.encode!(json_report, pretty: true)
case output_file do
nil ->
IO.puts(encoded)
file ->
File.write!(file, encoded)
if verbose, do: progress("Audit report written to #{file}")
end
{:error, reason} ->
IO.puts(:stderr, "Audit failed: #{inspect(reason)}")
System.halt(1)
end
end
defp run_supplementary(path) do
%{
business_logic:
safe_run(fn -> BusinessLogic.analyze_directory(path, min_severity: :medium) end),
dependencies: safe_run(fn -> DependencyGraph.analyze_all_dependencies() end),
quality_score: safe_run(fn -> Quality.analyze_quality(path) end)
}
end
defp build_json(path, result, supplementary, graph_stats, modules, functions, opts) do
with_empty = Keyword.get(opts, :with_empty, false)
quality_metrics =
case result.issues[:quality_metrics] do
m when is_map(m) and map_size(m) > 0 -> m
_ -> %{}
end
results =
%{
security: %{issues: result.issues[:security] || []},
complexity: %{complex_functions: result.issues[:complexity] || []},
smells: %{smells: result.issues[:smells] || []},
duplicates: %{duplicates: result.issues[:duplicates] || []},
dead_code: %{dead_functions: result.issues[:dead_code] || []},
circular_dependencies: %{cycles: result.issues[:circular_deps] || []},
quality_metrics: quality_metrics,
suggestions: %{items: result.issues[:suggestions] || []},
business_logic: supplementary.business_logic || %{total_issues: 0, results: []},
dependencies: supplementary.dependencies || %{modules: %{}},
quality: supplementary.quality_score || %{overall_score: 0}
}
|> then(fn r -> if with_empty, do: r, else: filter_non_empty(r) end)
%{
timestamp: DateTime.utc_now(),
path: path,
audit: result.report,
ai_status: result[:ai_status] || %{status: "unknown"},
graph: %{
nodes: graph_stats.nodes,
edges: graph_stats.edges,
embeddings: graph_stats.embeddings,
modules: length(modules),
functions: length(functions)
},
results: results,
summary: result.summary,
config: %{
dead_code: Keyword.get(opts, :dead_code, false),
provider: opts[:provider] || "default"
}
}
end
defp filter_non_empty(results) do
results
|> Enum.reject(fn {_key, value} -> empty_result?(value) end)
|> Map.new()
end
defp empty_result?(%{issues: []}), do: true
defp empty_result?(%{complex_functions: []}), do: true
defp empty_result?(%{smells: []}), do: true
defp empty_result?(%{duplicates: []}), do: true
defp empty_result?(%{dead_functions: []}), do: true
defp empty_result?(%{cycles: []}), do: true
defp empty_result?(%{items: []}), do: true
defp empty_result?(%{total_issues: 0}), do: true
defp empty_result?(%{modules: m}) when map_size(m) == 0, do: true
defp empty_result?(%{overall_score: _}), do: false
defp empty_result?(m) when m == %{}, do: true
defp empty_result?(_), do: false
# Helpers
defp safe_run(func) do
case func.() do
{:ok, result} -> result
{:error, _} -> nil
end
rescue
_ -> nil
catch
:exit, _ -> nil
end
defp parse_provider(nil), do: nil
defp parse_provider(name), do: String.to_existing_atom(name)
defp maybe_put(opts, _key, nil), do: opts
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
defp progress(msg), do: IO.puts(:stderr, msg)
end