Files
bikoret/sanhedrin/include/sanhedrin.hrl
T
Scott KarpandClaude Opus 4.6 66c936f7ca 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>
2026-04-24 17:31:37 -04:00

122 lines
4.4 KiB
Erlang

%%% 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
%%
%% 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, {
case_ref :: binary(), %% UUID
complexity :: complexity(),
question :: binary(),
context :: map(), %% see record commentary above
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