Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0eb2f983 | ||
|
|
ab6ca1ebcf | ||
|
|
e01181959d | ||
|
|
4bffa01e0e |
No files matched your search
@@ -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),
|
||||
|
||||
+10
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
%% ============================================================
|
||||
|
||||
@@ -45,6 +45,24 @@ process_request(request(resolve, CaseRef, Domain, Roots, Question), Response) :-
|
||||
; Response = response(error, no_resolution, CaseRef)
|
||||
).
|
||||
|
||||
%% The challenge step of the dialectic, on its own (tablet 55: the
|
||||
%% Sanhedrin's second seat asks it; until now no handler answered and
|
||||
%% the socket fell silent). Interpret, then look for a kushya against
|
||||
%% the interpretation: one stands → resolved_challenged, the kushya
|
||||
%% named; none → no_challenge, the interpretation as read.
|
||||
process_request(request(challenge, CaseRef, Domain, Roots, Question), Response) :-
|
||||
Case = case(CaseRef, Domain, Roots, Question),
|
||||
( interpret(Case, Interpretation)
|
||||
-> ( challenge(Interpretation, Kushya)
|
||||
-> Response = response(ok, resolved_challenged,
|
||||
result(interpretation(Interpretation),
|
||||
challenge(Kushya)))
|
||||
; format_interpretation(Interpretation, response(ok, interpreted, Result)),
|
||||
Response = response(ok, no_challenge, Result)
|
||||
)
|
||||
; Response = response(error, no_interpretation, CaseRef)
|
||||
).
|
||||
|
||||
process_request(request(halt_check, Action), Response) :-
|
||||
format_halt_check(Action, Response).
|
||||
|
||||
|
||||
@@ -153,11 +153,15 @@ read_coupling_term(Str, Term) :-
|
||||
%% JSON format (Construction §3.3):
|
||||
%% { "status": "resolved", "interpretation": { ... } }
|
||||
|
||||
%% Interpreted response
|
||||
response_to_json(response(ok, interpreted, result(ref(Ref), mitzvot(Mitzvot),
|
||||
%% Interpreted response — and the same shape under the names the
|
||||
%% resolve and challenge steps give it when the reading stands
|
||||
%% (tablet 55: resolved_accepted had no clause, so a resolve that
|
||||
%% succeeded came back UNEXPECTED_RESPONSE; no_challenge is new).
|
||||
response_to_json(response(ok, Kind, result(ref(Ref), mitzvot(Mitzvot),
|
||||
verses(Verses), reading(Reading),
|
||||
confidence(Conf))),
|
||||
Dict) :-
|
||||
memberchk(Kind, [interpreted, resolved_accepted, no_challenge]),
|
||||
maplist(term_to_string, Mitzvot, MitzvotStrs),
|
||||
maplist(term_to_string, Verses, VerseStrs),
|
||||
term_to_string(Reading, ReadingStr),
|
||||
@@ -171,21 +175,52 @@ response_to_json(response(ok, interpreted, result(ref(Ref), mitzvot(Mitzvot),
|
||||
confidence: ConfStr},
|
||||
challenge: null}.
|
||||
|
||||
%% Challenged response
|
||||
%% Challenged response — the interpretation's verses and mitzvot are
|
||||
%% carried, so a judge rejecting on the kushya still cites its ground.
|
||||
response_to_json(response(ok, resolved_challenged,
|
||||
result(interpretation(Interp), challenge(Kushya))),
|
||||
Dict) :-
|
||||
Interp = interpretation(_Ref, Mitzvot, Verses, _Reading, _Conf, _Trace),
|
||||
maplist(term_to_string, Mitzvot, MitzvotStrs),
|
||||
maplist(term_to_string, Verses, VerseStrs),
|
||||
term_to_string(Interp, InterpStr),
|
||||
term_to_string(Kushya, KushyaStr),
|
||||
Dict = json{status: "challenged",
|
||||
interpretation: json{text: InterpStr,
|
||||
prolog_form: InterpStr,
|
||||
grounding: [],
|
||||
grounding: MitzvotStrs,
|
||||
verses: VerseStrs,
|
||||
confidence: "medium"},
|
||||
challenge: json{kushya: KushyaStr,
|
||||
contradicting: "",
|
||||
strength: "moderate"}}.
|
||||
|
||||
%% Synthesized resolution: both readings stand on common ground.
|
||||
response_to_json(response(ok, resolved_synthesized,
|
||||
result(common(Common), verses(Verses), both_sides(Both))),
|
||||
Dict) :-
|
||||
maplist(term_to_string, Common, CommonStrs),
|
||||
maplist(term_to_string, Verses, VerseStrs),
|
||||
term_to_string(Both, BothStr),
|
||||
Dict = json{status: "resolved",
|
||||
interpretation: json{text: BothStr,
|
||||
prolog_form: BothStr,
|
||||
grounding: CommonStrs,
|
||||
verses: VerseStrs,
|
||||
confidence: "medium"},
|
||||
challenge: null}.
|
||||
|
||||
%% No interpretation possible (from resolve)
|
||||
response_to_json(response(ok, no_interpretation, result(ref(Ref))), Dict) :-
|
||||
atom_string(Ref, RefStr),
|
||||
Dict = json{status: "teiku",
|
||||
interpretation: json{text: "No interpretation found",
|
||||
prolog_form: RefStr,
|
||||
grounding: [],
|
||||
verses: [],
|
||||
confidence: "low"},
|
||||
challenge: null}.
|
||||
|
||||
%% Teiku response
|
||||
response_to_json(response(ok, teiku, result(record(Record))), Dict) :-
|
||||
term_to_string(Record, RecordStr),
|
||||
|
||||
@@ -112,3 +112,52 @@ test(teiku_count_works) :-
|
||||
integer(Count).
|
||||
|
||||
:- end_tests(teiku_module).
|
||||
|
||||
%% ============================================================
|
||||
%% Tablet 55 — the dialectic over the wire: interpret, challenge,
|
||||
%% resolve each answer JSON a judge can vote on. Before this the
|
||||
%% engine had no "challenge" action and a resolve that succeeded came
|
||||
%% back UNEXPECTED_RESPONSE (resolved_accepted had no JSON clause).
|
||||
%% ============================================================
|
||||
|
||||
:- use_module('../src/interface/api').
|
||||
:- use_module('../src/interface/json_bridge').
|
||||
:- use_module('../src/interface/socket_server').
|
||||
|
||||
:- begin_tests(dialectic_over_the_wire).
|
||||
|
||||
roots_line(Action, Line) :-
|
||||
format(atom(Line),
|
||||
'{"action":"~w","case":{"ref":"t55-~w","question":"q","context_roots":["bet-resh-aleph","aleph-lamed-he"]}}',
|
||||
[Action, Action]).
|
||||
|
||||
test(interpret_answers_resolved_with_verses, [nondet]) :-
|
||||
roots_line(interpret, L),
|
||||
socket_server:process_json_line(L, R),
|
||||
get_dict(status, R, "resolved"),
|
||||
get_dict(interpretation, R, I),
|
||||
get_dict(verses, I, Vs), Vs \== [].
|
||||
|
||||
test(challenge_answers_and_carries_the_ground, [nondet]) :-
|
||||
roots_line(challenge, L),
|
||||
socket_server:process_json_line(L, R),
|
||||
get_dict(status, R, S),
|
||||
memberchk(S, ["resolved", "challenged"]),
|
||||
get_dict(interpretation, R, I),
|
||||
get_dict(verses, I, Vs), Vs \== [].
|
||||
|
||||
test(resolve_answers_resolved_not_unexpected, [nondet]) :-
|
||||
roots_line(resolve, L),
|
||||
socket_server:process_json_line(L, R),
|
||||
\+ get_dict(error, R, _),
|
||||
get_dict(status, R, S),
|
||||
memberchk(S, ["resolved", "challenged", "teiku"]).
|
||||
|
||||
test(no_roots_resolves_on_an_empty_trace, [nondet]) :-
|
||||
socket_server:process_json_line('{"action":"interpret","case":{"ref":"t55-empty","question":"q","context_roots":[]}}', R),
|
||||
get_dict(status, R, "resolved"),
|
||||
get_dict(interpretation, R, I),
|
||||
get_dict(verses, I, []),
|
||||
get_dict(grounding, I, []).
|
||||
|
||||
:- end_tests(dialectic_over_the_wire).
|
||||
@@ -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}
|
||||
@@ -740,7 +850,10 @@ defmodule VavPipeline.BereishitCycle do
|
||||
@doc false
|
||||
defp route_if_unresolved(data, context) do
|
||||
roots = Map.get(data, :roots_activated, [])
|
||||
context_roots = Enum.map(roots, & &1.shoresh)
|
||||
# Tablet 55: the engine names roots by letter (bet_resh_aleph); the
|
||||
# Hebrew was sent since Step 14 and never matched a verse fact, so
|
||||
# every Talmudic interpretation was "resolved" over an empty trace.
|
||||
context_roots = roots |> Enum.map(&engine_root_name/1) |> Enum.reject(&is_nil/1)
|
||||
|
||||
case_ref = Map.get(data, :case_ref, generate_case_ref())
|
||||
|
||||
@@ -970,6 +1083,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
|
||||
# ============================================================
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user