cycle: the engine is consulted on every resolved question

The Talmudic route ran only on a teiku from ki_tov, and ki_tov returns
teiku only for a root with no Hebrew — which the Shoresh Engine never
leaves. So the engine had never read a real question. Now every cycle
that resolves tov consults it with the roots it activated
(consult_engine/2, on the socket directly — never the client's stub;
an unreachable engine is reported "not consulted" and the cycle's
answer stands). A tamid probe does not consult.

The reading is said plainly (engine_reading_text/1): "Talmudic Engine
(resolved): 11 verse(s): Bereishit 1:1, …; 4 mitzvah/mitzvot:
know_hashem (1, positive), …" — the engine's own term form is no longer
pasted into a reader's answer. The verses join the sources as
references ("Bereishit 1:3", engine form beside), the L3 interpretation
carries them in torah_references with the reading in its reasoning and
processing_component "bereishit_cycle+talmudic_engine".

Proven against the engine running locally on its socket, bereishit 1:1
through VavPipeline.process/1: consulted :ok, 11 verses in the answer
and the sources, L3 torah_references 12; engine stopped → "not
consulted (Cannot connect)", answer stands, L3 component
bereishit_cycle; tamid probe → not consulted. Elixir 632/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-19 19:02:51 +00:00
1 parent ab6ca1ebcf
commit 4e0eb2f983
1 file changed
+120 -10
+120 -10
View File
@@ -63,8 +63,8 @@ defmodule VavPipeline.BereishitCycle do
""" """
alias VavPipeline.{Specification, CycleLog, Manna, HaltConditions, alias VavPipeline.{Specification, CycleLog, Manna, HaltConditions,
ShoreshEngineClient, TalmudicEngineClient, SanhedrinClient, ShoreshEngineClient, TalmudicEngineClient, TalmudicEngineSocket,
Chatzotzrot} SanhedrinClient, Chatzotzrot}
require Logger require Logger
@@ -308,7 +308,11 @@ defmodule VavPipeline.BereishitCycle do
final = final =
case result do case result do
{:ok, data} -> {:ok, data} ->
check_halt_on_output(data) # Every question is read against the engine, not only a teiku
# that never comes (2026-10-19: ki_tov returns teiku only on a
# root with no Hebrew, and the Shoresh Engine never leaves one;
# the Talmudic route had never been walked by a real question).
data |> consult_engine(context) |> check_halt_on_output()
{:unresolved, data} -> {:unresolved, data} ->
case route_if_unresolved(data, context) do case route_if_unresolved(data, context) do
@@ -325,6 +329,90 @@ defmodule VavPipeline.BereishitCycle do
final final
end end
# ============================================================
# The engine, consulted on every resolved cycle
# ============================================================
# Architecture §3.3: the Talmudic Engine grounds a reading in verses
# and mitzvot over the case's roots. The cycle's own ki_tov says the
# roots stand in L1; the engine says what stands on them. Called
# directly on the socket — never the client's stub (tablet 41): an
# engine that cannot be reached is reported as not consulted, and the
# cycle's answer stands without it. A tamid probe does not consult
# (the fire is checked, the engine is not loaded by the check).
defp consult_engine(%{probe: true} = data, _context), do: data
defp consult_engine(data, _context) do
roots = Map.get(data, :roots_activated, [])
names = roots |> Enum.map(&engine_root_name/1) |> Enum.reject(&is_nil/1)
case_ref = Map.get(data, :case_ref, generate_case_ref())
cond do
names == [] ->
Map.put(data, :talmudic_consulted, {:skipped, :no_roots})
Process.whereis(TalmudicEngineSocket) == nil ->
Map.put(data, :talmudic_consulted, {:error, :engine_socket_not_started})
true ->
case TalmudicEngineSocket.interpret(case_ref, %{context_roots: names, question: Map.get(data, :raw, "")}) do
{:ok, %{"status" => _} = reply} ->
Map.merge(data, %{talmudic_interpretation: reply, talmudic_consulted: :ok})
{:ok, other} ->
Logger.warning("BereishitCycle: engine answered an unexpected shape for #{case_ref}: #{inspect(other)}")
Map.put(data, :talmudic_consulted, {:error, :unexpected_reply})
{:error, reason} ->
Logger.warning("BereishitCycle: engine not consulted for #{case_ref}: #{inspect(reason)}")
Map.put(data, :talmudic_consulted, {:error, reason})
end
end
end
@doc """
The engine's reading, said plainly: how many verses and mitzvot it
coupled to the roots, and which. `nil` when there is no reading.
"""
@spec engine_reading_text(map()) :: String.t() | nil
def engine_reading_text(data) do
case Map.get(data, :talmudic_interpretation) do
%{"interpretation" => interp} = reply when is_map(interp) ->
verses = interp |> Map.get("verses", []) |> List.wrap() |> Enum.map(&verse_ref_display/1) |> Enum.reject(&is_nil/1) |> Enum.uniq()
mitzvot = interp |> Map.get("grounding", []) |> List.wrap() |> Enum.map(&mitzvah_display/1) |> Enum.reject(&is_nil/1) |> Enum.uniq()
status = Map.get(reply, "status", "")
cond do
verses == [] and mitzvot == [] ->
"Talmudic Engine (#{status}): no verse or mitzvah is coupled to these roots."
true ->
v = if verses == [], do: "no verse", else: "#{length(verses)} verse(s): #{Enum.join(verses, ", ")}"
m = if mitzvot == [], do: "no mitzvah", else: "#{length(mitzvot)} mitzvah/mitzvot: #{Enum.join(mitzvot, ", ")}"
"Talmudic Engine (#{status}): #{v}; #{m}."
end
_ ->
nil
end
end
# "verse_ref(bereishit,1,1,'...')" / "verse(shemot,3,14)" → "Bereishit 1:1"
defp verse_ref_display(s) do
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.capitalize(book)} #{ch}:#{vs}"
_ -> nil
end
end
# "mitzvah(1,positive,know_hashem,always)" → "know_hashem (1, positive)"
defp mitzvah_display(s) do
case Regex.run(~r/mitzvah\((\d+)\s*,\s*(\w+)\s*,\s*(\w+)/, to_string(s)) do
[_, id, kind, name] -> "#{name} (#{id}, #{kind})"
_ -> nil
end
end
# ============================================================ # ============================================================
# Compose — the answer and its sources, from the context # Compose — the answer and its sources, from the context
# ============================================================ # ============================================================
@@ -385,15 +473,19 @@ defmodule VavPipeline.BereishitCycle do
deliberation = deliberation =
case Map.get(data, :resolution) do case Map.get(data, :resolution) do
:talmudic -> :talmudic ->
text = get_in(data, [:talmudic_interpretation, "interpretation", "text"]) || "" " " <> (engine_reading_text(data) || "Talmudic Engine resolved the case.")
" Talmudic Engine: #{text}"
:sanhedrin -> :sanhedrin ->
pos = get_in(data, [:sanhedrin_decision, :majority, :position]) || "" pos = get_in(data, [:sanhedrin_decision, :majority, :position]) || ""
" Sanhedrin majority: #{pos}" " Sanhedrin majority: #{pos}"
_ -> _ ->
"" # Consulted after ki_tov (every resolved question), or not.
case {engine_reading_text(data), Map.get(data, :talmudic_consulted)} do
{text, _} when is_binary(text) -> " " <> text
{nil, {:error, reason}} -> " Talmudic Engine: not consulted (#{inspect(reason)})."
_ -> ""
end
end end
"Roots activated in #{verse.ref}: #{Enum.join(root_lines, "; ")}." <> "Roots activated in #{verse.ref}: #{Enum.join(root_lines, "; ")}." <>
@@ -426,10 +518,19 @@ defmodule VavPipeline.BereishitCycle do
text: verse.text_hebrew text: verse.text_hebrew
} }
# The engine's verses as references a reader can follow ("Bereishit
# 1:3"), the engine's own form kept beside them; the scope verse is
# not repeated.
talmudic_verses = talmudic_verses =
case get_in(data, [:talmudic_interpretation, "interpretation", "verses"]) do case get_in(data, [:talmudic_interpretation, "interpretation", "verses"]) do
vs when is_list(vs) -> Enum.map(vs, fn v -> %{ref: to_string(v)} end) vs when is_list(vs) ->
_ -> [] vs
|> Enum.map(fn v -> %{ref: verse_ref_display(v) || to_string(v), engine_form: to_string(v)} end)
|> Enum.uniq_by(& &1.ref)
|> Enum.reject(&(&1.ref == verse.ref))
_ ->
[]
end end
[primary | talmudic_verses] [primary | talmudic_verses]
@@ -604,13 +705,22 @@ defmodule VavPipeline.BereishitCycle do
{:interpretation, attrs, nil} {:interpretation, attrs, nil}
_ -> _ ->
# The engine's reading, when it was consulted, rides in the record:
# its verses beside the scope, its text in the reasoning, and the
# component names both readers.
interp = get_in(data, [:talmudic_interpretation, "interpretation"]) || %{}
engine_verses = interp |> Map.get("verses", []) |> to_string_list()
reading = engine_reading_text(data)
attrs = attrs =
Map.merge(base, %{ Map.merge(base, %{
reasoning: single_judge_reasoning(data, context), reasoning:
if(reading, do: single_judge_reasoning(data, context) <> " " <> reading, else: single_judge_reasoning(data, context)),
torah_references: Enum.uniq([scope_ref | engine_verses]),
panel_size: 1, panel_size: 1,
vote_count_for: 1, vote_count_for: 1,
vote_count_against: 0, vote_count_against: 0,
processing_component: "bereishit_cycle" processing_component: if(reading, do: "bereishit_cycle+talmudic_engine", else: "bereishit_cycle")
}) })
{:interpretation, attrs, nil} {:interpretation, attrs, nil}