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>
371 lines
11 KiB
Elixir
371 lines
11 KiB
Elixir
defmodule VavPipeline.BereishitCycle do
|
|
@moduledoc """
|
|
The Bereishit processing cycle — the six days of creation as computation.
|
|
|
|
Architecture §3.3:
|
|
Each step chains to the next via |> (vav-consecutive).
|
|
|
|
The cycle operates WITHIN the ShemaContext, never modifying it.
|
|
The context was received by Shema (nishma). Now we do (na'aseh).
|
|
|
|
Steps:
|
|
1. tohu — decompose input into undifferentiated state
|
|
2. vayomer — produce structured symbolic output (Shoresh Engine)
|
|
3. havdalah — separate into distinguished categories
|
|
4. kriah — assign stable names
|
|
5. ki_tov — evaluate against specification
|
|
6. erev_voker — log cycle completion, enforce boundary
|
|
|
|
Latency budget (Construction spec §2.5): 5000ms total
|
|
"""
|
|
|
|
alias VavPipeline.{Specification, CycleLog, Manna,
|
|
ShoreshEngineClient, TalmudicEngineClient, SanhedrinClient}
|
|
|
|
require Logger
|
|
|
|
@type cycle_result ::
|
|
{:ok, map()}
|
|
| {:rejected, String.t()}
|
|
| {:unresolved, map()}
|
|
| {:halted, atom(), String.t()}
|
|
|
|
@doc """
|
|
Execute the Bereishit cycle on input within received context.
|
|
"""
|
|
@spec execute(VavPipeline.input(), VavPipeline.Shema.shema_context()) :: cycle_result()
|
|
def execute(input, context) do
|
|
started_at = System.monotonic_time(:millisecond)
|
|
|
|
result =
|
|
input
|
|
|> tohu(context)
|
|
|> vayomer(context)
|
|
|> havdalah(context)
|
|
|> kriah(context)
|
|
|> ki_tov(context)
|
|
|
|
completed_at = System.monotonic_time(:millisecond)
|
|
elapsed = completed_at - started_at
|
|
|
|
Logger.info("BereishitCycle: completed in #{elapsed}ms — #{inspect(elem(result, 0))}")
|
|
|
|
# erev_voker — cycle boundary
|
|
erev_voker(result, context)
|
|
end
|
|
|
|
@doc """
|
|
Tohu — decompose input into undifferentiated state.
|
|
|
|
Strip structure, extract raw content. This is the formless void
|
|
before vayomer brings order.
|
|
"""
|
|
@spec tohu(VavPipeline.input(), VavPipeline.Shema.shema_context()) :: map()
|
|
def tohu(input, _context) do
|
|
%{
|
|
raw: extract_content(input),
|
|
provenance: Map.get(input, :source, "unknown"),
|
|
scope: Map.get(input, :verse_scope, %{}),
|
|
started_at: DateTime.utc_now()
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Vayomer — produce structured symbolic output.
|
|
|
|
The Shoresh Engine receives the ShemaContext — it reads te'amim,
|
|
ketiv/qere, gematria as part of its analysis.
|
|
|
|
In Phase 2, this calls the Shoresh Engine via port/NIF.
|
|
Until the port is built (Step 14), this uses the L1 data
|
|
directly from the ShemaContext.
|
|
"""
|
|
@spec vayomer(map(), VavPipeline.Shema.shema_context()) :: map()
|
|
def vayomer(state, context) do
|
|
# 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
|
|
})
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("BereishitCycle.vayomer: engine error (#{reason}), using L1 fallback")
|
|
vayomer_fallback(state, context)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Havdalah — separate into distinguished categories.
|
|
|
|
Te'amim inform the separation — the text's own divisions guide havdalah.
|
|
Uses the te'amim marks from the ShemaContext to determine boundaries.
|
|
"""
|
|
@spec havdalah(map(), VavPipeline.Shema.shema_context()) :: map()
|
|
def havdalah(utterance, context) do
|
|
# Group roots by domain based on te'amim divisions
|
|
# Te'amim melekh marks (disjunctors) create boundaries
|
|
melekh_positions =
|
|
context.teamim_marks
|
|
|> Enum.filter(fn m -> m.role == "melekh" end)
|
|
|> Enum.map(fn m -> m.word_index end)
|
|
|> Enum.sort()
|
|
|
|
# Separate roots into groups divided by melekh marks
|
|
roots = Map.get(utterance, :roots_activated, [])
|
|
groups = separate_by_boundaries(roots, melekh_positions)
|
|
|
|
Map.merge(utterance, %{
|
|
separated_groups: groups,
|
|
separation_boundaries: melekh_positions,
|
|
stage: :havdalah
|
|
})
|
|
end
|
|
|
|
@doc """
|
|
Kriah — assign stable names to each distinction.
|
|
|
|
Each separated group receives a name based on its primary root.
|
|
"""
|
|
@spec kriah(map(), VavPipeline.Shema.shema_context()) :: map()
|
|
def kriah(distinctions, _context) do
|
|
named =
|
|
distinctions
|
|
|> Map.get(:separated_groups, [])
|
|
|> Enum.with_index()
|
|
|> Enum.map(fn {group, idx} ->
|
|
primary_root = List.first(group)
|
|
|
|
name =
|
|
if primary_root do
|
|
primary_root.shoresh
|
|
else
|
|
"group_#{idx}"
|
|
end
|
|
|
|
%{
|
|
name: name,
|
|
roots: group,
|
|
index: idx
|
|
}
|
|
end)
|
|
|
|
Map.merge(distinctions, %{
|
|
named_results: named,
|
|
stage: :kriah
|
|
})
|
|
end
|
|
|
|
@doc """
|
|
Ki Tov — evaluate against specification.
|
|
|
|
Ketiv/qere positions are checked — both readings inform evaluation.
|
|
Returns {:ok, results}, {:rejected, reason}, or {:unresolved, results}.
|
|
"""
|
|
@spec ki_tov(map(), VavPipeline.Shema.shema_context()) :: VavPipeline.cycle_result()
|
|
def ki_tov(named, context) do
|
|
results = Map.get(named, :named_results, [])
|
|
|
|
case Specification.evaluate(results, context.ketiv_qere) do
|
|
:tov ->
|
|
{:ok,
|
|
Map.merge(named, %{
|
|
evaluation: :tov,
|
|
stage: :ki_tov
|
|
})}
|
|
|
|
{:lo_tov, reason} ->
|
|
{:rejected, reason}
|
|
|
|
:teiku ->
|
|
{:unresolved,
|
|
Map.merge(named, %{
|
|
evaluation: :teiku,
|
|
stage: :ki_tov
|
|
})}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Erev Voker — log cycle completion, enforce boundary.
|
|
|
|
Release any manna surplus — do not hoard into next cycle.
|
|
"""
|
|
@spec erev_voker(VavPipeline.cycle_result(), VavPipeline.Shema.shema_context()) ::
|
|
VavPipeline.cycle_result()
|
|
def erev_voker(result, context) do
|
|
# Log the cycle
|
|
phase =
|
|
case result do
|
|
{:ok, _} -> :voker
|
|
{:rejected, _} -> :erev
|
|
{:unresolved, _} -> :erev
|
|
_ -> :erev
|
|
end
|
|
|
|
CycleLog.record(result, phase)
|
|
|
|
# Enforce anti-hoarding
|
|
Manna.enforce_anti_hoarding()
|
|
|
|
# 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
|
|
|
|
# Separate roots into groups based on melekh boundary positions
|
|
defp separate_by_boundaries([], _boundaries), do: [[]]
|
|
defp separate_by_boundaries(roots, []), do: [roots]
|
|
|
|
defp separate_by_boundaries(roots, boundaries) do
|
|
# Sort boundaries and use them to split the root list
|
|
sorted_boundaries = Enum.sort(boundaries)
|
|
|
|
{groups, current} =
|
|
Enum.reduce(roots, {[], []}, fn root, {groups, current} ->
|
|
pos = root.position - 1
|
|
|
|
if pos in sorted_boundaries and current != [] do
|
|
# Boundary hit — close current group, start new one with this root
|
|
{groups ++ [current], [root]}
|
|
else
|
|
{groups, current ++ [root]}
|
|
end
|
|
end)
|
|
|
|
# Don't forget the last group
|
|
groups ++ [current]
|
|
end
|
|
end
|