lib/mobus/stepwise/error.ex

defmodule Mobus.Stepwise.Error do
  @moduledoc """
  Structured error metadata for capability and engine errors.

  Provides richer error classification than raw reason terms,
  enabling orchestrators to make policy decisions (retry, skip,
  escalate) without string-parsing error reasons.

  ## Usage

  Capabilities can return structured errors:

      {:error, %Mobus.Stepwise.Error{
        type: :validation,
        source: :capability,
        reason: "email already exists",
        recoverable?: true,
        retryable?: false
      }}

  The engine passes these through transparently. Orchestrators
  can pattern-match on the struct:

      case Result.error_reason(result) do
        %Error{retryable?: true} -> schedule_retry(...)
        %Error{recoverable?: false} -> escalate(...)
        raw_reason -> handle_legacy(raw_reason)
      end
  """

  @enforce_keys [:type, :reason]
  defstruct [
    :type,
    :reason,
    :source,
    :detail,
    recoverable?: true,
    retryable?: false
  ]

  @type t :: %__MODULE__{
          type: atom(),
          reason: term(),
          source: atom() | nil,
          detail: term() | nil,
          recoverable?: boolean(),
          retryable?: boolean()
        }

  @doc "Creates a new structured error."
  @spec new(atom(), term(), keyword()) :: t()
  def new(type, reason, opts \\ []) do
    %__MODULE__{
      type: type,
      reason: reason,
      source: Keyword.get(opts, :source),
      detail: Keyword.get(opts, :detail),
      recoverable?: Keyword.get(opts, :recoverable?, true),
      retryable?: Keyword.get(opts, :retryable?, false)
    }
  end

  @doc "Returns `true` if the value is a `%Mobus.Stepwise.Error{}` struct."
  @spec structured?(term()) :: boolean()
  def structured?(%__MODULE__{}), do: true
  def structured?(_), do: false
end