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>
This commit is contained in:
Scott KarpandClaude Opus 4.6 committed 2026-03-12 17:39:21 -04:00
1 parent f8f70c77e5
commit c67672777b
18 files changed
+1435 -121

No files matched your search

+141 -23
View File
@@ -19,7 +19,8 @@ defmodule VavPipeline.BereishitCycle do
Latency budget (Construction spec §2.5): 5000ms total
"""
alias VavPipeline.{Specification, CycleLog, Manna}
alias VavPipeline.{Specification, CycleLog, Manna,
ShoreshEngineClient, TalmudicEngineClient, SanhedrinClient}
require Logger
@@ -81,27 +82,22 @@ defmodule VavPipeline.BereishitCycle do
"""
@spec vayomer(map(), VavPipeline.Shema.shema_context()) :: map()
def vayomer(state, context) do
# In Phase 2: use L1 data from ShemaContext directly
# In Phase 3: call ShoreshEngineClient.process(state.raw, context)
roots =
context.verse.words
|> Enum.filter(fn w -> w.shoresh != nil end)
|> Enum.map(fn w ->
%{
shoresh: w.shoresh,
word: w.consonants,
position: w.position,
morph: w.morph,
gematria: Map.get(context.root_gematria, w.shoresh, 0)
}
end)
# Step 14: Call Shoresh Engine via port. Falls back to L1 data.
case ShoreshEngineClient.process(state.raw, context) do
{:ok, engine_result} ->
Map.merge(state, %{
roots_activated: get_result(engine_result, :roots_activated, "roots_activated", []),
parse_tree: get_result(engine_result, :parse_tree, "parse_tree", nil),
dual_readings: get_result(engine_result, :dual_readings, "dual_readings", []),
teamim_parse: context.teamim_marks,
ketiv_qere: context.ketiv_qere,
stage: :vayomer
})
Map.merge(state, %{
roots_activated: roots,
teamim_parse: context.teamim_marks,
ketiv_qere: context.ketiv_qere,
stage: :vayomer
})
{:error, reason} ->
Logger.warning("BereishitCycle.vayomer: engine error (#{reason}), using L1 fallback")
vayomer_fallback(state, context)
end
end
@doc """
@@ -202,7 +198,7 @@ defmodule VavPipeline.BereishitCycle do
"""
@spec erev_voker(VavPipeline.cycle_result(), VavPipeline.Shema.shema_context()) ::
VavPipeline.cycle_result()
def erev_voker(result, _context) do
def erev_voker(result, context) do
# Log the cycle
phase =
case result do
@@ -217,11 +213,133 @@ defmodule VavPipeline.BereishitCycle do
# Enforce anti-hoarding
Manna.enforce_anti_hoarding()
result
# Step 14: Route unresolved cases to Talmudic Engine / Sanhedrin
case result do
{:unresolved, data} ->
route_if_unresolved(data, context)
_ ->
result
end
end
# --- Routing ---
@doc false
defp route_if_unresolved(data, context) do
# Extract roots for the Talmudic Engine query
roots = Map.get(data, :roots_activated, [])
context_roots = Enum.map(roots, fn
%{shoresh: s} when is_binary(s) -> s
%{"shoresh" => s} when is_list(s) -> Enum.join(s, "-")
_ -> "unknown"
end)
case_ref = "cycle-#{:erlang.system_time(:millisecond)}"
talmudic_context = %{
context_roots: context_roots,
question: "Interpret roots #{inspect(context_roots)} in context"
}
# First: consult the Talmudic Engine
case TalmudicEngineClient.interpret(case_ref, talmudic_context) do
{:ok, %{"status" => "resolved"} = interpretation} ->
Logger.info("BereishitCycle: Talmudic Engine resolved case #{case_ref}")
{:ok, Map.merge(data, %{
talmudic_interpretation: interpretation,
resolution: :talmudic,
stage: :erev_voker
})}
{:ok, %{"status" => "teiku"}} ->
# Still unresolved — escalate to Sanhedrin
Logger.info("BereishitCycle: Talmudic Engine returned teiku, escalating to Sanhedrin")
escalate_to_sanhedrin(data, context, case_ref, context_roots)
{:ok, interpretation} ->
# Challenged or other status — include and return
{:unresolved, Map.merge(data, %{
talmudic_interpretation: interpretation,
stage: :erev_voker
})}
{:error, reason} ->
Logger.warning("BereishitCycle: Talmudic routing failed: #{reason}")
{:unresolved, Map.put(data, :routing_error, reason)}
end
end
defp escalate_to_sanhedrin(data, _context, case_ref, context_roots) do
sanhedrin_params = %{
case_ref: case_ref,
complexity: :simple,
question: "Deliberate on unresolved interpretation of #{inspect(context_roots)}",
context: %{
roots: context_roots,
named_results: Map.get(data, :named_results, [])
},
requested_by: "bereishit_cycle"
}
case SanhedrinClient.submit_case(sanhedrin_params) do
{:ok, decision} ->
outcome = Map.get(decision, :outcome, :teiku)
Logger.info("BereishitCycle: Sanhedrin decided #{outcome} for #{case_ref}")
case outcome do
:accepted ->
{:ok, Map.merge(data, %{
sanhedrin_decision: decision,
resolution: :sanhedrin,
stage: :erev_voker
})}
_ ->
{:unresolved, Map.merge(data, %{
sanhedrin_decision: decision,
stage: :erev_voker
})}
end
{:error, reason} ->
Logger.warning("BereishitCycle: Sanhedrin escalation failed: #{reason}")
{:unresolved, Map.put(data, :escalation_error, reason)}
end
end
# --- L1 Fallback (when port is unavailable) ---
defp vayomer_fallback(state, context) do
roots =
context.verse.words
|> Enum.filter(fn w -> w.shoresh != nil end)
|> Enum.map(fn w ->
%{
shoresh: w.shoresh,
word: w.consonants,
position: w.position,
morph: w.morph,
gematria: Map.get(context.root_gematria, w.shoresh, 0)
}
end)
Map.merge(state, %{
roots_activated: roots,
teamim_parse: context.teamim_marks,
ketiv_qere: context.ketiv_qere,
stage: :vayomer
})
end
# --- Private helpers ---
# Get a value from a map that may have atom or string keys
# (atom keys from L1 fallback, string keys from JSON engine response)
defp get_result(map, atom_key, string_key, default) do
Map.get(map, atom_key, Map.get(map, string_key, default))
end
defp extract_content(input) do
Map.get(input, :content, "") |> String.trim()
end