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,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}.
|
||||
@@ -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.
|
||||
@@ -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]
|
||||
}.
|
||||
@@ -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
|
||||
).
|
||||
@@ -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).
|
||||
@@ -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, []}}.
|
||||
@@ -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, []}
|
||||
]}.
|
||||
@@ -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.
|
||||
@@ -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}}.
|
||||
Reference in new issue
Block a user