Phase 1-2 build: Steps 5-6, 8-13
Shoresh Engine (Haskell, Step 5) - 12 modules: Types, Lexicon, Binyanim, Teamim, Gematria, Graph, KetivQere, Smikhut, Havdalah, Kriah, KiTov, Firmware - 33 EUnit tests, 0 failures - Firmware module verifies L1 SHA-256 hash at startup Talmudic Engine (SWI-Prolog, Step 6) - 15 modules: halt, reasoning, specification, interface - 57 PlUnit tests, 0 failures - Three cardinal halt conditions: avodah_zarah, shfichut_damim, gilui_arayot Vav Pipeline (Elixir, Steps 8-11) - 14 modules: BereishitCycle, Shema, Cloud, Manna, L1Reader, Specification, HaltConditions, ShabbatTimer, CycleLog, Chatzotzrot, ShoreshEngineClient, TalmudicEngineClient, Application - 46 ExUnit tests, 0 failures - Nishma-first architecture: hears before it speaks Sanhedrin Framework (Erlang/OTP, Step 12) - 8 modules: sanhedrin_app, sanhedrin_sup, escalation, panel_sup, panel, judge, clerk, halt_monitor - 46 EUnit tests, 0 failures (includes fix: rejected outcome now reachable) - Panel sizes: 1/3/23/71 — unanimous capital guilty → acquit (Sanhedrin 17a) - Minority opinions append-only, never deleted Korban Processor (Python, Step 13) - 6 modules: korban, shechitah, semichah, zerikah, haktarah, router Builder tooling: AGENTS.md, opencode.json, memory/, src/tools/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
622e814c9e
commit
80ec364da6
86 files changed
+9646
No files matched your search
@@ -0,0 +1,323 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Panel — manage a deliberation panel.
|
||||
%%%
|
||||
%%% Architecture §3.2:
|
||||
%%% convene/3 spawns judges, deliberate/2 collects votes.
|
||||
%%%
|
||||
%%% THE CRITICAL CHECK: unanimous conviction on capital case
|
||||
%%% triggers suspicion of consensus — acquit.
|
||||
%%%
|
||||
%%% Minority opinions are ALWAYS preserved.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(panel).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("sanhedrin.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_link/2, deliberate_async/2, get_state/1]).
|
||||
%% Exported for testing
|
||||
-export([check_unanimous/2, compute_majority_minority/1, determine_outcome/3]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
|
||||
|
||||
-record(state, {
|
||||
panel :: #panel{},
|
||||
case_data :: #sanhedrin_case{},
|
||||
timeout :: pos_integer(),
|
||||
escalation :: pid() | undefined,
|
||||
timer_ref :: reference() | undefined
|
||||
}).
|
||||
|
||||
%%====================================================================
|
||||
%% API
|
||||
%%====================================================================
|
||||
|
||||
start_link(Case, Size) ->
|
||||
gen_server:start_link(?MODULE, {Case, Size}, []).
|
||||
|
||||
%% @doc Start deliberation asynchronously.
|
||||
%% The panel will collect votes and send the result
|
||||
%% back to escalation when done.
|
||||
-spec deliberate_async(pid(), pos_integer()) -> ok.
|
||||
deliberate_async(PanelPid, Timeout) ->
|
||||
gen_server:cast(PanelPid, {deliberate, Timeout}).
|
||||
|
||||
%% @doc Get current panel state (for testing/inspection).
|
||||
-spec get_state(pid()) -> map().
|
||||
get_state(PanelPid) ->
|
||||
gen_server:call(PanelPid, get_state).
|
||||
|
||||
%%====================================================================
|
||||
%% gen_server callbacks
|
||||
%%====================================================================
|
||||
|
||||
init({Case, Size}) ->
|
||||
PanelId = generate_id(),
|
||||
Now = erlang:system_time(millisecond),
|
||||
|
||||
%% Spawn judges
|
||||
Judges = convene_judges(Case, Size),
|
||||
|
||||
Panel = #panel{
|
||||
panel_id = PanelId,
|
||||
case_ref = Case#sanhedrin_case.case_ref,
|
||||
size = Size,
|
||||
judges = Judges,
|
||||
votes = [],
|
||||
status = convened,
|
||||
convened_at = Now,
|
||||
decided_at = undefined
|
||||
},
|
||||
|
||||
{ok, #state{
|
||||
panel = Panel,
|
||||
case_data = Case,
|
||||
timeout = ?TIMEOUT_MONETARY,
|
||||
escalation = whereis(escalation)
|
||||
}}.
|
||||
|
||||
handle_call(get_state, _From, State) ->
|
||||
P = State#state.panel,
|
||||
Reply = #{
|
||||
panel_id => P#panel.panel_id,
|
||||
case_ref => P#panel.case_ref,
|
||||
size => P#panel.size,
|
||||
status => P#panel.status,
|
||||
vote_count => length(P#panel.votes)
|
||||
},
|
||||
{reply, Reply, State};
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, {error, unknown_request}, State}.
|
||||
|
||||
handle_cast({deliberate, Timeout}, State) ->
|
||||
Panel = State#state.panel,
|
||||
|
||||
%% Instruct all judges to deliberate
|
||||
lists:foreach(
|
||||
fun(JudgePid) ->
|
||||
judge:deliberate(JudgePid, self())
|
||||
end,
|
||||
Panel#panel.judges
|
||||
),
|
||||
|
||||
%% Set timeout for deliberation
|
||||
TimerRef = erlang:send_after(Timeout, self(), deliberation_timeout),
|
||||
|
||||
NewPanel = Panel#panel{status = deliberating},
|
||||
{noreply, State#state{
|
||||
panel = NewPanel,
|
||||
timeout = Timeout,
|
||||
timer_ref = TimerRef
|
||||
}};
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info({vote, Vote}, State) ->
|
||||
Panel = State#state.panel,
|
||||
NewVotes = [Vote | Panel#panel.votes],
|
||||
NewPanel = Panel#panel{votes = NewVotes},
|
||||
NewState = State#state{panel = NewPanel},
|
||||
|
||||
%% Check if all votes are in
|
||||
case length(NewVotes) >= Panel#panel.size of
|
||||
true ->
|
||||
%% All votes collected — decide
|
||||
maybe_cancel_timer(State#state.timer_ref),
|
||||
decide(NewState);
|
||||
false ->
|
||||
{noreply, NewState}
|
||||
end;
|
||||
|
||||
handle_info(deliberation_timeout, State) ->
|
||||
%% Timeout — decide with whatever votes we have
|
||||
decide(State);
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, State) ->
|
||||
%% Ensure all judges are stopped
|
||||
Panel = State#state.panel,
|
||||
lists:foreach(
|
||||
fun(JudgePid) ->
|
||||
case is_process_alive(JudgePid) of
|
||||
true -> judge:stop(JudgePid);
|
||||
false -> ok
|
||||
end
|
||||
end,
|
||||
Panel#panel.judges
|
||||
),
|
||||
ok.
|
||||
|
||||
%%====================================================================
|
||||
%% Internal — Deliberation logic
|
||||
%%====================================================================
|
||||
|
||||
%% @doc Decide based on collected votes.
|
||||
%% THE CRITICAL CHECK: unanimous guilty on capital case → suspicion → acquit.
|
||||
decide(State) ->
|
||||
Panel = State#state.panel,
|
||||
Votes = Panel#panel.votes,
|
||||
Now = erlang:system_time(millisecond),
|
||||
|
||||
{Majority, Minority} = compute_majority_minority(Votes),
|
||||
UnanimousCheck = check_unanimous(Panel#panel.size, Votes),
|
||||
|
||||
Decision = #decision{
|
||||
case_ref = Panel#panel.case_ref,
|
||||
outcome = determine_outcome(Majority, Minority, UnanimousCheck),
|
||||
majority = Majority,
|
||||
minority = Minority,
|
||||
panel_size = Panel#panel.size,
|
||||
deliberation_time_ms = Now - Panel#panel.convened_at,
|
||||
unanimous_check = UnanimousCheck
|
||||
},
|
||||
|
||||
%% Store all opinions via clerk (majority AND minority)
|
||||
store_all_opinions(Panel#panel.case_ref, Votes, Majority),
|
||||
|
||||
%% Notify escalation
|
||||
case State#state.escalation of
|
||||
undefined -> ok;
|
||||
EscPid when is_pid(EscPid) ->
|
||||
EscPid ! {panel_decided, Panel#panel.case_ref, Decision}
|
||||
end,
|
||||
|
||||
FinalPanel = Panel#panel{
|
||||
status = decided,
|
||||
decided_at = Now
|
||||
},
|
||||
|
||||
{stop, normal, State#state{panel = FinalPanel}}.
|
||||
|
||||
%% @doc Compute majority and minority positions.
|
||||
compute_majority_minority([]) ->
|
||||
{#{position => <<"no votes">>,
|
||||
reasoning => <<"no judges voted">>,
|
||||
torah_refs => [],
|
||||
vote_count => 0},
|
||||
#{position => <<"no dissent">>,
|
||||
reasoning => <<"no judges voted">>,
|
||||
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],
|
||||
|
||||
{MajVotes, MinVotes} =
|
||||
case length(Accepts) >= length(Rejects) of
|
||||
true -> {Accepts, Rejects ++ Abstains};
|
||||
false -> {Rejects, Accepts ++ Abstains}
|
||||
end,
|
||||
|
||||
MajSample = case MajVotes of
|
||||
[First | _] -> First;
|
||||
[] -> #vote{position = <<"none">>, reasoning = <<"none">>,
|
||||
torah_refs = [], verdict = abstain,
|
||||
judge_id = make_ref(), cast_at = 0}
|
||||
end,
|
||||
MinSample = case MinVotes of
|
||||
[MFirst | _] -> MFirst;
|
||||
[] -> #vote{position = <<"no dissent">>,
|
||||
reasoning = <<"unanimous">>,
|
||||
torah_refs = [], verdict = abstain,
|
||||
judge_id = make_ref(), cast_at = 0}
|
||||
end,
|
||||
|
||||
Majority = #{
|
||||
position => MajSample#vote.position,
|
||||
reasoning => MajSample#vote.reasoning,
|
||||
torah_refs => MajSample#vote.torah_refs,
|
||||
vote_count => length(MajVotes),
|
||||
verdict => MajSample#vote.verdict
|
||||
},
|
||||
Minority = #{
|
||||
position => MinSample#vote.position,
|
||||
reasoning => MinSample#vote.reasoning,
|
||||
torah_refs => MinSample#vote.torah_refs,
|
||||
vote_count => length(MinVotes),
|
||||
verdict => MinSample#vote.verdict
|
||||
},
|
||||
{Majority, Minority}.
|
||||
|
||||
%% @doc THE CRITICAL CHECK.
|
||||
%% Architecture §3.2: unanimous conviction on capital case
|
||||
%% triggers suspicion of malfunction — acquit.
|
||||
%%
|
||||
%% "If the Sanhedrin unanimously finds guilty, the accused is acquitted."
|
||||
%% — Sanhedrin 17a
|
||||
check_unanimous(Size, Votes) when Size >= ?PANEL_CAPITAL ->
|
||||
AllGuilty = lists:all(
|
||||
fun(V) -> V#vote.verdict == reject end,
|
||||
Votes
|
||||
),
|
||||
case AllGuilty andalso length(Votes) > 0 of
|
||||
true -> {failed, suspicion_of_consensus};
|
||||
false -> passed
|
||||
end;
|
||||
check_unanimous(_Size, _Votes) ->
|
||||
%% Unanimous check only applies to capital+ panels
|
||||
passed.
|
||||
|
||||
%% @doc Determine the outcome from majority, minority, and unanimous check.
|
||||
determine_outcome(_Majority, _Minority, {failed, suspicion_of_consensus}) ->
|
||||
%% Unanimous guilty on capital → acquit (accepted, not rejected)
|
||||
accepted;
|
||||
determine_outcome(Majority, _Minority, passed) ->
|
||||
MajCount = maps:get(vote_count, Majority, 0),
|
||||
MajVerdict = maps:get(verdict, Majority, abstain),
|
||||
case {MajCount, MajVerdict} of
|
||||
{0, _} -> teiku;
|
||||
{_, accept} -> accepted;
|
||||
{_, reject} -> rejected;
|
||||
{_, abstain} -> teiku
|
||||
end.
|
||||
|
||||
%% @doc Store all opinions via clerk — majority AND minority preserved.
|
||||
store_all_opinions(CaseRef, Votes, Majority) ->
|
||||
MajPosition = maps:get(position, Majority, <<>>),
|
||||
lists:foreach(
|
||||
fun(Vote) ->
|
||||
IsMajority = (Vote#vote.position == MajPosition),
|
||||
Opinion = #opinion{
|
||||
case_ref = CaseRef,
|
||||
judge_id = Vote#vote.judge_id,
|
||||
position = Vote#vote.position,
|
||||
reasoning = Vote#vote.reasoning,
|
||||
torah_refs = Vote#vote.torah_refs,
|
||||
verdict = Vote#vote.verdict,
|
||||
is_majority = IsMajority,
|
||||
stored_at = erlang:system_time(millisecond)
|
||||
},
|
||||
clerk:store_opinion(Opinion)
|
||||
end,
|
||||
Votes
|
||||
).
|
||||
|
||||
%%====================================================================
|
||||
%% Internal — Panel setup
|
||||
%%====================================================================
|
||||
|
||||
convene_judges(Case, Size) ->
|
||||
lists:map(
|
||||
fun(_N) ->
|
||||
{ok, Pid} = judge:start_link(Case),
|
||||
Pid
|
||||
end,
|
||||
lists:seq(1, Size)
|
||||
).
|
||||
|
||||
generate_id() ->
|
||||
Bytes = crypto:strong_rand_bytes(8),
|
||||
base64:encode(Bytes).
|
||||
|
||||
maybe_cancel_timer(undefined) -> ok;
|
||||
maybe_cancel_timer(Ref) -> erlang:cancel_timer(Ref).
|
||||
Reference in new issue
Block a user