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),
provenance: Map.get(input, :source, "unknown"),
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(),
# 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.
@@ -137,14 +140,17 @@ defmodule VavPipeline.BereishitCycle do
"""
@spec vayomer(map(), VavPipeline.Shema.shema_context()) :: map()
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} ->
raw_roots = get_result(engine_result, :roots_activated, "roots_activated", [])
if raw_roots == [] do
Logger.info(
"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"
)
end
@@ -795,6 +801,31 @@ defmodule VavPipeline.BereishitCycle do
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) ---
defp vayomer_fallback(state, context) do
@@ -814,6 +845,8 @@ defmodule VavPipeline.BereishitCycle do
Map.merge(state, %{
roots_activated: roots,
# The fallback has only ever read the scoped verse — say so.
roots_read_from: :verse,
teamim_parse: context.teamim_marks,
ketiv_qere: context.ketiv_qere,
root_source: :l1,
@@ -336,6 +336,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
input = %{
source: "peah",
verse_scope: scope,
scope_resolution: resolved_by,
content: question
}
@@ -350,7 +351,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(),
deliberation: "none",
cycle_phase: "voker",
scope: scope_report
scope: Map.put(scope_report, :roots_read_from, roots_read_from(data))
},
disclosure: disclosure()
})
@@ -363,7 +364,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(),
deliberation: "single-judge",
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.",
disclosure: disclosure()
@@ -844,6 +845,11 @@ defmodule VavPipeline.BigdeiKodesh.Router do
%{verse: scope_verse(scope), resolved_by: to_string(resolved_by)}
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(%{"book" => book, "chapter" => ch, "verse" => v}) do