From 4e0eb2f983a8296627f03b9bde24847582928a33 Mon Sep 17 00:00:00 2001 From: Scott Karp Date: Mon, 19 Oct 2026 19:02:51 +0000 Subject: [PATCH] cycle: the engine is consulted on every resolved question MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../lib/vav_pipeline/bereishit_cycle.ex | 130 ++++++++++++++++-- 1 file changed, 120 insertions(+), 10 deletions(-) diff --git a/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex b/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex index d56b8d85..5c61f2c9 100644 --- a/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex +++ b/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex @@ -63,8 +63,8 @@ defmodule VavPipeline.BereishitCycle do """ alias VavPipeline.{Specification, CycleLog, Manna, HaltConditions, - ShoreshEngineClient, TalmudicEngineClient, SanhedrinClient, - Chatzotzrot} + ShoreshEngineClient, TalmudicEngineClient, TalmudicEngineSocket, + SanhedrinClient, Chatzotzrot} require Logger @@ -308,7 +308,11 @@ defmodule VavPipeline.BereishitCycle do final = case result do {: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} -> case route_if_unresolved(data, context) do @@ -325,6 +329,90 @@ defmodule VavPipeline.BereishitCycle do final 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 # ============================================================ @@ -385,15 +473,19 @@ defmodule VavPipeline.BereishitCycle do deliberation = case Map.get(data, :resolution) do :talmudic -> - text = get_in(data, [:talmudic_interpretation, "interpretation", "text"]) || "" - " Talmudic Engine: #{text}" + " " <> (engine_reading_text(data) || "Talmudic Engine resolved the case.") :sanhedrin -> pos = get_in(data, [:sanhedrin_decision, :majority, :position]) || "" " 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 "Roots activated in #{verse.ref}: #{Enum.join(root_lines, "; ")}." <> @@ -426,10 +518,19 @@ defmodule VavPipeline.BereishitCycle do 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 = 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 [primary | talmudic_verses] @@ -604,13 +705,22 @@ defmodule VavPipeline.BereishitCycle do {: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 = 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, vote_count_for: 1, vote_count_against: 0, - processing_component: "bereishit_cycle" + processing_component: if(reading, do: "bereishit_cycle+talmudic_engine", else: "bereishit_cycle") }) {:interpretation, attrs, nil}