From 4bffa01e0e5c4d682d8b836352c46c9dd6c8a04b Mon Sep 17 00:00:00 2001 From: Scott Karp Date: Mon, 19 Oct 2026 18:18:50 +0000 Subject: [PATCH] tablet 55, third: roots the engine can read; abstentions count; an empty trace is no ground MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live after the second commit (vav :046010f0): a monetary case naming bereishit 1:1 convened three judges and came back "accepted" — the interpret seat said "resolved (5 roots, 0 verses)" with reading(mitzvot_count(0),verse_count(0),grounded_in([],[])), the other two seats said teiku, and one accept beat two abstentions (the reply showed majority 1, minority 2). Three defects the tests could not see: - The engine names roots by letter (bet_resh_aleph; json_bridge reads "bet-resh-aleph") and the pipeline sent it the Hebrew, which its verse facts never match. BereishitCycle.engine_root_name/1 builds the engine's name from the Shoresh Engine's letter names or, for the L1 fallback, from the Hebrew with finals folded; the router sends that. (The cycle's own Talmudic routing still sends the Hebrew — recorded, not changed here: it alters /peah/ask and needs its own verification.) - judge.erl: "resolved" with no verse and no mitzvah is a reading coupled to nothing (avodah_zarah.pl Form 2); the judge abstains and says "resolved on an empty trace; nothing to stand on". - panel.erl: the majority is the verdict with the most votes, abstentions included; an abstaining plurality or an accept/reject tie is teiku. EUnit 57/0 (plurality, tie, empty trace). Elixir 632/0 (engine_root_name from names, from Hebrew, nil on nothing; the no-engine case is an abstaining majority of one, teiku). Co-Authored-By: Claude Fable 5.1 --- sanhedrin/src/judge.erl | 9 +++++ sanhedrin/src/panel.erl | 14 +++++--- sanhedrin/test/sanhedrin_SUITE.erl | 31 +++++++++++++++++ .../lib/vav_pipeline/bereishit_cycle.ex | 34 +++++++++++++++++++ .../lib/vav_pipeline/bigdei_kodesh/router.ex | 6 +++- vav_pipeline/test/bigdei_kodesh_test.exs | 6 ++-- .../test/tablet55_engine_root_name_test.exs | 27 +++++++++++++++ 7 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 vav_pipeline/test/tablet55_engine_root_name_test.exs diff --git a/sanhedrin/src/judge.erl b/sanhedrin/src/judge.erl index 3d071c88..83609612 100644 --- a/sanhedrin/src/judge.erl +++ b/sanhedrin/src/judge.erl @@ -157,7 +157,16 @@ vote_from_consultation(Step, {ok, Map}, RootCount) -> Status = maps:get(status, Map, <<"teiku">>), Text = maps:get(text, Map, <<>>), Verses = maps:get(verses, Map, []), + Grounding = maps:get(grounding, Map, []), case Status of + <<"resolved">> when Verses =:= [], Grounding =:= [] -> + %% The engine says "resolved" over an empty trace — no verse, + %% no mitzvah. That is a reading that stands on nothing, and + %% a vote to accept it would be the calf (avodah_zarah.pl + %% Form 2: the uncoupled output). Abstain, and say why. + {<<"Teiku — the reading is coupled to no verse and no mitzvah">>, + reasoning(Step, <<"resolved on an empty trace; nothing to stand on">>, RootCount, Verses), + [], abstain}; <<"resolved">> -> {Text, reasoning(Step, <<"resolved">>, RootCount, Verses), diff --git a/sanhedrin/src/panel.erl b/sanhedrin/src/panel.erl index b357e734..89282f6a 100644 --- a/sanhedrin/src/panel.erl +++ b/sanhedrin/src/panel.erl @@ -207,15 +207,21 @@ compute_majority_minority([]) -> torah_refs => [], vote_count => 0}}; compute_majority_minority(Votes) -> - %% Count accepts vs rejects Accepts = [V || V <- Votes, V#vote.verdict == accept], Rejects = [V || V <- Votes, V#vote.verdict == reject], Abstains = [V || V <- Votes, V#vote.verdict == abstain], + NA = length(Accepts), NR = length(Rejects), NB = length(Abstains), + %% The majority is the verdict with the most votes — abstentions + %% count (tablet 55: live, one accept "won" against two + %% abstentions, and the reply showed majority 1, minority 2). An + %% abstaining plurality, or an accept/reject tie, is teiku + %% (determine_outcome reads the majority's verdict). {MajVotes, MinVotes} = - case length(Accepts) >= length(Rejects) of - true -> {Accepts, Rejects ++ Abstains}; - false -> {Rejects, Accepts ++ Abstains} + if + NA > NR, NA >= NB -> {Accepts, Rejects ++ Abstains}; + NR > NA, NR >= NB -> {Rejects, Accepts ++ Abstains}; + true -> {Abstains, Accepts ++ Rejects} end, MajSample = case MajVotes of diff --git a/sanhedrin/test/sanhedrin_SUITE.erl b/sanhedrin/test/sanhedrin_SUITE.erl index f5bdf8a3..ab980a09 100644 --- a/sanhedrin/test/sanhedrin_SUITE.erl +++ b/sanhedrin/test/sanhedrin_SUITE.erl @@ -506,6 +506,37 @@ majority_verdict_reject_test() -> {Maj, _Min} = panel:compute_majority_minority(Votes), ?assertEqual(reject, maps:get(verdict, Maj)). +%% Tablet 55: abstentions count. One accept against two abstentions is +%% not an accepting majority — it is teiku. +majority_abstaining_plurality_is_teiku_test() -> + Votes = [make_vote(accept), make_vote(abstain), make_vote(abstain)], + {Maj, Min} = panel:compute_majority_minority(Votes), + ?assertEqual(abstain, maps:get(verdict, Maj)), + ?assertEqual(2, maps:get(vote_count, Maj)), + ?assertEqual(1, maps:get(vote_count, Min)), + ?assertEqual(teiku, panel:determine_outcome(Maj, Min, passed)). + +majority_accept_reject_tie_is_teiku_test() -> + Votes = [make_vote(accept), make_vote(reject)], + {Maj, _Min} = panel:compute_majority_minority(Votes), + ?assertEqual(abstain, maps:get(verdict, Maj)), + ?assertEqual(0, maps:get(vote_count, Maj)). + +%% Tablet 55: "resolved" over an empty trace — no verse, no mitzvah — +%% is nothing to stand on; the judge abstains rather than accept. +judge_resolved_on_empty_trace_abstains_test() -> + {Pos, Reason, Refs, Verdict} = + judge:vote_from_consultation(interpret, + {ok, #{status => <<"resolved">>, text => <<"reading(0,0)">>, verses => [], grounding => []}}, 5), + ?assertEqual(abstain, Verdict), + ?assertEqual([], Refs), + ?assertMatch({_, _}, binary:match(Pos, <<"Teiku">>)), + ?assertMatch({_, _}, binary:match(Reason, <<"empty trace">>)), + %% a mitzvah without a verse is still ground + {_, _, _, V2} = judge:vote_from_consultation(interpret, + {ok, #{status => <<"resolved">>, text => <<"r">>, verses => [], grounding => [<<"mitzvah(5)">>]}}, 1), + ?assertEqual(accept, V2). + %% ============================================================ %% determine_outcome tests — accepted, rejected, teiku reachable %% ============================================================ diff --git a/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex b/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex index 51ae2f1f..6cfcd2f1 100644 --- a/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex +++ b/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex @@ -970,6 +970,40 @@ defmodule VavPipeline.BereishitCycle do defp letters_to_hebrew(_), do: nil + # The inverse: Hebrew consonant → letter name, finals folded to their base. + @hebrew_to_name Map.new(@consonant_names, fn {name, heb} -> {heb, name} end) + |> Map.merge(%{"ך" => "kaf", "ם" => "mem", "ן" => "nun", "ף" => "pe", "ץ" => "tsadi"}) + + @doc """ + The root as the Talmudic Engine names it — letter names joined by `-` + (`json_bridge` `parse_root_name` → `bet_resh_aleph`, the form of its + verse facts). From the letter names the Shoresh Engine gives when it + has them, else from the Hebrew (the L1 fallback carries only that). + Tablet 55: the engine was being sent the Hebrew, which its facts never + match — 5 roots in, 0 verses out, and "resolved" over the emptiness. + `nil` when the root carries nothing nameable. + """ + @spec engine_root_name(map()) :: String.t() | nil + def engine_root_name(root) when is_map(root) do + letters = Map.get(root, :shoresh_letters) || [] + + names = + if is_list(letters) and letters != [] do + Enum.map(letters, &String.downcase(to_string(&1))) + else + root + |> Map.get(:shoresh) + |> case do + s when is_binary(s) -> s |> String.graphemes() |> Enum.map(&Map.get(@hebrew_to_name, &1)) + _ -> [nil] + end + end + + if names != [] and Enum.all?(names, &is_binary/1), do: Enum.join(names, "-"), else: nil + end + + def engine_root_name(_), do: nil + # ============================================================ # Private helpers # ============================================================ diff --git a/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex b/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex index bb93f7ba..ee03f76d 100644 --- a/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex +++ b/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex @@ -1250,7 +1250,11 @@ defmodule VavPipeline.BigdeiKodesh.Router do {:ok, %{ - roots: Enum.map(roots_full, & &1.shoresh), + # The engine's verse facts name roots by letter — bet_resh_aleph — + # and json_bridge reads "bet-resh-aleph". The cycle's root carries + # the letter names beside the Hebrew; the Hebrew never matches + # (live, 2026-10-19: 5 roots sent, 0 verses found, "resolved"). + roots: roots_full |> Enum.map(&VavPipeline.BereishitCycle.engine_root_name/1) |> Enum.reject(&is_nil/1), roots_full: roots_full, torah_ref: {String.to_atom(scope.book), scope.chapter, scope.verse}, read_from: Map.get(state, :roots_read_from) diff --git a/vav_pipeline/test/bigdei_kodesh_test.exs b/vav_pipeline/test/bigdei_kodesh_test.exs index 7a5441d0..723dea15 100644 --- a/vav_pipeline/test/bigdei_kodesh_test.exs +++ b/vav_pipeline/test/bigdei_kodesh_test.exs @@ -403,8 +403,10 @@ defmodule VavPipeline.BigdeiKodeshTest do body = Jason.decode!(conn.resp_body) assert body["deliberation"]["outcome"] == "teiku" assert body["deliberation"]["panel_size"] == 1 - assert body["deliberation"]["majority"]["votes"] == 0 - assert body["deliberation"]["minority"]["reasoning"] =~ "no consultation" + # the one judge abstained: an abstaining majority of one, no dissent + assert body["deliberation"]["majority"]["votes"] == 1 + assert body["deliberation"]["majority"]["reasoning"] =~ "no consultation" + assert body["deliberation"]["minority"]["votes"] == 0 end # Tablet 55: with a consultation behind the judges, the dialectic diff --git a/vav_pipeline/test/tablet55_engine_root_name_test.exs b/vav_pipeline/test/tablet55_engine_root_name_test.exs new file mode 100644 index 00000000..c245b289 --- /dev/null +++ b/vav_pipeline/test/tablet55_engine_root_name_test.exs @@ -0,0 +1,27 @@ +defmodule VavPipeline.Tablet55EngineRootNameTest do + @moduledoc """ + Tablet 55: the Talmudic Engine names roots by letter (bet_resh_aleph, + read from "bet-resh-aleph"). The pipeline was sending it the Hebrew, + which its verse facts never match — roots in, no verses out, and a + "resolved" reading over the emptiness. + """ + use ExUnit.Case, async: true + + alias VavPipeline.BereishitCycle + + test "from the Shoresh Engine's letter names" do + assert BereishitCycle.engine_root_name(%{shoresh: "ברא", shoresh_letters: ["bet", "resh", "aleph"]}) == + "bet-resh-aleph" + end + + test "from the Hebrew alone (the L1 fallback), finals folded" do + assert BereishitCycle.engine_root_name(%{shoresh: "דבר", shoresh_letters: []}) == "dalet-bet-resh" + assert BereishitCycle.engine_root_name(%{shoresh: "מלך"}) == "mem-lamed-kaf" + end + + test "nothing nameable → nil, never a Hebrew string the engine cannot read" do + assert BereishitCycle.engine_root_name(%{shoresh: nil}) == nil + assert BereishitCycle.engine_root_name(%{shoresh: "x1"}) == nil + assert BereishitCycle.engine_root_name(%{}) == nil + end +end