defmodule VavPipeline.SanhedrinConsult do @moduledoc """ What a judge consults (Architecture §3.2: "consults the specification (L0/L1)"). Registered in the `sanhedrin` application env as `{__MODULE__, :consult}` at pipeline start; `judge.erl` calls it once per vote with the step its seat performs — `:interpret`, `:challenge` or `:resolve` — and the case's roots and question. Tablet 55: before this the judge chose among six canned positions by a hash of the clock. Now the only ground a vote can stand on is the Talmudic Engine's answer over the case's roots — the same engine, the same rules, the same verses the bedikah and the Bereishit cycle use. When the engine's socket is not running, or answers with an error, the judge is told `{:error, reason}` and abstains. Nothing here is answered from a stub (tablet 41): `TalmudicEngineClient`'s stub fallbacks are deliberately NOT used — a court that accepts on a stub is the calf. """ require Logger alias VavPipeline.TalmudicEngineSocket @books ~w(bereishit shemot vayikra bamidbar devarim)a @typedoc "What the judge receives: a status the engine gave, the reading, its verses." @type consultation :: %{ required(:status) => String.t(), required(:text) => String.t(), required(:verses) => [{atom(), pos_integer(), pos_integer()}], optional(:kushya) => String.t(), optional(:grounding) => [String.t()] } @doc """ Called from the judge (Erlang): `consult(Step, CaseRef, Roots, Question, Context)`. Roots are the case's shoresh names as the cycle produces them; the Erlang side hands binaries or charlists — both are accepted. """ @spec consult(:interpret | :challenge | :resolve, binary() | charlist(), list(), binary() | charlist(), map()) :: {:ok, consultation()} | {:error, term()} def consult(step, case_ref, roots, question, _context) when step in [:interpret, :challenge, :resolve] do if Process.whereis(TalmudicEngineSocket) do ctx = %{ context_roots: Enum.map(roots, &to_string/1), question: to_string(question) } ref = "#{to_string(case_ref)}-#{step}" case apply(TalmudicEngineSocket, step, [ref, ctx]) do {:ok, %{"status" => status} = reply} when is_binary(status) -> {:ok, normalize(status, reply)} {:ok, other} -> {:error, {:unexpected_reply, other}} {:error, reason} -> Logger.warning("SanhedrinConsult: engine #{step} failed for #{ref}: #{inspect(reason)}") {:error, {:engine_unavailable, reason}} end else {:error, :engine_socket_not_started} end end def consult(step, _ref, _roots, _q, _ctx), do: {:error, {:unknown_step, step}} @doc "Parse the engine's verse strings (`verse_ref(bereishit,1,1,...)`, `verse(shemot,3,14)`) into `{book, ch, vs}`." @spec verses_from([String.t()]) :: [{atom(), pos_integer(), pos_integer()}] def verses_from(strings) when is_list(strings) do strings |> Enum.flat_map(fn s -> case Regex.run(~r/verse(?:_ref)?\((bereishit|shemot|vayikra|bamidbar|devarim)\s*,\s*(\d+)\s*,\s*(\d+)/, to_string(s)) do [_, book, ch, vs] -> [{String.to_existing_atom(book), String.to_integer(ch), String.to_integer(vs)}] _ -> [] end end) |> Enum.uniq() end def verses_from(_), do: [] @doc false def books, do: @books defp normalize(status, reply) do interp = Map.get(reply, "interpretation") || %{} chall = Map.get(reply, "challenge") || %{} base = %{ status: status, text: to_string(Map.get(interp, "text", "")), verses: verses_from(List.wrap(Map.get(interp, "verses", []))), grounding: List.wrap(Map.get(interp, "grounding", [])) |> Enum.map(&to_string/1) } case Map.get(chall, "kushya") do k when is_binary(k) and k != "" -> Map.put(base, :kushya, k) _ -> base end end end