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