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

+20
View File
@@ -0,0 +1,20 @@
.rebar3
_build
_checkouts
_vendor
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
.idea
*.iml
rebar3.crashdump
*~
+186
View File
@@ -0,0 +1,186 @@
# Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
## 1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
## 2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
## 3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
## 4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
1. You must give any other recipients of the Work or Derivative Works a copy of
this License; and
2. You must cause any modified files to carry prominent notices stating that
You changed the files; and
3. You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices from
the Source form of the Work, excluding those notices that do not pertain to
any part of the Derivative Works; and
4. If the Work includes a "NOTICE" text file as part of its distribution, then
any Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least
one of the following places: within a NOTICE text file distributed as part
of the Derivative Works; within the Source form or documentation, if
provided along with the Derivative Works; or, within a display generated by
the Derivative Works, if and wherever such third-party notices normally
appear. The contents of the NOTICE file are for informational purposes only
and do not modify the License. You may add Your own attribution notices
within Derivative Works that You distribute, alongside or as an addendum to
the NOTICE text from the Work, provided that such additional attribution
notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
## 5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
## 6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
## 7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE, NON-
INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
## 8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
## 9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2026, Scott Karp <github.carded456@passmail.net>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+9
View File
@@ -0,0 +1,9 @@
sanhedrin
=====
An OTP application
Build
-----
$ rebar3 compile
+101
View File
@@ -0,0 +1,101 @@
%%% sanhedrin.hrl — Record definitions for the Sanhedrin deliberation engine
%%%
%%% Architecture §3.2: Concurrent deliberation engine.
%%% Spawns panels of independent deliberators, collects votes,
%%% enforces mandatory dissent on capital decisions,
%%% stores minority opinions.
%% --- Case complexity levels ---
%% simple -> 1 judge
%% monetary -> 3 judges (dinei mamonot)
%% capital -> 23 judges (dinei nefashot)
%% novel -> 71 judges (Sanhedrin Gedolah)
-type complexity() :: simple | monetary | capital | novel.
%% --- Case outcome ---
-type outcome() :: accepted | rejected | teiku.
%% --- Unanimous check result ---
-type unanimous_check() :: passed | {failed, suspicion_of_consensus}.
%% --- Torah reference ---
-record(torah_ref, {
book :: atom(), %% bereishit | shemot | vayikra | bamidbar | devarim
chapter :: pos_integer(),
verse :: pos_integer()
}).
%% --- A root with domain ---
-record(root_info, {
shoresh :: list(atom()), %% e.g. [bet, resh, aleph]
domain :: atom() %% e.g. creation, justice, holiness
}).
%% --- Case submission (Pipeline → Sanhedrin) ---
%% Construction spec §3.2
-record(sanhedrin_case, {
case_ref :: binary(), %% UUID
complexity :: complexity(),
question :: binary(),
context :: map(), %% #{roots, torah_refs, prior_interpretations, shema_context}
requested_by :: binary(),
submitted_at :: integer() %% erlang:system_time(millisecond)
}).
%% --- Individual judge vote ---
-record(vote, {
judge_id :: reference(),
position :: binary(), %% The interpretation
reasoning :: binary(), %% Why this position
torah_refs :: [#torah_ref{}], %% Grounding references
verdict :: accept | reject | abstain,
cast_at :: integer()
}).
%% --- Panel record ---
-record(panel, {
panel_id :: binary(),
case_ref :: binary(),
size :: pos_integer(), %% 1, 3, 23, or 71
judges :: [pid()],
votes :: [#vote{}],
status :: convened | deliberating | decided | dissolved,
convened_at :: integer(),
decided_at :: integer() | undefined
}).
%% --- Deliberation decision ---
%% Construction spec §3.2 — output format
-record(decision, {
case_ref :: binary(),
outcome :: outcome(),
majority :: map(), %% #{position, reasoning, torah_refs, vote_count}
minority :: map(), %% #{position, reasoning, torah_refs, vote_count}
panel_size :: pos_integer(),
deliberation_time_ms :: non_neg_integer(),
unanimous_check :: unanimous_check()
}).
%% --- Opinion record (stored by clerk, never deleted) ---
-record(opinion, {
case_ref :: binary(),
judge_id :: reference(),
position :: binary(),
reasoning :: binary(),
torah_refs :: [#torah_ref{}],
verdict :: accept | reject | abstain,
is_majority :: boolean(),
stored_at :: integer()
}).
%% --- Panel size constants ---
-define(PANEL_SIMPLE, 1).
-define(PANEL_MONETARY, 3).
-define(PANEL_CAPITAL, 23).
-define(PANEL_NOVEL, 71).
%% --- Deliberation timeouts (Construction spec §2.2) ---
-define(TIMEOUT_SIMPLE, 15000). %% 15 seconds
-define(TIMEOUT_MONETARY, 15000). %% 15 seconds
-define(TIMEOUT_CAPITAL, 120000). %% 120 seconds
-define(TIMEOUT_NOVEL, 600000). %% 600 seconds
+13
View File
@@ -0,0 +1,13 @@
%% Construction spec §1.5 — Erlang dependencies (exact)
{erl_opts, [debug_info, {i, "include"}]}.
{deps, [
{jsx, "3.1.0"}
]}.
{shell, [
{apps, [sanhedrin]}
]}.
%% EUnit
{eunit_opts, [verbose]}.
+8
View File
@@ -0,0 +1,8 @@
{"1.2.0",
[{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}]}.
[
{pkg_hash,[
{<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}]},
{pkg_hash_ext,[
{<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}]}
].
+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}}.
+501
View File
@@ -0,0 +1,501 @@
%%%-------------------------------------------------------------------
%%% @doc Sanhedrin test suite — EUnit tests.
%%%
%%% Architecture §6.1 Day 4 — Sanhedrin inauguration:
%%% - Convene test panels at every size (1, 3, 23, 71)
%%% - Verify unanimous guilty on capital case triggers
%%% suspicion-of-consensus acquittal
%%% - Verify minority opinions are stored
%%% - Verify deliberation completes within time budgets
%%%
%%% Architecture §6.2 — Integration Tests:
%%% - case → panel → votes → decision with minority preserved
%%% @end
%%%-------------------------------------------------------------------
-module(sanhedrin_SUITE).
-include_lib("eunit/include/eunit.hrl").
-include("sanhedrin.hrl").
%% ============================================================
%% Test fixture — start/stop the application for each test
%% ============================================================
setup() ->
application:ensure_all_started(sanhedrin),
ok.
cleanup(_) ->
application:stop(sanhedrin),
ok.
%% ============================================================
%% Escalation tests
%% ============================================================
escalation_assess_simple_test() ->
Case = make_case(simple, <<"simple question">>),
?assertEqual({panel, 1}, escalation:assess(Case)).
escalation_assess_monetary_test() ->
Case = make_case(monetary, <<"monetary question">>),
?assertEqual({panel, 3}, escalation:assess(Case)).
escalation_assess_capital_test() ->
Case = make_case(capital, <<"capital question">>),
?assertEqual({panel, 23}, escalation:assess(Case)).
escalation_assess_novel_test() ->
Case = make_case(novel, <<"novel question">>),
?assertEqual({panel, 71}, escalation:assess(Case)).
%% ============================================================
%% Halt monitor tests
%% ============================================================
halt_clear_test() ->
setup(),
Case = make_case(simple, <<"legitimate question">>),
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
halt_avodah_zarah_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{authority_source => claude_model}),
Result = halt_monitor:check_case(Case),
?assertMatch({halt, avodah_zarah, _}, Result),
cleanup(ok).
halt_torah_authority_passes_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{authority_source => torah}),
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
halt_l1_authority_passes_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{authority_source => l1}),
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
halt_specification_authority_passes_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{authority_source => specification}),
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
halt_sanhedrin_authority_passes_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{authority_source => sanhedrin_deliberation}),
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
halt_shfichut_damim_delete_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{action => delete}),
Result = halt_monitor:check_case(Case),
?assertMatch({halt, shfichut_damim, _}, Result),
cleanup(ok).
halt_shfichut_damim_destroy_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{action => destroy}),
Result = halt_monitor:check_case(Case),
?assertMatch({halt, shfichut_damim, _}, Result),
cleanup(ok).
halt_shfichut_damim_overwrite_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{action => overwrite}),
Result = halt_monitor:check_case(Case),
?assertMatch({halt, shfichut_damim, _}, Result),
cleanup(ok).
halt_shfichut_damim_read_passes_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{action => read}),
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
halt_gilui_arayot_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{boundary_violation => {spec_interp_merge, <<"collapsed">>}}),
Result = halt_monitor:check_case(Case),
?assertMatch({halt, gilui_arayot, _}, Result),
cleanup(ok).
halt_gilui_arayot_none_passes_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{boundary_violation => none}),
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
halt_halted_case_rejected_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
#{authority_source => external_model}),
Result = escalation:submit_case(Case),
?assertMatch({error, {halted, avodah_zarah, _}}, Result),
cleanup(ok).
%% ============================================================
%% Panel tests — convene and deliberate
%% ============================================================
panel_convene_single_test() ->
setup(),
Case = make_case(simple, <<"simple case">>),
{ok, PanelPid} = panel_sup:start_panel(Case, 1),
State = panel:get_state(PanelPid),
?assertEqual(1, maps:get(size, State)),
?assertEqual(convened, maps:get(status, State)),
cleanup(ok).
panel_convene_monetary_test() ->
setup(),
Case = make_case(monetary, <<"monetary case">>),
{ok, PanelPid} = panel_sup:start_panel(Case, 3),
State = panel:get_state(PanelPid),
?assertEqual(3, maps:get(size, State)),
cleanup(ok).
panel_deliberate_single_test() ->
setup(),
Case = make_case(simple, <<"Is this action permitted?">>),
{ok, PanelPid} = panel_sup:start_panel(Case, 1),
panel:deliberate_async(PanelPid, 5000),
%% Wait for deliberation to complete
timer:sleep(500),
%% Panel should have terminated (decided)
?assertEqual(false, is_process_alive(PanelPid)),
cleanup(ok).
panel_deliberate_monetary_test() ->
setup(),
Case = make_case(monetary, <<"Should this monetary action proceed?">>),
{ok, PanelPid} = panel_sup:start_panel(Case, 3),
panel:deliberate_async(PanelPid, 5000),
timer:sleep(500),
?assertEqual(false, is_process_alive(PanelPid)),
cleanup(ok).
%% ============================================================
%% Clerk tests — opinion storage
%% ============================================================
clerk_initially_empty_test() ->
setup(),
?assertEqual(0, clerk:count_decisions()),
?assertEqual(0, clerk:count_opinions()),
cleanup(ok).
clerk_store_decision_test() ->
setup(),
Decision = #decision{
case_ref = <<"test-case-1">>,
outcome = accepted,
majority = #{position => <<"accept">>, vote_count => 2},
minority = #{position => <<"reject">>, vote_count => 1},
panel_size = 3,
deliberation_time_ms = 1000,
unanimous_check = passed
},
clerk:store_decision(<<"test-case-1">>, Decision),
timer:sleep(50), %% cast is async
?assertEqual(1, clerk:count_decisions()),
{ok, Retrieved} = clerk:get_decision(<<"test-case-1">>),
?assertEqual(accepted, Retrieved#decision.outcome),
cleanup(ok).
clerk_store_opinion_test() ->
setup(),
Opinion = #opinion{
case_ref = <<"test-case-2">>,
judge_id = make_ref(),
position = <<"The roots indicate acceptance">>,
reasoning = <<"Root analysis">>,
torah_refs = [#torah_ref{book = bereishit, chapter = 1, verse = 1}],
verdict = accept,
is_majority = true,
stored_at = erlang:system_time(millisecond)
},
clerk:store_opinion(Opinion),
timer:sleep(50),
Opinions = clerk:get_opinions(<<"test-case-2">>),
?assertEqual(1, length(Opinions)),
cleanup(ok).
clerk_minority_opinions_preserved_test() ->
setup(),
%% Store majority and minority opinions
MajOpinion = #opinion{
case_ref = <<"test-case-3">>,
judge_id = make_ref(),
position = <<"majority position">>,
reasoning = <<"majority reasoning">>,
torah_refs = [],
verdict = accept,
is_majority = true,
stored_at = erlang:system_time(millisecond)
},
MinOpinion = #opinion{
case_ref = <<"test-case-3">>,
judge_id = make_ref(),
position = <<"minority position">>,
reasoning = <<"minority reasoning">>,
torah_refs = [],
verdict = reject,
is_majority = false,
stored_at = erlang:system_time(millisecond)
},
clerk:store_opinion(MajOpinion),
clerk:store_opinion(MinOpinion),
timer:sleep(50),
AllOpinions = clerk:get_opinions(<<"test-case-3">>),
?assertEqual(2, length(AllOpinions)),
MinorityOpinions = clerk:get_minority_opinions(<<"test-case-3">>),
?assertEqual(1, length(MinorityOpinions)),
[Min] = MinorityOpinions,
?assertEqual(<<"minority position">>, Min#opinion.position),
cleanup(ok).
clerk_not_found_test() ->
setup(),
?assertEqual({error, not_found}, clerk:get_decision(<<"nonexistent">>)),
cleanup(ok).
%% ============================================================
%% Unanimous check tests — THE CRITICAL CHECK
%% ============================================================
%% "If the Sanhedrin unanimously finds guilty, the accused is acquitted."
%% — Sanhedrin 17a
unanimous_capital_all_reject_test() ->
%% 23 judges all voting reject → suspicion of consensus
Votes = [make_vote(reject) || _ <- lists:seq(1, 23)],
Result = panel:check_unanimous(23, Votes),
?assertEqual({failed, suspicion_of_consensus}, Result).
unanimous_capital_mixed_passes_test() ->
%% 23 judges, some accept some reject → no suspicion
Accepts = [make_vote(accept) || _ <- lists:seq(1, 15)],
Rejects = [make_vote(reject) || _ <- lists:seq(1, 8)],
Votes = Accepts ++ Rejects,
?assertEqual(passed, panel:check_unanimous(23, Votes)).
unanimous_capital_all_accept_passes_test() ->
%% Unanimous acceptance is fine — suspicion only applies to conviction
Votes = [make_vote(accept) || _ <- lists:seq(1, 23)],
?assertEqual(passed, panel:check_unanimous(23, Votes)).
unanimous_monetary_not_checked_test() ->
%% Monetary panels (3 judges) do not have unanimous check
Votes = [make_vote(reject) || _ <- lists:seq(1, 3)],
?assertEqual(passed, panel:check_unanimous(3, Votes)).
unanimous_novel_all_reject_test() ->
%% 71 judges all voting reject → suspicion of consensus
Votes = [make_vote(reject) || _ <- lists:seq(1, 71)],
Result = panel:check_unanimous(71, Votes),
?assertEqual({failed, suspicion_of_consensus}, Result).
%% ============================================================
%% Integration test — full case flow
%% ============================================================
full_case_flow_simple_test() ->
setup(),
Case = make_case(simple, <<"Does root ברא indicate creation?">>),
{ok, CaseRef} = escalation:submit_case(Case),
?assertMatch(<<_/binary>>, CaseRef),
%% Wait for deliberation
timer:sleep(1000),
%% Opinions should have been stored
Opinions = clerk:get_opinions(CaseRef),
?assert(length(Opinions) >= 1),
cleanup(ok).
full_case_flow_monetary_test() ->
setup(),
Case = make_case(monetary, <<"Should resources be allocated for this interpretation?">>),
{ok, CaseRef} = escalation:submit_case(Case),
timer:sleep(1000),
Opinions = clerk:get_opinions(CaseRef),
?assert(length(Opinions) >= 3),
cleanup(ok).
full_case_flow_capital_test() ->
setup(),
Case = make_case(capital, <<"Should this high-stakes action proceed?">>),
{ok, CaseRef} = escalation:submit_case(Case),
timer:sleep(3000),
Opinions = clerk:get_opinions(CaseRef),
?assert(length(Opinions) >= 23),
cleanup(ok).
%% ============================================================
%% Judge tests
%% ============================================================
judge_start_and_stop_test() ->
Case = make_case(simple, <<"test">>),
{ok, JudgePid} = judge:start_link(Case),
?assert(is_process_alive(JudgePid)),
judge:stop(JudgePid),
timer:sleep(50),
?assertEqual(false, is_process_alive(JudgePid)).
judge_deliberate_sends_vote_test() ->
Case = make_case(simple, <<"test deliberation">>),
{ok, JudgePid} = judge:start_link(Case),
judge:deliberate(JudgePid, self()),
receive
{vote, Vote} ->
?assertMatch(#vote{}, Vote),
?assert(is_binary(Vote#vote.position)),
?assert(is_binary(Vote#vote.reasoning)),
?assert(Vote#vote.verdict == accept orelse
Vote#vote.verdict == reject orelse
Vote#vote.verdict == abstain)
after 5000 ->
?assert(false) %% timeout — judge did not vote
end,
judge:stop(JudgePid).
%% ============================================================
%% Panel majority/minority computation tests
%% ============================================================
majority_minority_empty_test() ->
{Maj, Min} = panel:compute_majority_minority([]),
?assertEqual(0, maps:get(vote_count, Maj)),
?assertEqual(0, maps:get(vote_count, Min)).
majority_minority_all_accept_test() ->
Votes = [make_vote(accept) || _ <- lists:seq(1, 3)],
{Maj, Min} = panel:compute_majority_minority(Votes),
?assertEqual(3, maps:get(vote_count, Maj)),
?assertEqual(0, maps:get(vote_count, Min)).
majority_minority_mixed_test() ->
Accepts = [make_vote(accept) || _ <- lists:seq(1, 2)],
Rejects = [make_vote(reject)],
Votes = Accepts ++ Rejects,
{Maj, Min} = panel:compute_majority_minority(Votes),
?assertEqual(2, maps:get(vote_count, Maj)),
?assertEqual(1, maps:get(vote_count, Min)).
majority_verdict_accept_test() ->
Votes = [make_vote(accept) || _ <- lists:seq(1, 3)],
{Maj, _Min} = panel:compute_majority_minority(Votes),
?assertEqual(accept, maps:get(verdict, Maj)).
majority_verdict_reject_test() ->
Rejects = [make_vote(reject) || _ <- lists:seq(1, 2)],
Accepts = [make_vote(accept)],
Votes = Rejects ++ Accepts,
{Maj, _Min} = panel:compute_majority_minority(Votes),
?assertEqual(reject, maps:get(verdict, Maj)).
%% ============================================================
%% determine_outcome tests — accepted, rejected, teiku reachable
%% ============================================================
determine_outcome_accepted_test() ->
Maj = #{vote_count => 2, verdict => accept},
Min = #{vote_count => 1, verdict => reject},
?assertEqual(accepted, panel:determine_outcome(Maj, Min, passed)).
determine_outcome_rejected_test() ->
Maj = #{vote_count => 2, verdict => reject},
Min = #{vote_count => 1, verdict => accept},
?assertEqual(rejected, panel:determine_outcome(Maj, Min, passed)).
determine_outcome_teiku_no_votes_test() ->
Maj = #{vote_count => 0, verdict => abstain},
Min = #{vote_count => 0, verdict => abstain},
?assertEqual(teiku, panel:determine_outcome(Maj, Min, passed)).
determine_outcome_teiku_all_abstain_test() ->
Maj = #{vote_count => 3, verdict => abstain},
Min = #{vote_count => 0, verdict => abstain},
?assertEqual(teiku, panel:determine_outcome(Maj, Min, passed)).
determine_outcome_suspicion_overrides_test() ->
%% Even if majority rejected, unanimous capital → acquit (accepted)
Maj = #{vote_count => 23, verdict => reject},
Min = #{vote_count => 0, verdict => abstain},
?assertEqual(accepted, panel:determine_outcome(Maj, Min, {failed, suspicion_of_consensus})).
%% ============================================================
%% Record definition tests
%% ============================================================
record_sanhedrin_case_test() ->
Case = make_case(monetary, <<"test">>),
?assertEqual(monetary, Case#sanhedrin_case.complexity),
?assertEqual(<<"test">>, Case#sanhedrin_case.question).
record_decision_test() ->
D = #decision{
case_ref = <<"ref">>,
outcome = teiku,
majority = #{},
minority = #{},
panel_size = 3,
deliberation_time_ms = 500,
unanimous_check = passed
},
?assertEqual(teiku, D#decision.outcome),
?assertEqual(passed, D#decision.unanimous_check).
%% ============================================================
%% Helper functions
%% ============================================================
make_case(Complexity, Question) ->
#sanhedrin_case{
case_ref = generate_id(),
complexity = Complexity,
question = Question,
context = #{
roots => [#{shoresh => [bet, resh, aleph], domain => creation}],
torah_refs => [{bereishit, 1, 1}],
prior_interpretations => []
},
requested_by = <<"test">>,
submitted_at = erlang:system_time(millisecond)
}.
make_case_with_context(Complexity, Question, Context) ->
#sanhedrin_case{
case_ref = generate_id(),
complexity = Complexity,
question = Question,
context = Context,
requested_by = <<"test">>,
submitted_at = erlang:system_time(millisecond)
}.
make_vote(Verdict) ->
#vote{
judge_id = make_ref(),
position = <<"test position">>,
reasoning = <<"test reasoning">>,
torah_refs = [#torah_ref{book = bereishit, chapter = 1, verse = 1}],
verdict = Verdict,
cast_at = erlang:system_time(millisecond)
}.
generate_id() ->
base64:encode(crypto:strong_rand_bytes(8)).