defmodule Mobus.Stepwise.SpecHelpers do @moduledoc """ Shared pure functions for inspecting stepwise workflow specs. Extracted from `StepwiseAdvance`, `StepwiseProjection`, and `FsmBreakpoint` to eliminate internal duplication. Used by pipeline components in the foundation; workflow_stem inherits these via defdelegate. All functions are pure (no side effects, no process dependency) so they compose safely in ALF pipeline stages. """ @doc """ Returns the ordered list of component modules in the canonical stepwise pipeline. Both `Mobus.Stepwise.Pipeline.Stepwise` and `WorkflowStem.Pipelines.Stepwise` derive their `@components` from this function, guaranteeing a single source of truth for the pipeline stage order. Order matters — changing this list changes the pipeline for ALL consumers. """ @spec pipeline_stage_modules() :: [module()] def pipeline_stage_modules do [ Mobus.Stepwise.Components.StepwiseContextMerge, Mobus.Stepwise.Components.StepwiseAction, Mobus.Stepwise.Components.StepwiseAdvance, Mobus.Stepwise.Components.StepwiseEntryAction, Mobus.Stepwise.Components.FsmBreakpoint, Mobus.Stepwise.Components.StepwiseProjection ] end @doc """ Returns `true` when two state identifiers refer to the same state. Compares atoms directly, binaries directly, and cross-compares atom/binary via `Atom.to_string/1`. ## Examples iex> equivalent_state?(:greeting, :greeting) true iex> equivalent_state?(:greeting, "greeting") true iex> equivalent_state?(:greeting, :problem_solving) false """ @spec equivalent_state?(term(), term()) :: boolean() def equivalent_state?(a, b) when is_binary(a) and is_binary(b), do: a == b def equivalent_state?(a, b) when is_atom(a) and is_atom(b), do: a == b def equivalent_state?(a, b) when is_atom(a) and is_binary(b), do: Atom.to_string(a) == b def equivalent_state?(a, b) when is_binary(a) and is_atom(b), do: a == Atom.to_string(b) def equivalent_state?(_a, _b), do: false @doc """ Returns the ordered list of step identifiers for a workflow spec. Order is derived from: - `spec.steps` when present and non-empty (explicit order) - `spec.states` sorted by `step_number` (or `step`) ascending, otherwise insertion order Returns `[]` when the spec has no steps or states. ## Examples iex> ordered_steps(%{steps: [:a, :b, :c]}) [:a, :b, :c] """ @spec ordered_steps(map()) :: [atom() | String.t()] def ordered_steps(spec) do steps = Map.get(spec, :steps) || Map.get(spec, "steps") cond do is_list(steps) and steps != [] -> steps true -> states = Map.get(spec, :states) || %{} states |> Enum.map(fn {k, v} -> step_number = Map.get(v, :step_number) || Map.get(v, "step_number") || Map.get(v, :step) || Map.get(v, "step") || 0 {k, step_number} end) |> Enum.sort_by(fn {_k, n} -> n end) |> Enum.map(fn {k, _} -> k end) end end end