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>
128 lines
4.1 KiB
Erlang
128 lines
4.1 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @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}.
|