lib/foundry/lint/runner.ex

defmodule Foundry.Lint.Runner do
  @moduledoc """
  High-level orchestrator for the lint suite.

  Composes manifest validation, module discovery, and per-module rule execution
  into a single `LintReport` struct suitable for CI/CLI output and JSON serialization.

  ## Usage

      report = Foundry.Lint.Runner.run(project_root)
      # => %Foundry.Lint.LintReport{
      #      passed: true,
      #      violations: [...],
      #      error_count: 0,
      #      warning_count: 2,
      #      info_count: 0,
      #      generated_at: "2026-03-22T..."
      #    }
  """

  alias Foundry.Lint.LintReport
  alias Foundry.LintRules.Registry

  def run(project_root) do
    {:ok, manifest} = Foundry.Manifest.Parser.read(project_root)

    # Manifest-level validation (e.g., missing approvers, invalid resources)
    manifest_violations = Foundry.LintRules.ManifestValidator.check(manifest)

    # Module-level discovery and validation
    project_name = Keyword.get(manifest, :project_name, "")
    modules = Foundry.Context.ModuleDiscovery.all_project_modules(project_root, project_name)

    metadata = %{
      manifest: manifest,
      sensitive_modules: Keyword.get(manifest, :sensitive_resources, []),
      project_root: project_root
    }

    {spark_violations, _rule_errors} =
      SparkLint.Runner.run(
        Registry.module_rules(),
        modules,
        %{metadata: metadata}
      )

    # Merge manifest and module-level violations
    all_spark = manifest_violations ++ spark_violations

    # Convert SparkLint.Violation → LintReport.Violation (name field mapping: :rule → :rule_id)
    violations =
      all_spark
      |> Enum.map(&to_lint_violation/1)
      |> Enum.sort_by(fn v -> {severity_order(v.severity), v.module} end)

    error_count = Enum.count(violations, & &1.severity == :error)
    warning_count = Enum.count(violations, & &1.severity == :warning)
    info_count = Enum.count(violations, & &1.severity == :info)

    %LintReport{
      passed: error_count == 0,
      violations: violations,
      error_count: error_count,
      warning_count: warning_count,
      info_count: info_count,
      generated_at: DateTime.utc_now() |> DateTime.to_iso8601()
    }
  end

  # ---------------------------------------------------------------------------
  # Private
  # ---------------------------------------------------------------------------

  defp to_lint_violation(%SparkLint.Violation{} = v) do
    %LintReport.Violation{
      rule_id: v.rule,
      severity: v.severity,
      message: v.message,
      module: inspect(v.module)
    }
  end

  defp severity_order(:error), do: 0
  defp severity_order(:warning), do: 1
  defp severity_order(_), do: 2
end