Files
Scott KarpandClaude Opus 4.6 c67672777b Step 14: Connect components — chamishim karsay zahav
Three connections built and tested:
- Haskell Shoresh Engine via Erlang Port (JSON lines over stdin/stdout)
- Prolog Talmudic Engine via Unix domain socket (JSON over /tmp/shorashim/talmudic.sock)
- Erlang Sanhedrin via native BEAM calls

New: Server.hs (JSON-line server loop), json_bridge.pl + socket_server.pl
(Prolog socket interface), ShoreshEnginePort + TalmudicEngineSocket +
SanhedrinClient (Elixir GenServer wrappers). BereishitCycle now routes
unresolved cases through Talmudic Engine and Sanhedrin. All connections
gracefully degrade — pipeline falls back to L1 data when engines unavailable.

46 Elixir tests, 33 Haskell tests, 57 Prolog tests — 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:39:21 -04:00

142 lines
4.3 KiB
Elixir

defmodule VavPipeline.SanhedrinClient do
@moduledoc """
Client interface to the Erlang Sanhedrin deliberation engine.
Construction spec §3.2: Native BEAM message passing.
Protocol: Erlang records via escalation:submit_case/1.
The Sanhedrin runs in the same BEAM VM. This module translates
Elixir maps into Erlang record tuples and calls the Sanhedrin
API directly.
"""
require Logger
@doc """
Submit a case for deliberation.
Construction spec §3.2: submit_case with case_ref, complexity,
question, context, requested_by.
Returns {:ok, decision_map} or {:error, reason}.
"""
@spec submit_case(map()) :: {:ok, map()} | {:error, String.t()}
def submit_case(case_params) do
case_ref = Map.get(case_params, :case_ref, generate_ref())
complexity = Map.get(case_params, :complexity, :simple)
question = Map.get(case_params, :question, "")
context = Map.get(case_params, :context, %{})
requested_by = Map.get(case_params, :requested_by, "pipeline")
# Build Erlang record tuple matching #sanhedrin_case{}
# Record: {sanhedrin_case, case_ref, complexity, question, context,
# requested_by, submitted_at}
erlang_case =
{:sanhedrin_case, case_ref, complexity, question, context,
requested_by, :erlang.system_time(:millisecond)}
try do
case :escalation.submit_case(erlang_case) do
{:ok, decision_ref} ->
# Decision is stored asynchronously by the panel.
# Wait briefly for the result, then fetch from clerk.
wait_for_decision(decision_ref, case_ref)
{:error, {:halted, halt_type, reason}} ->
{:error, "Halted (#{halt_type}): #{reason}"}
{:error, reason} ->
{:error, "Submission failed: #{inspect(reason)}"}
end
catch
:exit, {:noproc, _} ->
Logger.warning("SanhedrinClient: Sanhedrin not running, returning teiku")
{:ok, stub_teiku(case_ref)}
kind, reason ->
Logger.error("SanhedrinClient: #{kind} — #{inspect(reason)}")
{:error, "Sanhedrin call failed: #{inspect(reason)}"}
end
end
@doc """
Get a previously stored decision from the clerk.
"""
@spec get_decision(binary()) :: {:ok, map()} | {:error, :not_found}
def get_decision(case_ref) do
try do
case :clerk.get_decision(case_ref) do
{:ok, decision} ->
{:ok, decision_to_map(decision)}
:not_found ->
{:error, :not_found}
end
catch
:exit, {:noproc, _} ->
{:error, :not_found}
end
end
# --- Private ---
defp wait_for_decision(decision_ref, case_ref) do
# The panel deliberates asynchronously. Poll clerk briefly.
wait_for_decision(decision_ref, case_ref, 10, 500)
end
defp wait_for_decision(_decision_ref, case_ref, 0, _interval) do
# Timeout — return teiku (unresolved)
Logger.warning("SanhedrinClient: deliberation timeout for #{case_ref}")
{:ok, stub_teiku(case_ref)}
end
defp wait_for_decision(decision_ref, case_ref, retries, interval) do
Process.sleep(interval)
case get_decision(case_ref) do
{:ok, decision} ->
{:ok, decision}
{:error, :not_found} ->
wait_for_decision(decision_ref, case_ref, retries - 1, interval)
end
end
defp decision_to_map({:decision, case_ref, outcome, majority, minority, panel_size,
delib_time, unanimous_check}) do
%{
case_ref: case_ref,
outcome: outcome,
majority: majority,
minority: minority,
panel_size: panel_size,
deliberation_time_ms: delib_time,
unanimous_check: unanimous_check
}
end
defp decision_to_map(other), do: other
defp stub_teiku(case_ref) do
%{
case_ref: case_ref,
outcome: :teiku,
majority: %{position: "Sanhedrin not available", reasoning: "Engine not running", torah_refs: [], vote_count: 0},
minority: %{position: "", reasoning: "", torah_refs: [], vote_count: 0},
panel_size: 0,
deliberation_time_ms: 0,
unanimous_check: :passed
}
end
defp generate_ref do
:crypto.strong_rand_bytes(16)
|> Base.encode16(case: :lower)
|> then(fn hex ->
<<a::binary-size(8), b::binary-size(4), c::binary-size(4), d::binary-size(4), e::binary-size(12)>> = hex
"#{a}-#{b}-#{c}-#{d}-#{e}"
end)
end
end