Compare commits

..
Author SHA1 Message Date
Scott KarpandClaude Fable 5.1 e01181959d tablet 55, fourth: the engine answers the whole dialectic over the wire
Live after vav :4bffa01e the judges consulted with roots the engine
could read — interpret found 4 mitzvot and 11 verses for bereishit 1:1
— and the other two seats still said teiku. The engine had no
"challenge" action at all (process_json_line failed and the socket
fell silent), and a resolve that SUCCEEDED came back
UNEXPECTED_RESPONSE because response(ok, resolved_accepted, ...) had
no JSON clause. Both pre-existing; both invisible until a judge asked.

- api.pl: request(challenge, ...) — interpret, then a kushya against
  it → resolved_challenged with the kushya; none → no_challenge with
  the reading.
- json_bridge.pl: interpreted | resolved_accepted | no_challenge share
  the "resolved" JSON; resolved_challenged now carries the
  interpretation's verses and mitzvot (a judge rejecting on the kushya
  cites its ground); resolved_synthesized and no_interpretation gain
  clauses.
- reasoning_test.pl: dialectic_over_the_wire — all three actions
  answer; no roots → resolved on an empty trace (the judge abstains).

Prolog: 7 suites green (bedikah 45, halt 49, middot 47, mitzvot 42,
phase3 28, reasoning 18, tzaraat 34). Local probe: interpret /
challenge / resolve each → resolved, 11 verses, 4 mitzvot.

Live proof waits for tomorrow's manna: today's five panels were all
spent on verification (503 MANNA_DEPLETED, as specified).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 18:25:17 +00:00
Scott KarpandClaude Fable 5.1 4bffa01e0e tablet 55, third: roots the engine can read; abstentions count; an empty trace is no ground
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 <noreply@anthropic.com>
2026-10-19 18:18:50 +00:00
10 changed files with 228 additions and 13 deletions

No files matched your search

+9
View File
@@ -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
View File
@@ -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
+31
View File
@@ -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
%% ============================================================
+18
View File
@@ -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).
+41 -6
View File
@@ -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),
verses(Verses), reading(Reading),
confidence(Conf))),
%% 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),
+49
View File
@@ -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).
@@ -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
# ============================================================
@@ -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)
+4 -2
View File
@@ -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