cycle: a named verse is read from the verse, not from the reference (tablet 32)

`devarim 6:5` on the live pod: 422 TEIKU_NO_ROOTS. Tablet 30 heard the
scope, but the Shoresh Engine activates roots from the text it is
handed, vayomer handed it the question, and a bare reference carries no
Hebrew. The 422's own guidance said "name a verse".

tablet30_scope_test asserted an explicit reference was honoured and
passed — on the L1 fallback, which reads the scoped verse and never
looks at the question. The Day-6 pattern, by the builder who named it.

- BereishitCycle.engine_text/2: when the scope came from an explicit
  reference AND the question has no Hebrew letter, the engine is handed
  the verse's own consonantal words. Default scope earns no such
  reading (English stays a teiku); quoted Torah and reference+Hebrew
  are read from the asker's words, as before.
- input.scope_resolution carries how the scope was chosen into tohu.
- processing.scope.roots_read_from: "verse" | "question".
- test/tablet32_reference_test.exs: an engine-shaped spy under the REAL
  name VavPipeline.ShoreshEnginePort reports the text it was handed.
  5 tests; 4 fail without the fix.

459 tests, 0 failures on seeds 0 1 7 42 99.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-04 16:52:11 +00:00
1 parent 7e1a4473c7
commit a48c22c5a3
3 files changed
+229 -4

No files matched your search

