Skip to main content

lib/jido/runic/signal_fact.ex

defmodule Jido.Runic.SignalFact do
  @moduledoc """
  Bidirectional adapter between Jido Signals and Runic Facts.

  Maps Jido's causality tracking (signal.source, signal.jidocause) to
  Runic's fact ancestry chain, ensuring provenance is continuous across
  both systems.
  """

  alias Runic.Workflow.Fact

  @doc """
  Convert a Jido Signal (or plain map) into a Runic Fact.

  Options:
  * `:ancestry_mode` — `:hash` (default) to hash source/jidocause or `:raw` to preserve them.
  """
  @spec from_signal(Jido.Signal.t(), keyword()) :: Fact.t()
  def from_signal(signal, opts \\ [])

  def from_signal(%{data: data} = signal, opts) do
    ancestry = build_ancestry(signal, opts)
    Fact.new(value: data, ancestry: ancestry)
  end

  def from_signal(signal, opts) when is_map(signal) do
    data = Map.get(signal, :data, signal)
    ancestry = build_ancestry(signal, opts)
    Fact.new(value: data, ancestry: ancestry)
  end

  @doc """
  Convert a Runic Fact back into a Jido Signal.

  Options:
  * `:type` — signal type (default: `"runic.production"`).
  * `:source` — signal source path (default: `\"/runic/workflow\"`).
  * `:include_ancestry?` — when true and ancestry present, rehydrate `source`/`jidocause` from it.
  """
  @spec to_signal(Fact.t(), keyword()) :: Jido.Signal.t()
  def to_signal(%Fact{} = fact, opts \\ []) do
    type = Keyword.get(opts, :type, "runic.production")
    default_source = Keyword.get(opts, :source, "/runic/workflow")
    include_ancestry? = Keyword.get(opts, :include_ancestry?, false)

    {source, jidocause} =
      case {include_ancestry?, fact.ancestry} do
        {true, {src, cause}} -> {src || default_source, cause}
        _ -> {default_source, nil}
      end

    Jido.Signal.new!(type, fact.value, source: source, jidocause: jidocause)
  end

  defp build_ancestry(signal, opts) do
    mode = Keyword.get(opts, :ancestry_mode, :hash)
    source = Map.get(signal, :source)
    cause = Map.get(signal, :jidocause)

    cond do
      source && cause ->
        case mode do
          :raw -> {source, cause}
          _ -> {:erlang.phash2(source), :erlang.phash2(cause)}
        end

      source ->
        case mode do
          :raw -> {source, nil}
          _ -> {:erlang.phash2(source), nil}
        end

      true ->
        nil
    end
  end
end