tablet 55, second: the judge consults — the Sanhedrin no longer rolls dice
judge.erl chose one of six canned positions by a hash over the question,
its own pid and the clock; a panel of one "accepted" on zero roots and
zero references under "Torah references ground this interpretation" —
the calf, cast by the court itself. Architecture §3.2 says a judge
"consults the specification (L0/L1)"; Construction §3.2 says the
pipeline hands the case roots and torah_refs. Neither happened.
Sanhedrin (Erlang):
- judge.erl: seat N performs step N of the dialectic — interpret,
challenge, resolve, cycling — through a consult {M, F} in the app env.
resolved → accept with the verses; challenged (a kushya stands) →
reject; halt → reject; teiku → abstain. No consult, an error, or a
consult that raises → abstain, and the reasoning says so; the judge
stands. judge:start_link/2 takes the seat; panel.erl seats its judges.
- EUnit 54/0: seats cycle; no consult → abstain; resolved → accept with
#torah_ref{}; a raising consult → abstain and alive; a panel of three
with a kushya at seat 2 → accepted 2–1, minority preserved with verse.
Pipeline (Elixir):
- VavPipeline.SanhedrinConsult: what a judge asks — TalmudicEngineSocket
interpret/challenge/resolve over the case's roots, direct (the client's
stub fallbacks are NOT used: a court that accepts on a stub is the
calf). Engine absent → {:error, _} → the judge abstains. Registered in
the sanhedrin env at application start.
- router process_sanhedrin: the case is heard before it is submitted —
ScopeResolver → Shema context (cloud honoured) → tohu → vayomer
(Shoresh Engine over the verse) → roots; torah_refs = the scope;
complexity_hint names the panel (422 on an unknown one); one unit of
sanhedrin_panels_max per case (503 MANNA_DEPLETED when spent); the
decision engraved in L3 as an interpretation (panel_size, votes
for/against, processing_component "sanhedrin", coupling root→verse)
with the minority beside it (grounding_type achorai); the reply in
the Tamid's shape (position, reasoning, sources, votes) plus
grounding {scope, roots_consulted, recorded}.
- Tests: Elixir 628/0 (router 30, incl. monetary → panel 3; no engine →
teiku with 0 votes, never accepted; fake consult → 2–1 with sources;
manna → 503; unknown hint → 422); L3 test: interpretation + minority
rows land.
Pre-existing and untouched: with --include l3 the gate and hakhel
tests fail 17 times on main as well (the gate's local upstream double
refuses connections under the L3 flag) — a separate finding.
Doorpost pin unchanged (no @spec_files touched).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c40200dfc
commit
046010f0ed
8 files changed
+734
-107
No files matched your search
+115
-93
@@ -11,6 +11,20 @@
|
||||
%%%
|
||||
%%% Judges deliberate independently — they do not see
|
||||
%%% other judges' votes until the panel collects them.
|
||||
%%%
|
||||
%%% Tablet 55 (2026-10-19): until this rewrite the judge chose one of
|
||||
%%% six canned positions by a hash over the question, its own pid and
|
||||
%%% the clock. The verdict had no relation to the case; "Torah
|
||||
%%% references ground this interpretation (0 roots, 0 references
|
||||
%%% examined)" was accepted on nothing — the calf, cast by the court
|
||||
%%% itself. Now a judge CONSULTS: the application env `consult` names
|
||||
%%% an {M, F} the pipeline registers (the Talmudic Engine behind it),
|
||||
%%% and each seat on the panel performs one step of the dialectic —
|
||||
%%% interpret, challenge, resolve, cycling by seat — on the case's
|
||||
%%% roots and question. Resolved → accept, with the verses; a standing
|
||||
%%% kushya → reject; teiku → abstain. No consult, or an engine that
|
||||
%%% cannot be reached → abstain, and the reasoning says so. Dissent
|
||||
%%% arises from the dialectic, never from a die.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(judge).
|
||||
@@ -20,24 +34,34 @@
|
||||
-include("sanhedrin.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_link/1, deliberate/2, stop/1]).
|
||||
-export([start_link/1, start_link/2, deliberate/2, stop/1]).
|
||||
|
||||
%% Exposed for the suite
|
||||
-export([step_for_seat/1, vote_from_consultation/3]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
||||
|
||||
-record(state, {
|
||||
judge_id :: reference(),
|
||||
seat :: pos_integer(),
|
||||
case_data :: #sanhedrin_case{},
|
||||
vote :: #vote{} | undefined,
|
||||
panel_pid :: pid() | undefined
|
||||
}).
|
||||
|
||||
-define(STEPS, [interpret, challenge, resolve]).
|
||||
|
||||
%%====================================================================
|
||||
%% API
|
||||
%%====================================================================
|
||||
|
||||
start_link(Case) ->
|
||||
gen_server:start_link(?MODULE, Case, []).
|
||||
start_link(Case, 1).
|
||||
|
||||
%% @doc Start a judge in seat N of its panel (1-based).
|
||||
start_link(Case, Seat) ->
|
||||
gen_server:start_link(?MODULE, {Case, Seat}, []).
|
||||
|
||||
%% @doc Instruct this judge to deliberate and send vote to PanelPid.
|
||||
-spec deliberate(pid(), pid()) -> ok.
|
||||
@@ -49,28 +73,35 @@ deliberate(JudgePid, PanelPid) ->
|
||||
stop(JudgePid) ->
|
||||
gen_server:stop(JudgePid, normal, 5000).
|
||||
|
||||
%% @doc The step of the dialectic a seat performs: 1 → interpret,
|
||||
%% 2 → challenge, 3 → resolve, 4 → interpret, ...
|
||||
-spec step_for_seat(pos_integer()) -> interpret | challenge | resolve.
|
||||
step_for_seat(Seat) when is_integer(Seat), Seat >= 1 ->
|
||||
lists:nth(((Seat - 1) rem length(?STEPS)) + 1, ?STEPS).
|
||||
|
||||
%%====================================================================
|
||||
%% gen_server callbacks
|
||||
%%====================================================================
|
||||
|
||||
init(Case) ->
|
||||
JudgeId = make_ref(),
|
||||
init({Case, Seat}) ->
|
||||
{ok, #state{
|
||||
judge_id = JudgeId,
|
||||
judge_id = make_ref(),
|
||||
seat = Seat,
|
||||
case_data = Case,
|
||||
vote = undefined,
|
||||
panel_pid = undefined
|
||||
}}.
|
||||
}};
|
||||
init(Case) ->
|
||||
init({Case, 1}).
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, {error, unknown_request}, State}.
|
||||
|
||||
handle_cast({deliberate, PanelPid}, State) ->
|
||||
Vote = perform_deliberation(State#state.judge_id, State#state.case_data),
|
||||
Vote = perform_deliberation(State#state.judge_id, State#state.seat, State#state.case_data),
|
||||
%% Send vote to panel
|
||||
PanelPid ! {vote, Vote},
|
||||
{noreply, State#state{vote = Vote, panel_pid = PanelPid}};
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
@@ -78,112 +109,103 @@ handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
%%====================================================================
|
||||
%% Internal — Deliberation logic
|
||||
%% Internal — Deliberation
|
||||
%%====================================================================
|
||||
|
||||
%% @doc Perform independent deliberation.
|
||||
%%
|
||||
%% Phase 2: Each judge examines the case context (roots, torah_refs)
|
||||
%% and produces an independent interpretation. In Phase 3, judges
|
||||
%% will call the Talmudic Engine for deeper reasoning.
|
||||
%%
|
||||
%% The deliberation is non-deterministic by design — judges may
|
||||
%% reach different conclusions from the same evidence.
|
||||
perform_deliberation(JudgeId, Case) ->
|
||||
perform_deliberation(JudgeId, Seat, Case) ->
|
||||
Step = step_for_seat(Seat),
|
||||
Context = Case#sanhedrin_case.context,
|
||||
Question = Case#sanhedrin_case.question,
|
||||
|
||||
%% Extract roots and references from context
|
||||
Roots = maps:get(roots, Context, []),
|
||||
TorahRefs = maps:get(torah_refs, Context, []),
|
||||
|
||||
%% Generate interpretation based on case content
|
||||
{Position, Reasoning, Verdict} = interpret(Question, Roots, TorahRefs),
|
||||
|
||||
%% Build Torah references for this vote
|
||||
VoteRefs = build_refs(TorahRefs),
|
||||
|
||||
Question = Case#sanhedrin_case.question,
|
||||
Consultation = consult(Step, Case#sanhedrin_case.case_ref, Roots, Question, Context),
|
||||
{Position, Reasoning, Refs, Verdict} = vote_from_consultation(Step, Consultation, length(Roots)),
|
||||
#vote{
|
||||
judge_id = JudgeId,
|
||||
position = Position,
|
||||
reasoning = Reasoning,
|
||||
torah_refs = VoteRefs,
|
||||
torah_refs = build_refs(Refs),
|
||||
verdict = Verdict,
|
||||
cast_at = erlang:system_time(millisecond)
|
||||
}.
|
||||
|
||||
%% @doc Generate an interpretation.
|
||||
%%
|
||||
%% Phase 2: Uses deterministic logic based on case content.
|
||||
%% Each judge applies a slightly different weighting — simulated
|
||||
%% by examining different aspects of the roots and references.
|
||||
%%
|
||||
%% Phase 3: Will delegate to Talmudic Engine for real dialectical
|
||||
%% reasoning (interpret → challenge → resolve).
|
||||
interpret(Question, Roots, TorahRefs) ->
|
||||
%% Base interpretation on root analysis
|
||||
RootCount = length(Roots),
|
||||
RefCount = length(TorahRefs),
|
||||
|
||||
%% Deterministic but varied deliberation:
|
||||
%% Judges use their unique ID (via process randomness) to
|
||||
%% introduce variation, simulating independent thought.
|
||||
Hash = erlang:phash2({Question, self(), erlang:monotonic_time()}),
|
||||
|
||||
case Hash rem 6 of
|
||||
0 ->
|
||||
{<<"The roots indicate acceptance — the action aligns with Torah principle">>,
|
||||
format_reasoning(<<"Root analysis supports this interpretation">>, RootCount, RefCount),
|
||||
accept};
|
||||
1 ->
|
||||
{<<"The roots indicate rejection — the action contradicts specification">>,
|
||||
format_reasoning(<<"Root tension detected — contradicting principles found">>, RootCount, RefCount),
|
||||
reject};
|
||||
2 ->
|
||||
{<<"The roots support acceptance with qualification">>,
|
||||
format_reasoning(<<"Conditional acceptance — roots align but context requires care">>, RootCount, RefCount),
|
||||
accept};
|
||||
3 ->
|
||||
{<<"Insufficient grounding — the question cannot be resolved from available sources">>,
|
||||
format_reasoning(<<"Teiku — the evidence is insufficient for determination">>, RootCount, RefCount),
|
||||
abstain};
|
||||
4 ->
|
||||
{<<"The references confirm the action is consistent with specification">>,
|
||||
format_reasoning(<<"Torah references ground this interpretation">>, RootCount, RefCount),
|
||||
accept};
|
||||
5 ->
|
||||
{<<"The references reveal a tension that prohibits this action">>,
|
||||
format_reasoning(<<"Prohibition detected via root-reference cross-analysis">>, RootCount, RefCount),
|
||||
reject}
|
||||
%% The consult callback: {M, F} in the sanhedrin application env,
|
||||
%% called as M:F(Step, CaseRef, Roots, Question, Context) and expected
|
||||
%% to answer {ok, #{status := binary(), text := binary(),
|
||||
%% verses := [{atom(), integer(), integer()}], kushya => binary()}}
|
||||
%% or {error, term()}. Anything it raises is caught: a judge that
|
||||
%% cannot consult abstains; it does not fall.
|
||||
consult(Step, CaseRef, Roots, Question, Context) ->
|
||||
case application:get_env(sanhedrin, consult) of
|
||||
{ok, {M, F}} when is_atom(M), is_atom(F) ->
|
||||
try M:F(Step, CaseRef, Roots, Question, Context) of
|
||||
{ok, Map} when is_map(Map) -> {ok, Map};
|
||||
{error, Reason} -> {error, Reason};
|
||||
Other -> {error, {bad_consultation, Other}}
|
||||
catch
|
||||
Class:Reason -> {error, {Class, Reason}}
|
||||
end;
|
||||
_ ->
|
||||
{error, no_consult_registered}
|
||||
end.
|
||||
|
||||
format_reasoning(Base, RootCount, RefCount) ->
|
||||
%% @doc The vote a consultation yields. Exposed for the suite.
|
||||
-spec vote_from_consultation(interpret | challenge | resolve,
|
||||
{ok, map()} | {error, term()},
|
||||
non_neg_integer()) ->
|
||||
{binary(), binary(), list(), accept | reject | abstain}.
|
||||
vote_from_consultation(Step, {ok, Map}, RootCount) ->
|
||||
Status = maps:get(status, Map, <<"teiku">>),
|
||||
Text = maps:get(text, Map, <<>>),
|
||||
Verses = maps:get(verses, Map, []),
|
||||
case Status of
|
||||
<<"resolved">> ->
|
||||
{Text,
|
||||
reasoning(Step, <<"resolved">>, RootCount, Verses),
|
||||
Verses, accept};
|
||||
<<"challenged">> ->
|
||||
Kushya = maps:get(kushya, Map, <<"a kushya stands">>),
|
||||
{Kushya,
|
||||
reasoning(Step, <<"a kushya stands against the reading">>, RootCount, Verses),
|
||||
Verses, reject};
|
||||
<<"halt">> ->
|
||||
{Text,
|
||||
reasoning(Step, <<"the halt rules fired">>, RootCount, Verses),
|
||||
Verses, reject};
|
||||
_ ->
|
||||
{<<"Teiku — the question is not resolved from the specification">>,
|
||||
reasoning(Step, <<"teiku">>, RootCount, Verses),
|
||||
Verses, abstain}
|
||||
end;
|
||||
vote_from_consultation(Step, {error, Reason}, RootCount) ->
|
||||
{<<"Teiku — the specification was not consulted">>,
|
||||
iolist_to_binary([step_name(Step), <<": no consultation (">>,
|
||||
io_lib:format("~p", [Reason]), <<") — ">>,
|
||||
integer_to_binary(RootCount), <<" roots given; abstaining">>]),
|
||||
[], abstain}.
|
||||
|
||||
reasoning(Step, What, RootCount, Verses) ->
|
||||
iolist_to_binary([
|
||||
Base,
|
||||
<<" (">>,
|
||||
integer_to_binary(RootCount),
|
||||
<<" roots, ">>,
|
||||
integer_to_binary(RefCount),
|
||||
<<" references examined)">>
|
||||
step_name(Step), <<" via the Talmudic Engine: ">>, What,
|
||||
<<" (">>, integer_to_binary(RootCount), <<" roots, ">>,
|
||||
integer_to_binary(length(Verses)), <<" verses)">>
|
||||
]).
|
||||
|
||||
step_name(interpret) -> <<"interpret">>;
|
||||
step_name(challenge) -> <<"challenge">>;
|
||||
step_name(resolve) -> <<"resolve">>.
|
||||
|
||||
build_refs(TorahRefs) ->
|
||||
lists:map(
|
||||
fun(Ref) when is_tuple(Ref) ->
|
||||
case Ref of
|
||||
{Book, Chapter, Verse} ->
|
||||
#torah_ref{book = Book, chapter = Chapter, verse = Verse};
|
||||
_ ->
|
||||
#torah_ref{book = unknown, chapter = 0, verse = 0}
|
||||
end;
|
||||
fun({Book, Chapter, Verse}) ->
|
||||
#torah_ref{book = Book, chapter = Chapter, verse = Verse};
|
||||
(Ref) when is_map(Ref) ->
|
||||
#torah_ref{
|
||||
book = maps:get(book, Ref, unknown),
|
||||
chapter = maps:get(chapter, Ref, 0),
|
||||
verse = maps:get(verse, Ref, 0)
|
||||
};
|
||||
#torah_ref{
|
||||
book = maps:get(book, Ref, unknown),
|
||||
chapter = maps:get(chapter, Ref, 0),
|
||||
verse = maps:get(verse, Ref, 0)
|
||||
};
|
||||
(_) ->
|
||||
#torah_ref{book = unknown, chapter = 0, verse = 0}
|
||||
#torah_ref{book = unknown, chapter = 0, verse = 0}
|
||||
end,
|
||||
TorahRefs
|
||||
).
|
||||
@@ -307,9 +307,10 @@ store_all_opinions(CaseRef, Votes, Majority) ->
|
||||
%%====================================================================
|
||||
|
||||
convene_judges(Case, Size) ->
|
||||
%% Seat N performs step N of the dialectic (judge:step_for_seat/1).
|
||||
lists:map(
|
||||
fun(_N) ->
|
||||
{ok, Pid} = judge:start_link(Case),
|
||||
fun(Seat) ->
|
||||
{ok, Pid} = judge:start_link(Case, Seat),
|
||||
Pid
|
||||
end,
|
||||
lists:seq(1, Size)
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include("sanhedrin.hrl").
|
||||
|
||||
%% The consult callback the tablet-55 tests register (judge:consult reads {M,F}).
|
||||
-export([fake_consult/5]).
|
||||
|
||||
%% ============================================================
|
||||
%% Test fixture — start/stop the application for each test
|
||||
%% ============================================================
|
||||
@@ -382,6 +385,92 @@ judge_deliberate_sends_vote_test() ->
|
||||
end,
|
||||
judge:stop(JudgePid).
|
||||
|
||||
%% ============================================================
|
||||
%% Tablet 55 — the judge consults; it does not roll dice
|
||||
%% ============================================================
|
||||
|
||||
judge_seats_cycle_the_dialectic_test() ->
|
||||
?assertEqual(interpret, judge:step_for_seat(1)),
|
||||
?assertEqual(challenge, judge:step_for_seat(2)),
|
||||
?assertEqual(resolve, judge:step_for_seat(3)),
|
||||
?assertEqual(interpret, judge:step_for_seat(4)),
|
||||
?assertEqual(challenge, judge:step_for_seat(23)),
|
||||
?assertEqual(challenge, judge:step_for_seat(71)).
|
||||
|
||||
judge_without_consult_abstains_test() ->
|
||||
application:unset_env(sanhedrin, consult),
|
||||
Case = make_case_with_context(simple, <<"q">>, #{roots => [<<"br-a">>]}),
|
||||
{ok, JudgePid} = judge:start_link(Case, 1),
|
||||
judge:deliberate(JudgePid, self()),
|
||||
receive
|
||||
{vote, Vote} ->
|
||||
?assertEqual(abstain, Vote#vote.verdict),
|
||||
?assertEqual([], Vote#vote.torah_refs),
|
||||
?assertMatch({_, _}, binary:match(Vote#vote.reasoning, <<"no consultation">>))
|
||||
after 5000 -> ?assert(false)
|
||||
end,
|
||||
judge:stop(JudgePid).
|
||||
|
||||
judge_consult_resolved_accepts_with_verses_test() ->
|
||||
application:set_env(sanhedrin, consult, {?MODULE, fake_consult}),
|
||||
Case = make_case_with_context(simple, <<"q">>, #{roots => [<<"br-a">>], fake => resolved}),
|
||||
{ok, JudgePid} = judge:start_link(Case, 1),
|
||||
judge:deliberate(JudgePid, self()),
|
||||
receive
|
||||
{vote, Vote} ->
|
||||
?assertEqual(accept, Vote#vote.verdict),
|
||||
?assertEqual(<<"the reading">>, Vote#vote.position),
|
||||
?assertMatch([#torah_ref{book = bereishit, chapter = 1, verse = 1}], Vote#vote.torah_refs),
|
||||
?assertMatch({_, _}, binary:match(Vote#vote.reasoning, <<"interpret via the Talmudic Engine">>))
|
||||
after 5000 -> ?assert(false)
|
||||
end,
|
||||
judge:stop(JudgePid),
|
||||
application:unset_env(sanhedrin, consult).
|
||||
|
||||
judge_consult_raises_abstains_and_stands_test() ->
|
||||
application:set_env(sanhedrin, consult, {?MODULE, fake_consult}),
|
||||
Case = make_case_with_context(simple, <<"q">>, #{fake => raise}),
|
||||
{ok, JudgePid} = judge:start_link(Case, 2),
|
||||
judge:deliberate(JudgePid, self()),
|
||||
receive
|
||||
{vote, Vote} -> ?assertEqual(abstain, Vote#vote.verdict)
|
||||
after 5000 -> ?assert(false)
|
||||
end,
|
||||
?assert(is_process_alive(JudgePid)),
|
||||
judge:stop(JudgePid),
|
||||
application:unset_env(sanhedrin, consult).
|
||||
|
||||
%% A panel of three where the dialectic finds a kushya at seat 2:
|
||||
%% interpret → resolved, challenge → challenged, resolve → resolved.
|
||||
%% Majority 2 accept, minority 1 reject — the minority preserved.
|
||||
panel_of_three_dissent_from_the_dialectic_test() ->
|
||||
setup(),
|
||||
application:set_env(sanhedrin, consult, {?MODULE, fake_consult}),
|
||||
Case = make_case_with_context(monetary, <<"q">>, #{roots => [<<"br-a">>], fake => dialectic}),
|
||||
{ok, CaseRef} = escalation:submit_case(Case),
|
||||
timer:sleep(600),
|
||||
{ok, Decision} = clerk:get_decision(CaseRef),
|
||||
?assertEqual(3, Decision#decision.panel_size),
|
||||
?assertEqual(accepted, Decision#decision.outcome),
|
||||
?assertEqual(2, maps:get(vote_count, Decision#decision.majority)),
|
||||
?assertEqual(1, maps:get(vote_count, Decision#decision.minority)),
|
||||
?assertEqual(<<"but the verse says otherwise">>, maps:get(position, Decision#decision.minority)),
|
||||
application:unset_env(sanhedrin, consult),
|
||||
cleanup(ok).
|
||||
|
||||
%% The fake consultation the tests register. Step-aware for `dialectic`.
|
||||
fake_consult(_Step, _Ref, _Roots, _Q, #{fake := raise}) ->
|
||||
erlang:error(engine_exploded);
|
||||
fake_consult(_Step, _Ref, _Roots, _Q, #{fake := resolved}) ->
|
||||
{ok, #{status => <<"resolved">>, text => <<"the reading">>, verses => [{bereishit, 1, 1}]}};
|
||||
fake_consult(challenge, _Ref, _Roots, _Q, #{fake := dialectic}) ->
|
||||
{ok, #{status => <<"challenged">>, text => <<>>, kushya => <<"but the verse says otherwise">>,
|
||||
verses => [{devarim, 6, 4}]}};
|
||||
fake_consult(_Step, _Ref, _Roots, _Q, #{fake := dialectic}) ->
|
||||
{ok, #{status => <<"resolved">>, text => <<"the reading">>, verses => [{bereishit, 1, 1}]}};
|
||||
fake_consult(_Step, _Ref, _Roots, _Q, _Ctx) ->
|
||||
{error, no_fake}.
|
||||
|
||||
%% ============================================================
|
||||
%% Panel majority/minority computation tests
|
||||
%% ============================================================
|
||||
|
||||
Reference in new issue
Block a user