@@ -114,6 +114,9 @@ defmodule VavPipeline.BereishitCycle do
raw: extract_content(input), raw: extract_content(input),
provenance: Map.get(input, :source, "unknown"), provenance: Map.get(input, :source, "unknown"),
scope: Map.get(input, :verse_scope, %{}), scope: Map.get(input, :verse_scope, %{}),
# How the scope was chosen (:verse_reference | :text_match | :default),
# when the caller knows. vayomer needs it: see engine_text/2.
scope_resolution: Map.get(input, :scope_resolution),
case_ref: generate_case_ref(), case_ref: generate_case_ref(),
# A tamid probe (tamid §2.1 check 7) runs the real cycle but must # A tamid probe (tamid §2.1 check 7) runs the real cycle but must
# not consume the day's manna or append to the oral tradition. # not consume the day's manna or append to the oral tradition.
@@ -137,14 +140,17 @@ defmodule VavPipeline.BereishitCycle do
""" """
@spec vayomer(map(), VavPipeline.Shema.shema_context()) :: map() @spec vayomer(map(), VavPipeline.Shema.shema_context()) :: map()
def vayomer(state, context) do def vayomer(state, context) do
case ShoreshEngineClient.process(state.raw, context) do {text, read_from} = engine_text(state, context)
state = Map.put(state, :roots_read_from, read_from)
case ShoreshEngineClient.process(text, context) do
{:ok, engine_result} -> {:ok, engine_result} ->
raw_roots = get_result(engine_result, :roots_activated, "roots_activated", []) raw_roots = get_result(engine_result, :roots_activated, "roots_activated", [])
if raw_roots == [] do if raw_roots == [] do
Logger.info( Logger.info(
"BereishitCycle.vayomer: Shoresh Engine activated no roots from " <> "BereishitCycle.vayomer: Shoresh Engine activated no roots from " <>
"#{inspect(String.slice(state.raw, 0, 60))} — the input carries no " <> "#{inspect(String.slice(text, 0, 60))} — the input carries no " <>
"Hebrew root the engine can read; ki_tov will not call this tov" "Hebrew root the engine can read; ki_tov will not call this tov"
) )
end end
@@ -795,6 +801,31 @@ defmodule VavPipeline.BereishitCycle do
end end
end end
# What the engine is given to read.
#
# Tablet 32 (2026-10-04): the engine activates roots from the TEXT it is
# handed, and it was always handed the question. "devarim 6:5" names a
# verse and carries no Hebrew, so on the pod it activated nothing and
# the ger got TEIKU_NO_ROOTS — under an error message that told them to
# "name a verse". The L1 fallback hid this in every test, because the
# fallback reads the scoped verse and never looks at the question.
#
# When the asker NAMED the verse and quoted none of it, the verse is
# what they are asking about: the engine reads the verse's own words.
# A default scope earns no such reading — nothing was named, and an
# English question against an assumed verse stays a teiku.
@hebrew_letter ~r/[\x{05D0}-\x{05EA}]/u
defp engine_text(%{scope_resolution: :verse_reference, raw: raw} = _state, context) do
if String.match?(raw, @hebrew_letter) do
{raw, :question}
else
{Enum.map_join(context.verse.words, " ", & &1.consonants), :verse}
end
end
defp engine_text(state, _context), do: {state.raw, :question}
# --- L1 Fallback (when port is unavailable) --- # --- L1 Fallback (when port is unavailable) ---
defp vayomer_fallback(state, context) do defp vayomer_fallback(state, context) do
@@ -814,6 +845,8 @@ defmodule VavPipeline.BereishitCycle do
Map.merge(state, %{ Map.merge(state, %{
roots_activated: roots, roots_activated: roots,
# The fallback has only ever read the scoped verse — say so.
roots_read_from: :verse,
teamim_parse: context.teamim_marks, teamim_parse: context.teamim_marks,
ketiv_qere: context.ketiv_qere, ketiv_qere: context.ketiv_qere,
root_source: :l1, root_source: :l1,
@@ -336,6 +336,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
input = %{ input = %{
source: "peah", source: "peah",
verse_scope: scope, verse_scope: scope,
scope_resolution: resolved_by,
content: question content: question
} }
@@ -350,7 +351,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(), roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(),
deliberation: "none", deliberation: "none",
cycle_phase: "voker", cycle_phase: "voker",
scope: scope_report scope: Map.put(scope_report, :roots_read_from, roots_read_from(data))
}, },
disclosure: disclosure() disclosure: disclosure()
}) })
@@ -363,7 +364,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(), roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(),
deliberation: "single-judge", deliberation: "single-judge",
cycle_phase: "voker", cycle_phase: "voker",
scope: scope_report scope: Map.put(scope_report, :roots_read_from, roots_read_from(data))
}, },
note: "This question produced an unresolved result — teiku.", note: "This question produced an unresolved result — teiku.",
disclosure: disclosure() disclosure: disclosure()
@@ -844,6 +845,11 @@ defmodule VavPipeline.BigdeiKodesh.Router do
%{verse: scope_verse(scope), resolved_by: to_string(resolved_by)} %{verse: scope_verse(scope), resolved_by: to_string(resolved_by)}
end end
# "question" — the engine read the asker's own Hebrew.
# "verse" — the asker named a verse and quoted none of it, so the
# roots are the verse's own (tablet 32).
defp roots_read_from(data), do: data |> Map.get(:roots_read_from, :question) |> to_string()
defp parse_scope(nil), do: %{book: "bereishit", chapter: 1, verse: 1} defp parse_scope(nil), do: %{book: "bereishit", chapter: 1, verse: 1}
defp parse_scope(%{"book" => book, "chapter" => ch, "verse" => v}) do defp parse_scope(%{"book" => book, "chapter" => ch, "verse" => v}) do
@@ -0,0 +1,186 @@
defmodule VavPipeline.Tablet32ReferenceTest do
@moduledoc """
Tablet 32 (2026-10-04) — `devarim 6:5` on the live pod: 422
TEIKU_NO_ROOTS. The scope was heard (tablet 30), but the Shoresh Engine
activates roots from the text it is handed, it was handed the question,
and a bare reference has no Hebrew in it.
`tablet30_scope_test` asserted "an explicit reference is honoured" and
passed — on the L1 fallback, which reads the scoped verse and never
looks at the question. The Day-6 pattern, again: green here, broken
where the engine is real.
So this file does not run the fallback. Like `day6_serving_test`, it
registers a process under the REAL name `VavPipeline.ShoreshEnginePort`
that behaves the way the engine behaves — roots only from Hebrew it is
given — and it reports back exactly what text it was given.
"""
use ExUnit.Case, async: false
import Plug.Test
import Plug.Conn
alias VavPipeline.{HaltMonitor, L1Reader, MannaLedger}
alias VavPipeline.BigdeiKodesh.Router
@opts Router.init([])
# ============================================================
# An engine-shaped stand-in that tells the test what it was handed
# ============================================================
defmodule SpyPort do
use GenServer
def start(test_pid),
do: GenServer.start(__MODULE__, test_pid, name: VavPipeline.ShoreshEnginePort)
@impl true
def init(test_pid), do: {:ok, test_pid}
@impl true
def handle_call({:request, json_line}, _from, test_pid) do
text = json_line |> Jason.decode!() |> get_in(["input", "text"]) || ""
send(test_pid, {:engine_received, text})
roots =
cond do
# Devarim 6:5 opens וְאָהַבְתָּ — the engine would find א-ה-ב.
String.contains?(text, "ואהבת") -> [root(["aleph", "he", "bet"], "אהב", 8)]
String.match?(text, ~r/[\x{05D0}-\x{05EA}]/u) -> [root(["bet", "resh", "aleph"], "ברא", 203)]
true -> []
end
{:reply, {:ok, %{"roots_activated" => roots, "parse_tree" => nil, "dual_readings" => []}},
test_pid}
end
# The engine's root shape as the pod delivers it: string keys, no `position`.
defp root(letters, hebrew, gematria) do
%{
"shoresh" => letters,
"shoresh_hebrew" => hebrew,
"gematria_value" => gematria,
"binyan" => "Qal",
"domain" => "unknown",
"torah_refs" => [],
"neighbors" => [],
"gematria_neighbors" => []
}
end
end
setup do
if HaltMonitor.halted?(), do: HaltMonitor.resolve("tablet32 test setup")
write_provision(50)
# This file is about the real-engine path. If a real engine is
# registered on this host there is nothing to stand in for.
assert Process.whereis(VavPipeline.ShoreshEnginePort) == nil,
"a real ShoreshEnginePort is registered; this test needs to spy on the port"
{:ok, pid} = SpyPort.start(self())
on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) end)
:ok
end
describe "a named verse, no Hebrew in the question" do
test "the engine is handed the verse's own words, and the ger is answered" do
conn = post_ask("devarim 6:5")
body = Jason.decode!(conn.resp_body)
{:ok, verse} = L1Reader.read_verse("devarim", 6, 5)
verse_text = Enum.map_join(verse.words, " ", & &1.consonants)
assert_received {:engine_received, ^verse_text}
assert verse_text =~ "ואהבת"
assert conn.status == 200, "#{conn.status} #{conn.resp_body}"
assert [%{"ref" => "Devarim 6:5"}] = body["sources"]
assert body["processing"]["scope"]["resolved_by"] == "verse_reference"
assert body["processing"]["scope"]["roots_read_from"] == "verse"
assert body["processing"]["roots_consulted"] != []
assert body["answer"] =~ "Devarim 6:5"
end
test "English around the reference changes nothing" do
conn = post_ask("What does Deuteronomy 6:5 command?")
body = Jason.decode!(conn.resp_body)
assert_received {:engine_received, text}
assert text =~ "ואהבת"
assert conn.status == 200
assert body["processing"]["scope"]["roots_read_from"] == "verse"
end
end
describe "what must NOT be handed the verse" do
test "no reference, no Hebrew: the default scope earns no reading — still a teiku" do
conn = post_ask("Test query 1")
body = Jason.decode!(conn.resp_body)
assert_received {:engine_received, "Test query 1"}
assert conn.status == 422
assert body["error"]["code"] == "TEIKU_NO_ROOTS"
end
test "quoted Torah: the engine reads the asker's own words" do
conn = post_ask("ויאמר אלהים יהי אור")
body = Jason.decode!(conn.resp_body)
assert_received {:engine_received, "ויאמר אלהים יהי אור"}
assert conn.status == 200
assert body["processing"]["scope"]["resolved_by"] == "text_match"
assert body["processing"]["scope"]["roots_read_from"] == "question"
end
test "a reference WITH Hebrew: the asker's Hebrew is what is read" do
q = "devarim 6:5 — ואהבת"
conn = post_ask(q)
body = Jason.decode!(conn.resp_body)
assert_received {:engine_received, ^q}
assert conn.status == 200
assert body["processing"]["scope"]["resolved_by"] == "verse_reference"
assert body["processing"]["scope"]["roots_read_from"] == "question"
end
end
# ------------------------------------------------------------
# Helpers (shape from day6_serving_test.exs / tablet30_scope_test.exs)
# ------------------------------------------------------------
defp write_provision(compute_units) do
MannaLedger.reset()
on_exit(fn -> MannaLedger.reset() end)
today = Date.utc_today() |> Date.to_iso8601()
expires = DateTime.utc_now() |> DateTime.add(6 * 3600) |> DateTime.to_iso8601()
provision = %{
"date" => today,
"provision" => %{
"compute_units" => compute_units,
"memory_mb" => 512,
"api_calls_external" => 5,
"storage_delta_mb" => 2,
"sanhedrin_panels_max" => 2
},
"type" => "regular",
"expires_at" => expires,
"written_by" => "tablet32_test",
"written_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
path = VavPipeline.Manna.provision_file()
File.mkdir_p!(Path.dirname(path))
File.write!(path, Jason.encode!(provision))
on_exit(fn -> File.rm(path) end)
path
end
defp post_ask(question) do
conn(:post, "/peah/ask", Jason.encode!(%{question: question}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
end
end