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

@@ -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