Skip to main content

lib/mix/tasks/scoria.warning_inventory.ex

defmodule Mix.Tasks.Scoria.WarningInventory do
  use Mix.Task

  @shortdoc "Captures and classifies full-suite warning inventory for maintainers"

  alias Scoria.WarningInventory

  @switches [
    format: :string,
    write: :boolean,
    since: :string,
    scope: :string,
    include_runtime: :boolean,
    quiet: :boolean
  ]

  @impl Mix.Task
  def run(args) do
    {opts, _, invalid} = OptionParser.parse(args, strict: @switches)

    if invalid != [] do
      Mix.raise("invalid options: #{inspect(invalid)}")
    end

    preflight!()

    format = Keyword.get(opts, :format, "table")
    scope = Keyword.get(opts, :scope, "full")
    quiet? = Keyword.get(opts, :quiet, false)
    since = validate_since!(Keyword.get(opts, :since))
    include_runtime? = Keyword.get(opts, :include_runtime, false)

    output =
      if quiet? do
        ""
      else
        WarningInventory.capture_output()
      end

    parsed = WarningInventory.parse_output(output)

    runtime_rows =
      if include_runtime? do
        runtime_log_rows(output)
      else
        []
      end

    rows =
      (parsed ++ runtime_rows)
      |> WarningInventory.classify()
      |> apply_scope(scope)
      |> WarningInventory.join_baseline()

    metadata = %{
      "schema_version" => "1.0",
      "git_sha" => git_sha(),
      "generated_at" => DateTime.utc_now() |> DateTime.to_iso8601(),
      "since" => since,
      "scope" => scope
    }

    if Keyword.get(opts, :write, false) do
      write_artifacts!(rows, metadata)
    end

    render(rows, format, metadata)
  end

  defp preflight! do
    WarningInventory.ensure_clean_tmp!()

    unless pgvector_available?() do
      Mix.shell().info(
        "Note: pgvector may be unavailable locally; knowledge cluster counts can be incomplete."
      )
    end
  end

  defp runtime_log_rows(output) do
    output
    |> String.split("\n")
    |> Enum.filter(&Regex.match?(~r/(async|teardown|sandbox)/iu, &1))
    |> Enum.reject(&String.contains?(&1, "[warning]"))
    |> Enum.map(fn line ->
      %{
        file: "runtime",
        line: 0,
        message: String.trim(line),
        signal_kind: :runtime_log,
        compiler_kind: :runtime
      }
    end)
  end

  defp apply_scope(rows, "high_signal") do
    Enum.filter(rows, fn row ->
      String.starts_with?(row.file, "lib/") or
        row.file in Mix.Tasks.Scoria.Test.Adoption.adoption_test_files() or
        String.contains?(row.file, "test/scoria_web/live/")
    end)
  end

  defp apply_scope(rows, _scope), do: rows

  defp render(rows, "json", metadata) do
    payload = Map.put(metadata, "rows", json_encode_rows(rows))
    Mix.shell().info(Jason.encode!(payload, pretty: true))
  end

  defp render(rows, "md", metadata) do
    Mix.shell().info(render_markdown(rows, metadata))
  end

  defp render(rows, _format, metadata) do
    Mix.shell().info("Warning inventory (#{metadata["scope"]}, #{map_size(WarningInventory.cluster_counts(rows))} clusters)")

    for row <- Enum.sort_by(rows, &{&1.ratchet_tier, &1.cluster_id, &1.file}) do
      Mix.shell().info(
        "#{row.cluster_id} #{row.file}:#{row.line} #{String.slice(row.message, 0, 80)}"
      )
    end
  end

  defp write_artifacts!(rows, metadata) do
    File.mkdir_p!("tmp/warning-inventory")

    baseline_json = %{
      "schema_version" => metadata["schema_version"],
      "git_sha" => metadata["git_sha"],
      "generated_at" => metadata["generated_at"],
      "clusters" =>
        rows
        |> WarningInventory.cluster_counts()
        |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
    }

    File.write!(
      ".planning/warning-inventory.baseline.json",
      Jason.encode!(baseline_json, pretty: true)
    )

    File.write!(".planning/WARNING-INVENTORY.md", render_markdown(rows, metadata))

    File.write!(
      "tmp/warning-inventory/latest.json",
      Jason.encode!(Map.put(metadata, "rows", json_encode_rows(rows)), pretty: true)
    )

    Mix.shell().info("==> Wrote .planning/warning-inventory.baseline.json")
    Mix.shell().info("==> Wrote .planning/WARNING-INVENTORY.md")
    Mix.shell().info("==> Wrote tmp/warning-inventory/latest.json")
  end

  defp render_markdown(rows, metadata) do
    counts = WarningInventory.cluster_counts(rows)

    queue =
      counts
      |> Enum.sort_by(fn {cluster_id, _count} -> WarningInventory.ratchet_tier(cluster_id) end)
      |> Enum.map(fn {cluster_id, count} ->
        "| #{cluster_id} | #{count} | #{WarningInventory.ratchet_tier(cluster_id)} |"
      end)
      |> Enum.join("\n")

    phase_67_queue = """
    ## Phase 67 — Fixed vs Deferred

    | Cluster | Action | Owner | Expiry / Notes |
    |---------|--------|-------|----------------|
    | :test_unused_binding | fix | @scoria-core | Phase 67 plan 67-03/67-04 |
    | :test_dead_default_args | fix | @scoria-core | Phase 67 plan 67-03/67-04 |
    | :knowledge_migration_redefine | fix | @scoria-core | migrate-once + scoped ignore_module_conflict (D-11) |
    | :unclassified_compile | fix | @scoria-core | zero in high-signal scope; classify or fix code |
    | :host_proof_generated_compile | defer | @scoria-core | p2 guard only; overlay stays in priv/ |
    | :host_overlay_test_path | defer | @scoria-core | p2 guard only; no CI adoption WAE in Phase 67 |
    | :liveview_async_teardown | defer | @scoria-web-runtime | p4 baselined until 2026-06-30 |
    """

    """
    # Warning Inventory

    Generated: #{metadata["generated_at"]}
    Git SHA: #{metadata["git_sha"]}
    Scope: #{metadata["scope"]}

    ## Phase 67 Ratchet Queue

    | Cluster | Count | Ratchet Tier |
    |---------|------:|--------------|
    #{queue}

    #{phase_67_queue}
    """
  end

  defp json_encode_rows(rows) do
    Enum.map(rows, fn row ->
      Map.new(row, fn {key, value} ->
        {Atom.to_string(key), json_encode_value(value)}
      end)
    end)
  end

  defp json_encode_value(value) when is_atom(value), do: Atom.to_string(value)

  defp json_encode_value(value) when is_list(value) do
    Enum.map(value, &json_encode_value/1)
  end

  defp json_encode_value({a, b, c}) when is_atom(a), do: [Atom.to_string(a), b, c]
  defp json_encode_value(value), do: value

  defp git_sha do
    case System.cmd("git", ["rev-parse", "HEAD"], stderr_to_stdout: true) do
      {sha, 0} -> String.trim(sha)
      _ -> "unknown"
    end
  end

  defp validate_since!(nil), do: nil

  defp validate_since!(ref) when is_binary(ref) do
    if String.contains?(ref, ";") or String.contains?(ref, "`") do
      Mix.raise("invalid --since ref: #{inspect(ref)}")
    end

    ref
  end

  defp pgvector_available? do
    false
  end
end