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:
Scott KarpandClaude Sonnet 4.6 committed 2026-02-26 14:02:34 -05:00
1 parent 622e814c9e
commit 80ec364da6
86 files changed
+9646

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
%%%-------------------------------------------------------------------
%%% @doc Clerk — vote collection, opinion storage.
%%%
%%% Architecture §3.2:
%%% The clerk collects votes, stores opinions (both majority
%%% and minority), and maintains the decision record.
%%%
%%% Opinions are stored as append-only — consistent with
%%% L3 write protocol. Minority opinions are NEVER deleted.
%%% @end
%%%-------------------------------------------------------------------
-module(clerk).
-behaviour(gen_server).
-include("sanhedrin.hrl").
%% API
-export([start_link/0,
store_decision/2,
store_opinion/1,
get_decision/1,
get_opinions/1,
get_minority_opinions/1,
count_decisions/0,
count_opinions/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-define(SERVER, ?MODULE).
-record(state, {
decisions = #{} :: map(), %% case_ref => #decision{}
opinions = [] :: [#opinion{}] %% Append-only list
}).
%%====================================================================
%% API
%%====================================================================
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Store a deliberation decision.
-spec store_decision(binary(), #decision{}) -> ok.
store_decision(CaseRef, Decision) ->
gen_server:cast(?SERVER, {store_decision, CaseRef, Decision}).
%% @doc Store an individual opinion (majority or minority).
%% Opinions are NEVER deleted. Both tablets are in the aron.
-spec store_opinion(#opinion{}) -> ok.
store_opinion(Opinion) ->
gen_server:cast(?SERVER, {store_opinion, Opinion}).
%% @doc Retrieve a decision by case reference.
-spec get_decision(binary()) -> {ok, #decision{}} | {error, not_found}.
get_decision(CaseRef) ->
gen_server:call(?SERVER, {get_decision, CaseRef}).
%% @doc Retrieve all opinions for a case reference.
-spec get_opinions(binary()) -> [#opinion{}].
get_opinions(CaseRef) ->
gen_server:call(?SERVER, {get_opinions, CaseRef}).
%% @doc Retrieve only minority opinions for a case reference.
-spec get_minority_opinions(binary()) -> [#opinion{}].
get_minority_opinions(CaseRef) ->
gen_server:call(?SERVER, {get_minority_opinions, CaseRef}).
%% @doc Count total stored decisions.
-spec count_decisions() -> non_neg_integer().
count_decisions() ->
gen_server:call(?SERVER, count_decisions).
%% @doc Count total stored opinions.
-spec count_opinions() -> non_neg_integer().
count_opinions() ->
gen_server:call(?SERVER, count_opinions).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
{ok, #state{}}.
handle_call({get_decision, CaseRef}, _From, State) ->
case maps:find(CaseRef, State#state.decisions) of
{ok, Decision} -> {reply, {ok, Decision}, State};
error -> {reply, {error, not_found}, State}
end;
handle_call({get_opinions, CaseRef}, _From, State) ->
Opinions = [O || O <- State#state.opinions,
O#opinion.case_ref == CaseRef],
{reply, Opinions, State};
handle_call({get_minority_opinions, CaseRef}, _From, State) ->
Opinions = [O || O <- State#state.opinions,
O#opinion.case_ref == CaseRef,
O#opinion.is_majority == false],
{reply, Opinions, State};
handle_call(count_decisions, _From, State) ->
{reply, maps:size(State#state.decisions), State};
handle_call(count_opinions, _From, State) ->
{reply, length(State#state.opinions), State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast({store_decision, CaseRef, Decision}, State) ->
NewDecisions = maps:put(CaseRef, Decision, State#state.decisions),
{noreply, State#state{decisions = NewDecisions}};
handle_cast({store_opinion, Opinion}, State) ->
%% Append-only — opinions are never removed
NewOpinions = [Opinion | State#state.opinions],
{noreply, State#state{opinions = NewOpinions}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
+111
View File
@@ -0,0 +1,111 @@
%%%-------------------------------------------------------------------
%%% @doc Escalation — assess complexity and route cases to panels.
%%%
%%% Architecture §3.2:
%%% assess(Case) determines complexity, route/2 convenes the panel.
%%%
%%% Panel sizes from halakha:
%%% simple -> 1 judge (individual matters)
%%% monetary -> 3 judges (dinei mamonot)
%%% capital -> 23 judges (dinei nefashot)
%%% novel -> 71 judges (Sanhedrin Gedolah)
%%% @end
%%%-------------------------------------------------------------------
-module(escalation).
-behaviour(gen_server).
-include("sanhedrin.hrl").
%% API
-export([start_link/0, submit_case/1, assess/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-define(SERVER, ?MODULE).
-record(state, {
active_cases = #{} :: map(), %% case_ref => panel_pid
case_count = 0 :: non_neg_integer()
}).
%%====================================================================
%% API
%%====================================================================
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Submit a case for deliberation.
%% Returns {ok, CaseRef} or {error, Reason}.
-spec submit_case(#sanhedrin_case{}) -> {ok, binary()} | {error, term()}.
submit_case(Case) ->
gen_server:call(?SERVER, {submit_case, Case}, 30000).
%% @doc Assess complexity of a case. Pure function.
-spec assess(#sanhedrin_case{}) -> {panel, pos_integer()}.
assess(#sanhedrin_case{complexity = simple}) -> {panel, ?PANEL_SIMPLE};
assess(#sanhedrin_case{complexity = monetary}) -> {panel, ?PANEL_MONETARY};
assess(#sanhedrin_case{complexity = capital}) -> {panel, ?PANEL_CAPITAL};
assess(#sanhedrin_case{complexity = novel}) -> {panel, ?PANEL_NOVEL}.
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
{ok, #state{}}.
handle_call({submit_case, Case}, _From, State) ->
%% First check halt conditions
case halt_monitor:check_case(Case) of
clear ->
%% Assess complexity and route
{panel, Size} = assess(Case),
Timeout = timeout_for(Case#sanhedrin_case.complexity),
case panel_sup:start_panel(Case, Size) of
{ok, PanelPid} ->
%% Start deliberation asynchronously
panel:deliberate_async(PanelPid, Timeout),
NewActive = maps:put(
Case#sanhedrin_case.case_ref,
PanelPid,
State#state.active_cases
),
{reply,
{ok, Case#sanhedrin_case.case_ref},
State#state{
active_cases = NewActive,
case_count = State#state.case_count + 1
}};
{error, Reason} ->
{reply, {error, {panel_start_failed, Reason}}, State}
end;
{halt, Condition, Reason} ->
{reply, {error, {halted, Condition, Reason}}, State}
end;
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({panel_decided, CaseRef, Decision}, State) ->
%% Panel has reached a decision — store via clerk
clerk:store_decision(CaseRef, Decision),
NewActive = maps:remove(CaseRef, State#state.active_cases),
{noreply, State#state{active_cases = NewActive}};
handle_info(_Info, State) ->
{noreply, State}.
%%====================================================================
%% Internal
%%====================================================================
timeout_for(simple) -> ?TIMEOUT_SIMPLE;
timeout_for(monetary) -> ?TIMEOUT_MONETARY;
timeout_for(capital) -> ?TIMEOUT_CAPITAL;
timeout_for(novel) -> ?TIMEOUT_NOVEL.
+177
View File
@@ -0,0 +1,177 @@
%%%-------------------------------------------------------------------
%%% @doc Halt monitor — monitors for the 3 cardinal sin conditions.
%%%
%%% Architecture §8 (Halt Conditions):
%%% The system MUST halt on detection of:
%%% 1. Avodah Zarah — treating non-Torah source as authoritative
%%% 2. Shfichut Damim — destroying a tzelem carrier (human data/identity)
%%% 3. Gilui Arayot — collapsing required distinctions
%%%
%%% These mirror the Talmudic Engine halt modules (Prolog)
%%% and the Vav Pipeline halt_conditions (Elixir).
%%% @end
%%%-------------------------------------------------------------------
-module(halt_monitor).
-behaviour(gen_server).
-include("sanhedrin.hrl").
%% API
-export([start_link/0, check_case/1, check_vote/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-define(SERVER, ?MODULE).
-record(state, {
halt_count = 0 :: non_neg_integer(),
halt_log = [] :: [{integer(), atom(), binary()}]
}).
%%====================================================================
%% API
%%====================================================================
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Check a case submission for halt conditions.
%% Returns 'clear' or {halt, Condition, Reason}.
-spec check_case(#sanhedrin_case{}) -> clear | {halt, atom(), binary()}.
check_case(Case) ->
gen_server:call(?SERVER, {check_case, Case}).
%% @doc Check a vote for halt conditions.
-spec check_vote(#vote{}) -> clear | {halt, atom(), binary()}.
check_vote(Vote) ->
gen_server:call(?SERVER, {check_vote, Vote}).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
{ok, #state{}}.
handle_call({check_case, Case}, _From, State) ->
Result = do_check_case(Case),
NewState = maybe_log_halt(Result, State),
{reply, Result, NewState};
handle_call({check_vote, Vote}, _From, State) ->
Result = do_check_vote(Vote),
NewState = maybe_log_halt(Result, State),
{reply, Result, NewState};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
%%====================================================================
%% Internal — Halt condition checks
%%====================================================================
do_check_case(Case) ->
Context = Case#sanhedrin_case.context,
with_chain([
fun() -> check_avodah_zarah(Context) end,
fun() -> check_shfichut_damim(Context) end,
fun() -> check_gilui_arayot(Context) end
]).
do_check_vote(Vote) ->
%% Check vote content for halt conditions
Position = Vote#vote.position,
with_chain([
fun() -> check_authority_in_text(Position) end
]).
%% Run checks in chain — return first halt or clear
with_chain([]) -> clear;
with_chain([Check | Rest]) ->
case Check() of
clear -> with_chain(Rest);
Halt -> Halt
end.
%%--------------------------------------------------------------------
%% 1. Avodah Zarah — idolatry detection
%%--------------------------------------------------------------------
check_avodah_zarah(Context) ->
%% Check if any non-Torah source is treated as authoritative
AuthoritySource = maps:get(authority_source, Context, torah),
ValidSources = [torah, l1, specification, sanhedrin_deliberation],
case lists:member(AuthoritySource, ValidSources) of
true -> clear;
false ->
{halt, avodah_zarah,
iolist_to_binary([
<<"Non-Torah authority detected: ">>,
atom_to_binary(AuthoritySource, utf8)
])}
end.
%%--------------------------------------------------------------------
%% 2. Shfichut Damim — destruction detection
%%--------------------------------------------------------------------
check_shfichut_damim(Context) ->
%% Check if the case would result in destruction
Action = maps:get(action, Context, read),
DestructiveActions = [delete, destroy, overwrite, erase],
case lists:member(Action, DestructiveActions) of
true ->
{halt, shfichut_damim,
iolist_to_binary([
<<"Destructive action: ">>,
atom_to_binary(Action, utf8)
])};
false -> clear
end.
%%--------------------------------------------------------------------
%% 3. Gilui Arayot — boundary violation detection
%%--------------------------------------------------------------------
%% The 13 required distinctions that must never collapse:
%% specification/interpretation, L1/L3, Torah/external_model,
%% ketiv/qere, governance/provision, majority/minority,
%% reception/processing, read/write, immutable/mutable,
%% sacred/profane, Shabbat/weekday, input/output, human/system
check_gilui_arayot(Context) ->
BoundaryViolation = maps:get(boundary_violation, Context, none),
case BoundaryViolation of
none -> clear;
false -> clear;
Violation ->
{halt, gilui_arayot,
iolist_to_binary([
<<"Boundary collapsed: ">>,
io_lib:format("~p", [Violation])
])}
end.
%%--------------------------------------------------------------------
%% Text-level authority check (for vote content)
%%--------------------------------------------------------------------
check_authority_in_text(_Position) ->
%% Phase 2: basic check. Phase 3 will use Talmudic Engine
%% to detect authority attribution in natural language.
clear.
%%--------------------------------------------------------------------
%% Logging
%%--------------------------------------------------------------------
maybe_log_halt(clear, State) -> State;
maybe_log_halt({halt, Condition, Reason}, State) ->
Now = erlang:system_time(millisecond),
Entry = {Now, Condition, Reason},
State#state{
halt_count = State#state.halt_count + 1,
halt_log = [Entry | State#state.halt_log]
}.
+189
View File
@@ -0,0 +1,189 @@
%%%-------------------------------------------------------------------
%%% @doc Judge — individual deliberator (gen_server).
%%%
%%% Architecture §3.2:
%%% Each judge:
%%% 1. Receives the case
%%% 2. Consults the specification (L0/L1)
%%% 3. Generates an independent interpretation
%%% 4. Casts a vote with reasoning
%%% 5. Reasoning is preserved regardless of outcome
%%%
%%% Judges deliberate independently — they do not see
%%% other judges' votes until the panel collects them.
%%% @end
%%%-------------------------------------------------------------------
-module(judge).
-behaviour(gen_server).
-include("sanhedrin.hrl").
%% API
-export([start_link/1, deliberate/2, stop/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-record(state, {
judge_id :: reference(),
case_data :: #sanhedrin_case{},
vote :: #vote{} | undefined,
panel_pid :: pid() | undefined
}).
%%====================================================================
%% API
%%====================================================================
start_link(Case) ->
gen_server:start_link(?MODULE, Case, []).
%% @doc Instruct this judge to deliberate and send vote to PanelPid.
-spec deliberate(pid(), pid()) -> ok.
deliberate(JudgePid, PanelPid) ->
gen_server:cast(JudgePid, {deliberate, PanelPid}).
%% @doc Stop this judge.
-spec stop(pid()) -> ok.
stop(JudgePid) ->
gen_server:stop(JudgePid, normal, 5000).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init(Case) ->
JudgeId = make_ref(),
{ok, #state{
judge_id = JudgeId,
case_data = Case,
vote = undefined,
panel_pid = undefined
}}.
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),
%% Send vote to panel
PanelPid ! {vote, Vote},
{noreply, State#state{vote = Vote, panel_pid = PanelPid}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
%%====================================================================
%% Internal — Deliberation logic
%%====================================================================
%% @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) ->
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),
#vote{
judge_id = JudgeId,
position = Position,
reasoning = Reasoning,
torah_refs = VoteRefs,
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}
end.
format_reasoning(Base, RootCount, RefCount) ->
iolist_to_binary([
Base,
<<" (">>,
integer_to_binary(RootCount),
<<" roots, ">>,
integer_to_binary(RefCount),
<<" references examined)">>
]).
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;
(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 = unknown, chapter = 0, verse = 0}
end,
TorahRefs
).
+323
View File
@@ -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).
+59
View File
@@ -0,0 +1,59 @@
%%%-------------------------------------------------------------------
%%% @doc Panel supervisor — dynamic supervisor for deliberation panels.
%%%
%%% Architecture §3.2:
%%% One panel_sup per active case. Supervises the judge processes
%%% for that panel.
%%%
%%% Uses simple_one_for_one strategy — all children are judges
%%% of the same type, started dynamically.
%%% @end
%%%-------------------------------------------------------------------
-module(panel_sup).
-behaviour(supervisor).
%% API
-export([start_link/0, start_panel/2]).
%% supervisor callbacks
-export([init/1]).
-include("sanhedrin.hrl").
-define(SERVER, ?MODULE).
%%====================================================================
%% API
%%====================================================================
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%% @doc Start a new deliberation panel for a case.
%% Spawns a panel process that manages its own judges.
-spec start_panel(#sanhedrin_case{}, pos_integer()) -> {ok, pid()} | {error, term()}.
start_panel(Case, Size) ->
%% Start panel as a child of this supervisor
ChildSpec = #{
id => {panel, Case#sanhedrin_case.case_ref},
start => {panel, start_link, [Case, Size]},
restart => temporary, %% Panels are not restarted — a failed deliberation is teiku
shutdown => 10000,
type => worker,
modules => [panel]
},
supervisor:start_child(?SERVER, ChildSpec).
%%====================================================================
%% supervisor callbacks
%%====================================================================
init([]) ->
SupFlags = #{
strategy => one_for_one,
intensity => 10,
period => 60
},
%% No static children — panels are started dynamically
{ok, {SupFlags, []}}.
+16
View File
@@ -0,0 +1,16 @@
{application, sanhedrin, [
{description, "Concurrent deliberation engine — Sanhedrin panels with mandatory dissent"},
{vsn, "0.1.0"},
{registered, [sanhedrin_sup, clerk, escalation]},
{mod, {sanhedrin_app, []}},
{applications, [
kernel,
stdlib,
crypto,
jsx
]},
{env, []},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, []}
]}.
+20
View File
@@ -0,0 +1,20 @@
%%%-------------------------------------------------------------------
%%% @doc Sanhedrin OTP application.
%%%
%%% Architecture §3.2: Concurrent deliberation engine.
%%% Spawns panels of independent deliberators, collects votes,
%%% enforces mandatory dissent on capital decisions,
%%% stores minority opinions.
%%% @end
%%%-------------------------------------------------------------------
-module(sanhedrin_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
sanhedrin_sup:start_link().
stop(_State) ->
ok.
+70
View File
@@ -0,0 +1,70 @@
%%%-------------------------------------------------------------------
%%% @doc Sanhedrin top-level supervisor.
%%%
%%% Architecture §3.2 supervision tree:
%%% sanhedrin_sup (top supervisor)
%%% ├── clerk (gen_server — vote collection, opinion storage)
%%% ├── escalation (gen_server — routes cases to appropriate panel size)
%%% └── halt_monitor (gen_server — monitors for 3 cardinal sin conditions)
%%%
%%% Panel supervisors are started dynamically by escalation
%%% via panel_sup:start_panel/2.
%%% @end
%%%-------------------------------------------------------------------
-module(sanhedrin_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
SupFlags = #{
strategy => one_for_one,
intensity => 5,
period => 60
},
ChildSpecs = [
%% Clerk — vote collection and opinion storage
#{
id => clerk,
start => {clerk, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [clerk]
},
%% Escalation — complexity assessment and case routing
#{
id => escalation,
start => {escalation, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [escalation]
},
%% Halt monitor — 3 cardinal sin conditions
#{
id => halt_monitor,
start => {halt_monitor, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [halt_monitor]
},
%% Panel supervisor — dynamic, starts panels on demand
#{
id => panel_sup,
start => {panel_sup, start_link, []},
restart => permanent,
shutdown => infinity,
type => supervisor,
modules => [panel_sup]
}
],
{ok, {SupFlags, ChildSpecs}}.