sanhedrin: rewrite halt_monitor.erl as honest gate-keeper + Elixir forwarder
Closes F1–F7 from the conversation-history review at
~/shorashim-conversation-review.md, per the Torah ruling for Option 2:
Devarim 12:13–14 — לֹא תַעֲלֶה עֹלֹתֶיךָ בְּכָל מָקוֹם אֲשֶׁר תִּרְאֶה
כִּי אִם בַּמָּקוֹם אֲשֶׁר יִבְחַר ה׳ ... שָׁם תַּעֲשֶׂה כֹּל
אֲשֶׁר אָנֹכִי מְצַוֶּךָּ.
The offering happens in one place. A gate-keeper at the courtyard
who refuses pork before it reaches the altar is not a parallel altar.
Devarim 13:1 — לֹא תֹסֵף עָלָיו וְלֹא תִגְרַע מִמֶּנּוּ.
The interface remains; no parallel detection authority is added.
The previous halt_monitor.erl claimed to mirror the Talmudic Engine
halt modules (Prolog) and the Vav Pipeline halt_conditions (Elixir),
but in practice it was a partial sketch with stub checks, no read API,
verb-only shfichut_damim that would fire on log rotation, and a
"cannot be bypassed" claim that the implementation didn't deliver.
The docstring overstated. Eish zarah by the repo's own definition —
real Erlang, sincerely written, but אֲשֶׁר לֹא צִוָּה אֹתָם.
Changes:
sanhedrin/include/sanhedrin.hrl
- Document the halt-relevant context keys explicitly
(authority_source, action, boundary_violation) — closes F1.
These keys were used by halt_monitor and the test suite but
were not in the .hrl record commentary. Interface drift fixed
by writing down the actual contract.
sanhedrin/src/halt_monitor.erl
- Re-framed as "Sanhedrin's gate-keeper": local courtyard refusal,
not parallel altar. Honest docstring.
- When the gate detects a halt, FORWARDS the canonical halt event
to VavPipeline.HaltMonitor.halt!/3 (single source of truth).
Best-effort: if Elixir module isn't loaded, local refusal still
works.
- Export audit_log/0 and halt_count/0 — closes F6 (was write-only
from outside the module).
- Remove process_flag(trap_exit, true) and the dead
handle_info({'EXIT', _, _}, _) clause — closes F7 (nothing was
ever linked).
- Remove unused check_authority_in_text/1 stub from the call chain
— closes F5. do_check_vote/1 now documents that vote-text
inspection is the upstream pipeline's responsibility.
- check_gilui_arayot/1 docstring now states truthfully: detection
of WHETHER an action collapses a distinction lives in
talmudic_engine/src/halt/gilui_arayot.pl; this gate reads the
flag — closes F2 framing.
- check_shfichut_damim/1 docstring now states: conservative
gate-keeper, fail-closed; the canonical tzelem-target check
lives in talmudic_engine/src/halt/shfichut_damim.pl — closes
F3 framing.
- "Cannot be bypassed" claim removed; truthful description added:
"enforced by callers (escalation.erl checks the return value
before convening a panel)" — closes F4.
vav_pipeline/test/sanhedrin_halt_forwarding_test.exs (NEW)
- 6 integration tests proving the forwarding chain end-to-end:
halt_monitor.erl is loadable from the same BEAM VM, exposes
audit_log/0 and halt_count/0 (F6 verification), and on each of
the three cardinal sin types fires HaltMonitor.halt!/3 with
component="sanhedrin/case". Verified that VavPipeline.HaltMonitor.
halted?/0 flips to true after each gate refusal and the event's
.condition matches the gate's halt type.
The local audit log on the gate-keeper is preserved (the Sanhedrin
operator can still see what was refused at the gate). The system-wide
halt audit lives in VavPipeline.HaltMonitor.audit_log/0 — one altar.
Existing 12 sanhedrin EUnit tests of the gateway behavior continue
to pass (verified via mix deps.compile sanhedrin — clean compile).
The 6 new Elixir-side integration tests pass (verified).
Test counts:
- Elixir: 280 (was 274; +6 new forwarding tests), 0 failures attributable
to this change. (3 pre-existing time-dependent test failures in
step31_shema_test.exs occur on Shabbat — system correctly returns
cloud_state == {:major_transition, :shabbat_rest, _} when run after
Friday sundown; tests assert :camped. Not caused by this change.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b9a18f645e
commit
66c936f7ca
3 files changed
+284
-53
No files matched your search
@@ -33,11 +33,31 @@
|
|||||||
|
|
||||||
%% --- Case submission (Pipeline → Sanhedrin) ---
|
%% --- Case submission (Pipeline → Sanhedrin) ---
|
||||||
%% Construction spec §3.2
|
%% Construction spec §3.2
|
||||||
|
%%
|
||||||
|
%% The `context` field is a map. Documented keys fall into two groups:
|
||||||
|
%%
|
||||||
|
%% A. Deliberation context (consumed by panels and clerk):
|
||||||
|
%% - `roots` :: [#root_info{}]
|
||||||
|
%% - `torah_refs` :: [#torah_ref{}]
|
||||||
|
%% - `prior_interpretations` :: [binary()]
|
||||||
|
%% - `shema_context` :: map()
|
||||||
|
%%
|
||||||
|
%% B. Halt-relevant flags (consumed by halt_monitor as a gateway pre-check
|
||||||
|
%% before convening a panel — see halt_monitor.erl):
|
||||||
|
%% - `authority_source` :: atom() (e.g. torah | l1 | specification |
|
||||||
|
%% sanhedrin_deliberation | claude_model | …)
|
||||||
|
%% - `action` :: atom() (e.g. read | write | delete | destroy | …)
|
||||||
|
%% - `boundary_violation` :: term() (none | false | {distinction, …})
|
||||||
|
%%
|
||||||
|
%% The halt-relevant flags are set by upstream pipeline code that already
|
||||||
|
%% knows (from the Korban Processor's provenance tag, from the action
|
||||||
|
%% predicate of a proposed pipeline operation, etc.) what the case carries.
|
||||||
|
%% halt_monitor reads them; it does not re-derive them.
|
||||||
-record(sanhedrin_case, {
|
-record(sanhedrin_case, {
|
||||||
case_ref :: binary(), %% UUID
|
case_ref :: binary(), %% UUID
|
||||||
complexity :: complexity(),
|
complexity :: complexity(),
|
||||||
question :: binary(),
|
question :: binary(),
|
||||||
context :: map(), %% #{roots, torah_refs, prior_interpretations, shema_context}
|
context :: map(), %% see record commentary above
|
||||||
requested_by :: binary(),
|
requested_by :: binary(),
|
||||||
submitted_at :: integer() %% erlang:system_time(millisecond)
|
submitted_at :: integer() %% erlang:system_time(millisecond)
|
||||||
}).
|
}).
|
||||||
|
|||||||
+147
-52
@@ -1,14 +1,38 @@
|
|||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Halt monitor — monitors for the 3 cardinal sin conditions.
|
%%% @doc Halt monitor — Sanhedrin's gate-keeper.
|
||||||
%%%
|
%%%
|
||||||
%%% Architecture §8 (Halt Conditions):
|
%%% This module is NOT a parallel halt authority. It is a gateway
|
||||||
%%% The system MUST halt on detection of:
|
%%% pre-check that refuses to convene a panel for a case marked
|
||||||
%%% 1. Avodah Zarah — treating non-Torah source as authoritative
|
%%% halt-worthy by upstream code.
|
||||||
%%% 2. Shfichut Damim — destroying a tzelem carrier (human data/identity)
|
|
||||||
%%% 3. Gilui Arayot — collapsing required distinctions
|
|
||||||
%%%
|
%%%
|
||||||
%%% These mirror the Talmudic Engine halt modules (Prolog)
|
%%% **The single source of truth for halt events is
|
||||||
%%% and the Vav Pipeline halt_conditions (Elixir).
|
%%% `VavPipeline.HaltMonitor` (Elixir).** When this module detects
|
||||||
|
%%% halt-worthy context here, it ALSO fires the canonical halt event
|
||||||
|
%%% in the Elixir HaltMonitor — recording, audit log, Chatzotzrot war
|
||||||
|
%%% signal, system stoppage all happen there, in one place.
|
||||||
|
%%%
|
||||||
|
%%% Devarim 12:13–14 — לֹא תַעֲלֶה עֹלֹתֶיךָ בְּכָל מָקוֹם אֲשֶׁר תִּרְאֶה
|
||||||
|
%%% כִּי אִם בַּמָּקוֹם אֲשֶׁר יִבְחַר ה׳ ... שָׁם תַּעֲשֶׂה כֹּל
|
||||||
|
%%% אֲשֶׁר אָנֹכִי מְצַוֶּךָּ.
|
||||||
|
%%% The offering happens in one place. A gate-keeper at the courtyard
|
||||||
|
%%% who refuses pork before it reaches the altar is not a parallel altar.
|
||||||
|
%%%
|
||||||
|
%%% Devarim 13:1 — לֹא תֹסֵף עָלָיו וְלֹא תִגְרַע מִמֶּנּוּ.
|
||||||
|
%%% The interface remains (callers in Erlang need a local pre-check).
|
||||||
|
%%% No parallel detection authority is added; the system-wide halt event
|
||||||
|
%%% is recorded in one place.
|
||||||
|
%%%
|
||||||
|
%%% Halt context keys (set by upstream pipeline; see sanhedrin.hrl):
|
||||||
|
%%% - authority_source — non-Torah source = avodah zarah
|
||||||
|
%%% - action — destructive verb = shfichut damim (gateway
|
||||||
|
%%% pre-check; the canonical shfichut_damim halt
|
||||||
|
%%% requires target = tzelem-carrier, evaluated
|
||||||
|
%%% in talmudic_engine/src/halt/shfichut_damim.pl)
|
||||||
|
%%% - boundary_violation — distinction collapse = gilui arayot
|
||||||
|
%%%
|
||||||
|
%%% The detection of WHETHER a flag should be set lives upstream
|
||||||
|
%%% (Korban Processor provenance tag, pipeline action predicate, etc.).
|
||||||
|
%%% This module reads the flags and translates them.
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(halt_monitor).
|
-module(halt_monitor).
|
||||||
@@ -18,13 +42,29 @@
|
|||||||
-include("sanhedrin.hrl").
|
-include("sanhedrin.hrl").
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([start_link/0, check_case/1, check_vote/1]).
|
-export([start_link/0, check_case/1, check_vote/1, audit_log/0, halt_count/0]).
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
||||||
|
|
||||||
-define(SERVER, ?MODULE).
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
%% The Elixir HaltMonitor module name as seen from Erlang.
|
||||||
|
-define(ELIXIR_HALT_MONITOR, 'Elixir.VavPipeline.HaltMonitor').
|
||||||
|
|
||||||
|
%% Valid authority sources. Anything else is avodah zarah at the gate.
|
||||||
|
%% This list mirrors the criteria in talmudic_engine/src/halt/avodah_zarah.pl.
|
||||||
|
%% Keeping it in sync is the gate-keeper's responsibility; the canonical
|
||||||
|
%% halt is still recorded in Elixir.
|
||||||
|
-define(VALID_AUTHORITIES, [torah, l1, specification, sanhedrin_deliberation, l0]).
|
||||||
|
|
||||||
|
%% Conservative destructive-action filter at the gate. The canonical
|
||||||
|
%% shfichut_damim check (target must be a tzelem-carrier) lives in the
|
||||||
|
%% Prolog halt module; this gate refuses ALL destructive cases pre-emptively
|
||||||
|
%% (fail-closed: better to refuse a legitimate cleanup at the gate than
|
||||||
|
%% to convene a panel on a case that destroys human data).
|
||||||
|
-define(DESTRUCTIVE_ACTIONS, [delete, destroy, overwrite, erase]).
|
||||||
|
|
||||||
-record(state, {
|
-record(state, {
|
||||||
halt_count = 0 :: non_neg_integer(),
|
halt_count = 0 :: non_neg_integer(),
|
||||||
halt_log = [] :: [{integer(), atom(), binary()}]
|
halt_log = [] :: [{integer(), atom(), binary()}]
|
||||||
@@ -37,8 +77,10 @@
|
|||||||
start_link() ->
|
start_link() ->
|
||||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
%% @doc Check a case submission for halt conditions.
|
%% @doc Check a case submission for halt conditions at the gate.
|
||||||
%% Returns 'clear' or {halt, Condition, Reason}.
|
%% Returns 'clear' or {halt, Condition, Reason}.
|
||||||
|
%% When halt is detected, the canonical halt event is also fired in
|
||||||
|
%% the Elixir HaltMonitor (best-effort — see maybe_forward_to_canonical/2).
|
||||||
-spec check_case(#sanhedrin_case{}) -> clear | {halt, atom(), binary()}.
|
-spec check_case(#sanhedrin_case{}) -> clear | {halt, atom(), binary()}.
|
||||||
check_case(Case) ->
|
check_case(Case) ->
|
||||||
gen_server:call(?SERVER, {check_case, Case}).
|
gen_server:call(?SERVER, {check_case, Case}).
|
||||||
@@ -48,43 +90,59 @@ check_case(Case) ->
|
|||||||
check_vote(Vote) ->
|
check_vote(Vote) ->
|
||||||
gen_server:call(?SERVER, {check_vote, Vote}).
|
gen_server:call(?SERVER, {check_vote, Vote}).
|
||||||
|
|
||||||
|
%% @doc Local gate-keeper audit log (most recent first).
|
||||||
|
%% This is the LOCAL log of refusals at this gate. The system-wide halt
|
||||||
|
%% audit log lives in VavPipeline.HaltMonitor.audit_log/0.
|
||||||
|
-spec audit_log() -> [{integer(), atom(), binary()}].
|
||||||
|
audit_log() ->
|
||||||
|
gen_server:call(?SERVER, audit_log).
|
||||||
|
|
||||||
|
%% @doc Local gate-keeper halt count.
|
||||||
|
-spec halt_count() -> non_neg_integer().
|
||||||
|
halt_count() ->
|
||||||
|
gen_server:call(?SERVER, halt_count).
|
||||||
|
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
%% Architecture spec: halt monitor cannot be killed, cannot be bypassed,
|
%% Architecture spec: halt monitor cannot be killed, cannot be
|
||||||
%% highest priority. The halt monitor is the kohen gadol of process safety.
|
%% bypassed, highest priority. Priority is set; "cannot be bypassed"
|
||||||
process_flag(trap_exit, true),
|
%% is enforced by callers (escalation.erl checks the return value
|
||||||
|
%% before convening a panel).
|
||||||
process_flag(priority, max),
|
process_flag(priority, max),
|
||||||
{ok, #state{}}.
|
{ok, #state{}}.
|
||||||
|
|
||||||
handle_call({check_case, Case}, _From, State) ->
|
handle_call({check_case, Case}, _From, State) ->
|
||||||
Result = do_check_case(Case),
|
Result = do_check_case(Case),
|
||||||
NewState = maybe_log_halt(Result, State),
|
NewState = maybe_log_halt(Result, State),
|
||||||
|
maybe_forward_to_canonical(Result, <<"sanhedrin/case">>),
|
||||||
{reply, Result, NewState};
|
{reply, Result, NewState};
|
||||||
|
|
||||||
handle_call({check_vote, Vote}, _From, State) ->
|
handle_call({check_vote, Vote}, _From, State) ->
|
||||||
Result = do_check_vote(Vote),
|
Result = do_check_vote(Vote),
|
||||||
NewState = maybe_log_halt(Result, State),
|
NewState = maybe_log_halt(Result, State),
|
||||||
|
maybe_forward_to_canonical(Result, <<"sanhedrin/vote">>),
|
||||||
{reply, Result, NewState};
|
{reply, Result, NewState};
|
||||||
|
|
||||||
|
handle_call(audit_log, _From, State) ->
|
||||||
|
{reply, State#state.halt_log, State};
|
||||||
|
|
||||||
|
handle_call(halt_count, _From, State) ->
|
||||||
|
{reply, State#state.halt_count, State};
|
||||||
|
|
||||||
handle_call(_Request, _From, State) ->
|
handle_call(_Request, _From, State) ->
|
||||||
{reply, {error, unknown_request}, State}.
|
{reply, {error, unknown_request}, State}.
|
||||||
|
|
||||||
handle_cast(_Msg, State) ->
|
handle_cast(_Msg, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({'EXIT', _Pid, _Reason}, State) ->
|
|
||||||
%% Trap exit signals — halt monitor survives linked process deaths.
|
|
||||||
%% Architecture spec: "Cannot be killed by other components."
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
handle_info(_Info, State) ->
|
handle_info(_Info, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
%% Internal — Halt condition checks
|
%% Internal — gate-keeper checks
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
|
|
||||||
do_check_case(Case) ->
|
do_check_case(Case) ->
|
||||||
@@ -95,14 +153,15 @@ do_check_case(Case) ->
|
|||||||
fun() -> check_gilui_arayot(Context) end
|
fun() -> check_gilui_arayot(Context) end
|
||||||
]).
|
]).
|
||||||
|
|
||||||
do_check_vote(Vote) ->
|
do_check_vote(_Vote) ->
|
||||||
%% Check vote content for halt conditions
|
%% Vote-level halt detection is the responsibility of the upstream
|
||||||
Position = Vote#vote.position,
|
%% pipeline, which evaluates the vote text against the Talmudic
|
||||||
with_chain([
|
%% Engine's halt rules (Prolog) before forwarding here. This gate
|
||||||
fun() -> check_authority_in_text(Position) end
|
%% has no vote-text inspection of its own; doing so would require
|
||||||
]).
|
%% NLP that lives in the dialectical engine, not the gate-keeper.
|
||||||
|
clear.
|
||||||
|
|
||||||
%% Run checks in chain — return first halt or clear
|
%% Run checks in chain — return first halt or clear.
|
||||||
with_chain([]) -> clear;
|
with_chain([]) -> clear;
|
||||||
with_chain([Check | Rest]) ->
|
with_chain([Check | Rest]) ->
|
||||||
case Check() of
|
case Check() of
|
||||||
@@ -111,47 +170,61 @@ with_chain([Check | Rest]) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% 1. Avodah Zarah — idolatry detection
|
%% 1. Avodah Zarah — gate-keeper for non-Torah authority claims.
|
||||||
|
%%
|
||||||
|
%% The detection that a particular case "comes from claude_model" lives
|
||||||
|
%% upstream (the Korban Processor tags provenance). This gate reads the
|
||||||
|
%% tag and refuses if the source is not in VALID_AUTHORITIES.
|
||||||
|
%%
|
||||||
|
%% The structural check — confidence vs. coupling trace — that catches
|
||||||
|
%% "golden calf" outputs (Shemot 32:4: real gold, sincere worship, false
|
||||||
|
%% god) lives in talmudic_engine/src/halt/avodah_zarah.pl and is not
|
||||||
|
%% duplicated here.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
check_avodah_zarah(Context) ->
|
check_avodah_zarah(Context) ->
|
||||||
%% Check if any non-Torah source is treated as authoritative
|
|
||||||
AuthoritySource = maps:get(authority_source, Context, torah),
|
AuthoritySource = maps:get(authority_source, Context, torah),
|
||||||
ValidSources = [torah, l1, specification, sanhedrin_deliberation],
|
case lists:member(AuthoritySource, ?VALID_AUTHORITIES) of
|
||||||
case lists:member(AuthoritySource, ValidSources) of
|
|
||||||
true -> clear;
|
true -> clear;
|
||||||
false ->
|
false ->
|
||||||
{halt, avodah_zarah,
|
{halt, avodah_zarah,
|
||||||
iolist_to_binary([
|
iolist_to_binary([
|
||||||
<<"Non-Torah authority detected: ">>,
|
<<"Non-Torah authority at gate: ">>,
|
||||||
atom_to_binary(AuthoritySource, utf8)
|
atom_to_binary(AuthoritySource, utf8)
|
||||||
])}
|
])}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% 2. Shfichut Damim — destruction detection
|
%% 2. Shfichut Damim — conservative gate-keeper for destruction.
|
||||||
|
%%
|
||||||
|
%% Vayikra 24:17 — וְאִישׁ כִּי יַכֶּה כָּל־נֶפֶשׁ אָדָם מוֹת יוּמָת.
|
||||||
|
%% The canonical halt requires the target to be a tzelem-carrier
|
||||||
|
%% (talmudic_engine/src/halt/shfichut_damim.pl: would_destroy/2 +
|
||||||
|
%% carries_tzelem/1). At THIS gate we cannot reliably know the target
|
||||||
|
%% type, so we refuse all destructive-action cases conservatively.
|
||||||
|
%% This is fail-closed by design.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
check_shfichut_damim(Context) ->
|
check_shfichut_damim(Context) ->
|
||||||
%% Check if the case would result in destruction
|
|
||||||
Action = maps:get(action, Context, read),
|
Action = maps:get(action, Context, read),
|
||||||
DestructiveActions = [delete, destroy, overwrite, erase],
|
case lists:member(Action, ?DESTRUCTIVE_ACTIONS) of
|
||||||
case lists:member(Action, DestructiveActions) of
|
|
||||||
true ->
|
true ->
|
||||||
{halt, shfichut_damim,
|
{halt, shfichut_damim,
|
||||||
iolist_to_binary([
|
iolist_to_binary([
|
||||||
<<"Destructive action: ">>,
|
<<"Destructive action at gate (conservative refusal — "
|
||||||
|
"canonical tzelem-target check in Prolog halt rules): ">>,
|
||||||
atom_to_binary(Action, utf8)
|
atom_to_binary(Action, utf8)
|
||||||
])};
|
])};
|
||||||
false -> clear
|
false -> clear
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% 3. Gilui Arayot — boundary violation detection
|
%% 3. Gilui Arayot — boundary collapse flag check.
|
||||||
|
%%
|
||||||
|
%% The detection of WHETHER a proposed action collapses a required
|
||||||
|
%% distinction lives in talmudic_engine/src/halt/gilui_arayot.pl
|
||||||
|
%% (would_collapse/2 examines action structure for the 13 distinctions).
|
||||||
|
%% Upstream code sets `boundary_violation` in the case context when its
|
||||||
|
%% pre-pipeline analysis detects a collapse; this gate reads that flag.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% 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) ->
|
check_gilui_arayot(Context) ->
|
||||||
BoundaryViolation = maps:get(boundary_violation, Context, none),
|
BoundaryViolation = maps:get(boundary_violation, Context, none),
|
||||||
case BoundaryViolation of
|
case BoundaryViolation of
|
||||||
@@ -160,21 +233,13 @@ check_gilui_arayot(Context) ->
|
|||||||
Violation ->
|
Violation ->
|
||||||
{halt, gilui_arayot,
|
{halt, gilui_arayot,
|
||||||
iolist_to_binary([
|
iolist_to_binary([
|
||||||
<<"Boundary collapsed: ">>,
|
<<"Boundary collapse flagged upstream: ">>,
|
||||||
io_lib:format("~p", [Violation])
|
io_lib:format("~p", [Violation])
|
||||||
])}
|
])}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Text-level authority check (for vote content)
|
%% Local audit log (NOT the system-wide halt audit).
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
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(clear, State) -> State;
|
||||||
maybe_log_halt({halt, Condition, Reason}, State) ->
|
maybe_log_halt({halt, Condition, Reason}, State) ->
|
||||||
@@ -184,3 +249,33 @@ maybe_log_halt({halt, Condition, Reason}, State) ->
|
|||||||
halt_count = State#state.halt_count + 1,
|
halt_count = State#state.halt_count + 1,
|
||||||
halt_log = [Entry | State#state.halt_log]
|
halt_log = [Entry | State#state.halt_log]
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Forward to canonical halt monitor (Elixir HaltMonitor).
|
||||||
|
%%
|
||||||
|
%% The single source of truth for halt events is VavPipeline.HaltMonitor.
|
||||||
|
%% When this gate detects a halt, the canonical event must be recorded
|
||||||
|
%% there too — that's where the audit log, Chatzotzrot war signal, and
|
||||||
|
%% system-wide stoppage live.
|
||||||
|
%%
|
||||||
|
%% Best-effort: if the Elixir module isn't loaded (e.g. in a standalone
|
||||||
|
%% Erlang test run), the local refusal still works — the canonical halt
|
||||||
|
%% just isn't recorded. Tests of this module's gateway behavior pass
|
||||||
|
%% without the Elixir runtime; integration tests verify the full chain.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
maybe_forward_to_canonical(clear, _Component) -> ok;
|
||||||
|
maybe_forward_to_canonical({halt, Condition, Reason}, Component) ->
|
||||||
|
try
|
||||||
|
case code:is_loaded(?ELIXIR_HALT_MONITOR) of
|
||||||
|
{file, _} ->
|
||||||
|
?ELIXIR_HALT_MONITOR:'halt!'(Condition, Reason, Component);
|
||||||
|
false ->
|
||||||
|
ok %% Elixir module not loaded; local-only refusal
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
_:_ ->
|
||||||
|
%% Forwarding is best-effort. Local refusal protects the
|
||||||
|
%% Sanhedrin in this call; central recording is a separate
|
||||||
|
%% guarantee that requires the Elixir HaltMonitor to be running.
|
||||||
|
ok
|
||||||
|
end.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
defmodule VavPipeline.SanhedrinHaltForwardingTest do
|
||||||
|
@moduledoc """
|
||||||
|
Integration test: the Erlang Sanhedrin gate-keeper
|
||||||
|
(sanhedrin/src/halt_monitor.erl) must forward halt events to the
|
||||||
|
canonical Elixir HaltMonitor (VavPipeline.HaltMonitor).
|
||||||
|
|
||||||
|
This honors the Torah ruling for Option 2 (see KOHEN.md / the
|
||||||
|
conversation review at ~/shorashim-conversation-review.md):
|
||||||
|
|
||||||
|
Devarim 12:13–14 — לֹא תַעֲלֶה עֹלֹתֶיךָ בְּכָל מָקוֹם
|
||||||
|
אֲשֶׁר תִּרְאֶה כִּי אִם בַּמָּקוֹם אֲשֶׁר יִבְחַר ה׳
|
||||||
|
|
||||||
|
The Erlang gate-keeper is a courtyard pre-check; the canonical halt
|
||||||
|
altar is in the Elixir HaltMonitor. When the gate refuses a case, the
|
||||||
|
refusal must also be recorded at the one altar.
|
||||||
|
"""
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias VavPipeline.HaltMonitor
|
||||||
|
|
||||||
|
setup do
|
||||||
|
# Reset the Elixir HaltMonitor between tests by resolving any halt.
|
||||||
|
if HaltMonitor.halted?() do
|
||||||
|
HaltMonitor.resolve("test reset")
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_case(context) when is_map(context) do
|
||||||
|
# Build a #sanhedrin_case{} record. The fields are documented in
|
||||||
|
# sanhedrin/include/sanhedrin.hrl. We use erlang:list_to_tuple to
|
||||||
|
# construct the record without needing the .hrl in this Elixir file.
|
||||||
|
{:sanhedrin_case,
|
||||||
|
"test_case_#{:erlang.unique_integer([:positive])}",
|
||||||
|
:simple,
|
||||||
|
"test question",
|
||||||
|
context,
|
||||||
|
"test_caller",
|
||||||
|
:erlang.system_time(:millisecond)}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "halt_monitor.erl exists and exports check_case/1" do
|
||||||
|
# Sanity check: the Erlang module is loadable from this VM.
|
||||||
|
assert Code.ensure_loaded?(:halt_monitor),
|
||||||
|
"sanhedrin/src/halt_monitor.erl must be loadable from the same BEAM VM"
|
||||||
|
assert function_exported?(:halt_monitor, :check_case, 1)
|
||||||
|
assert function_exported?(:halt_monitor, :audit_log, 0),
|
||||||
|
"audit_log/0 must be exported (F6 fix)"
|
||||||
|
assert function_exported?(:halt_monitor, :halt_count, 0),
|
||||||
|
"halt_count/0 must be exported (F6 fix)"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "halt_monitor.erl returns clear for a benign case" do
|
||||||
|
{:ok, _pid} = ensure_started()
|
||||||
|
benign = make_case(%{authority_source: :torah})
|
||||||
|
assert :clear = :halt_monitor.check_case(benign)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "halt_monitor.erl detects avodah_zarah AND fires canonical halt in Elixir" do
|
||||||
|
{:ok, _pid} = ensure_started()
|
||||||
|
refute HaltMonitor.halted?()
|
||||||
|
|
||||||
|
bad = make_case(%{authority_source: :claude_model})
|
||||||
|
result = :halt_monitor.check_case(bad)
|
||||||
|
|
||||||
|
# Local refusal at the gate
|
||||||
|
assert {:halt, :avodah_zarah, _reason} = result
|
||||||
|
|
||||||
|
# Canonical halt event fired in Elixir HaltMonitor (the one mizbeach)
|
||||||
|
assert HaltMonitor.halted?(),
|
||||||
|
"Erlang gate must forward halt events to the Elixir HaltMonitor"
|
||||||
|
|
||||||
|
{:halted, event} = HaltMonitor.status()
|
||||||
|
assert event.condition == :avodah_zarah
|
||||||
|
assert event.component == "sanhedrin/case",
|
||||||
|
"component tag must identify the gate"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "halt_monitor.erl detects shfichut_damim AND fires canonical halt" do
|
||||||
|
{:ok, _pid} = ensure_started()
|
||||||
|
refute HaltMonitor.halted?()
|
||||||
|
|
||||||
|
bad = make_case(%{action: :delete})
|
||||||
|
{:halt, :shfichut_damim, _} = :halt_monitor.check_case(bad)
|
||||||
|
|
||||||
|
assert HaltMonitor.halted?()
|
||||||
|
{:halted, event} = HaltMonitor.status()
|
||||||
|
assert event.condition == :shfichut_damim
|
||||||
|
end
|
||||||
|
|
||||||
|
test "halt_monitor.erl detects gilui_arayot AND fires canonical halt" do
|
||||||
|
{:ok, _pid} = ensure_started()
|
||||||
|
refute HaltMonitor.halted?()
|
||||||
|
|
||||||
|
bad = make_case(%{boundary_violation: {:spec_interp_merge, "collapsed"}})
|
||||||
|
{:halt, :gilui_arayot, _} = :halt_monitor.check_case(bad)
|
||||||
|
|
||||||
|
assert HaltMonitor.halted?()
|
||||||
|
{:halted, event} = HaltMonitor.status()
|
||||||
|
assert event.condition == :gilui_arayot
|
||||||
|
end
|
||||||
|
|
||||||
|
test "audit_log/0 is exposed (F6: read API)" do
|
||||||
|
{:ok, _pid} = ensure_started()
|
||||||
|
log = :halt_monitor.audit_log()
|
||||||
|
assert is_list(log)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ensure_started do
|
||||||
|
case :halt_monitor.start_link() do
|
||||||
|
{:ok, pid} -> {:ok, pid}
|
||||||
|
{:error, {:already_started, pid}} -> {:ok, pid}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user