Files
bikoret/sanhedrin/src/clerk.erl
T
Scott KarpandClaude Sonnet 4.6 80ec364da6 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>
2026-02-26 14:02:34 -05:00

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}.