lib/mix/tasks/relyra.batteries_included.ex

defmodule Mix.Tasks.Relyra.BatteriesIncluded do
  @moduledoc """
  Generates the checked-in batteries-included proof artifact from executable repo state.

      mix relyra.batteries_included
      mix relyra.batteries_included --output tmp/BATTERIES_INCLUDED.md
      mix relyra.batteries_included --check
  """
  @shortdoc "Generate or drift-check BATTERIES_INCLUDED.md."

  use Mix.Task

  @default_output "BATTERIES_INCLUDED.md"
  @install_test "test/mix/relyra_install_test.exs"
  @demo_test "test/test_support_demo_test.exs"
  @task_test "test/mix/tasks/relyra_batteries_included_test.exs"

  @impl true
  def run(args) do
    Mix.Task.run("app.start")

    {opts, _argv, invalid} =
      OptionParser.parse(args,
        strict: [output: :string, check: :boolean],
        aliases: [o: :output]
      )

    if invalid != [] do
      Mix.raise("invalid options: #{Enum.map_join(invalid, ", ", &elem(&1, 0))}")
    end

    output_path = output_path(opts)
    contents = render_report()

    if Keyword.get(opts, :check, false) do
      check_report!(output_path, contents)
    else
      File.write!(output_path, contents)
      Mix.shell().info("relyra.batteries_included: wrote generated report to #{output_path}")
      :ok
    end
  end

  defp output_path(opts) do
    opts
    |> Keyword.get(:output, @default_output)
    |> Path.expand()
  end

  defp check_report!(output_path, contents) do
    case File.read(output_path) do
      {:ok, existing} when existing == contents ->
        Mix.shell().info(
          "relyra.batteries_included: #{output_path} matches generated batteries-included state"
        )

        :ok

      {:ok, _existing} ->
        Mix.raise(
          "relyra.batteries_included drift detected for #{output_path}; rerun mix relyra.batteries_included"
        )

      {:error, :enoent} ->
        Mix.raise("relyra.batteries_included --check target is missing: #{output_path}")

      {:error, reason} ->
        Mix.raise(
          "relyra.batteries_included could not read #{output_path}: #{:file.format_error(reason)}"
        )
    end
  end

  defp render_report do
    providers =
      [:okta, :entra, :google_workspace]
      |> Enum.map(&Relyra.Provider.fetch!/1)

    [
      "# Batteries Included Proof",
      "",
      "Generated from the shipped installer, test-support seam, provider registry, and focused proof commands in this repository.",
      "",
      "## Rerun Commands",
      "",
      "- `mix ci.docs`",
      "- `mix relyra.batteries_included --check`",
      "- `mix test #{@install_test} #{@demo_test} --warnings-as-errors`",
      "",
      "## Supported Provider Scope",
      "",
      "- First-class batteries-included providers: #{provider_name_list(providers)}",
      "- Runbooks: #{provider_guide_list(providers)}",
      "",
      "## Claim To Proof Map",
      "",
      claim_table(providers),
      ""
    ]
    |> Enum.join("\n")
  end

  defp claim_table(providers) do
    provider_scope = provider_name_list(providers)

    [
      "| claim | executable state | seam | proof command | artifact |",
      "| --- | --- | --- | --- | --- |",
      "| install path is blessed and reproducible | `mix relyra.install` scaffolds the host integration surface and optional LiveAdmin contract | `Mix.Tasks.Relyra.Install.run/1` | `mix test #{@install_test} --warnings-as-errors` | `#{@install_test}` |",
      "| local-first proof starts with FakeIdP | a tiny host-side ACS flow succeeds before any real IdP setup | `Relyra.TestSupport` + `Relyra.TestSupport.FakeIdP` | `mix test #{@demo_test} --warnings-as-errors` | `#{@demo_test}` |",
      "| supported provider scope stays narrow | first-class scope is limited to #{provider_scope} | `Relyra.Provider.list/0` | `mix test #{@task_test} --warnings-as-errors` | `guides/recipes/okta.md`, `guides/recipes/entra.md`, `guides/recipes/google_workspace.md` |",
      "| provider runbooks stay tied to repo reality | Day-1 routing points to three authoritative runbooks and no broader preset catalog | `guides/getting_started.md` + `Relyra.Provider.guide_url/1` | `mix test #{@task_test} --warnings-as-errors` | `guides/getting_started.md` |",
      "| optional admin remains a later receipt | LiveAdmin is optional and the installer can scaffold its host-side scope contract | `Relyra.LiveAdmin.ScopeProvider` | `mix test #{@install_test} --warnings-as-errors` | `#{@install_test}` |",
      "| metadata and certificate lifecycle stay observable | metadata refresh and certificate transitions have focused proof lanes and operator-facing docs | `Relyra.Metadata.AutoRefresh` + `Relyra.Ecto.CertificateInventory` | `mix test test/relyra/metadata/auto_refresh_test.exs test/relyra/ecto/certificate_inventory_transition_test.exs --warnings-as-errors` | `guides/case_studies/operator_managed_rollout.md` |",
      "| audit and telemetry are explicit follow-ons | operator receipts include audit evidence and telemetry-facing proof seams | `Relyra.Ecto.AuditWriter` + `Relyra.Telemetry` | `mix test test/relyra/ecto/audit_hardening_test.exs test/relyra/telemetry_test.exs --warnings-as-errors` | `guides/batteries_included.md` |",
      "| scheduled refresh is not a marketing claim | background refresh remains backed by focused tests and explicit operator review posture | `Relyra.Metadata.AutoRefresh` + `Relyra.Workers.MetadataRefresh` | `mix test test/relyra/metadata/scheduler_test.exs test/relyra/workers/metadata_refresh_test.exs --warnings-as-errors` | `guides/batteries_included.md` |",
      "| diagnostic bundle support is real and bounded | diagnostic export exists as a library-owned surface with controller and allow-list coverage | `Relyra.Diagnostic` + `Relyra.Diagnostic.AllowList` | `mix test test/phoenix/diagnostic_controller_test.exs test/relyra/diagnostic_test.exs test/relyra/diagnostic/allow_list_test.exs --warnings-as-errors` | `guides/batteries_included.md` |"
    ]
    |> Enum.join("\n")
  end

  defp provider_name_list(providers) do
    providers
    |> Enum.map(& &1.display_name())
    |> Enum.join(", ")
  end

  defp provider_guide_list(providers) do
    providers
    |> Enum.map(fn provider ->
      "#{provider.display_name()} (`#{local_guide_path(provider.id())}`)"
    end)
    |> Enum.join("; ")
  end

  defp local_guide_path(:okta), do: "guides/recipes/okta.md"
  defp local_guide_path(:entra), do: "guides/recipes/entra.md"
  defp local_guide_path(:google_workspace), do: "guides/recipes/google_workspace.md"
end