tablet 55: the row that was a label — the Sanhedrin's example request crashed its own gate

Verified live 2026-10-19: Tamid's own example for POST /chatzer/sanhedrin
(a case with `sides`) answered 500. The router passed the sides LIST as
the case context; halt_monitor:check_avodah_zarah/1 reads a map, maps:get
raised, and the halt monitor and escalation gen_servers died with the
caller's input (supervisor restarted them; health stayed 8/8). The router
then matched {:ok, decision} on the error and made a 500 of its own.

- sanhedrin/src/halt_monitor.erl: do_check_case refuses a non-map
  context as {halt, malformed_case, _} and stays standing. EUnit 49/0
  with the new halt_malformed_context_test.
- router.ex process_sanhedrin: sides validated at the kiyor (422 on a bad
  shape), carried as context %{sides: sides}; a Sanhedrin refusal answers
  422 SANHEDRIN_HALTED, a failure 503 SANHEDRIN_UNAVAILABLE. Two new
  router tests, one asserting the gate processes survive the example.
  Elixir 624/0.
- specs/broken_tablets/README.md: tablet 55, including what is NOT fixed
  — a panel of one accepting on zero references under a grounded-
  sounding sentence, the ignored complexity hint, no panel manna spent,
  no L3 record. The row in Tamid is still larger than the thing behind it.

Doorpost pin unchanged (no @spec_files touched; scripts/doorpost_check.py
passes).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-19 16:44:53 +00:00
1 parent 04f61c1238
commit 7c40200dfc
5 files changed
+184 -24

No files matched your search

+19 -3
View File
@@ -145,13 +145,29 @@ handle_info(_Info, State) ->
%% Internal — gate-keeper checks
%%====================================================================
do_check_case(Case) ->
Context = Case#sanhedrin_case.context,
%% The context is a map (authority_source, action, ...). Anything else is
%% a malformed case, refused here — never reasoned about, never a crash.
%% Tablet 55 (2026-10-19): the Tamid's own example request carried a
%% LIST as context; maps:get raised, and this gen_server and escalation
%% died with the caller's input. A gate that can be felled by what it is
%% handed is not a gate. Fail closed, stay standing.
do_check_case(#sanhedrin_case{context = Context}) when is_map(Context) ->
with_chain([
fun() -> check_avodah_zarah(Context) end,
fun() -> check_shfichut_damim(Context) end,
fun() -> check_gilui_arayot(Context) end
]).
]);
do_check_case(#sanhedrin_case{context = Other}) ->
{halt, malformed_case,
iolist_to_binary([<<"case context must be a map, got ">>,
atom_to_binary(type_name(Other), utf8)])}.
type_name(T) when is_list(T) -> list;
type_name(T) when is_binary(T) -> binary;
type_name(T) when is_atom(T) -> atom;
type_name(T) when is_tuple(T) -> tuple;
type_name(T) when is_number(T) -> number;
type_name(_) -> other.
do_check_vote(_Vote) ->
%% Vote-level halt detection is the responsibility of the upstream
+10
View File
@@ -58,6 +58,16 @@ halt_clear_test() ->
?assertEqual(clear, halt_monitor:check_case(Case)),
cleanup(ok).
%% Tablet 55: a non-map context is refused, and the monitor survives it.
halt_malformed_context_test() ->
{ok, _} = application:ensure_all_started(sanhedrin),
Sides = [#{<<"position">> => <<"a">>, <<"evidence">> => [<<"x">>]}],
Case = make_case_with_context(simple, <<"question">>, Sides),
?assertMatch({halt, malformed_case, _}, halt_monitor:check_case(Case)),
%% still standing, and still judging
?assertEqual(clear, halt_monitor:check_case(make_case(simple, <<"q">>))),
ok.
halt_avodah_zarah_test() ->
setup(),
Case = make_case_with_context(simple, <<"question">>,
+46
View File
@@ -132,3 +132,49 @@ make. The fence held throughout — each forgery was stripped and named by
a verdict the builder did not write — which is the design proving itself
on its builder: the check has to stand outside the model, because the
model's own intention is subject to the same pull it is meant to catch.
## Tablet 55 — the row that was a label (2026-10-19)
Tamid promises `POST /chatzer/sanhedrin`: a case, optional sides, a
panel of three, a majority of two and a minority of one with their
sources. The row had stood since the endpoint was written. The build
state carried a line beside it — "never verified on prod" — for months.
On 2026-10-19 the builder sent the spec's own example request to the
live system, as the roadmap's rule asks: a spec row is a claim to test,
not a label.
The first request answered 500. The router had handed the `sides` list
to the Sanhedrin as the case's *context*, where the halt monitor reads a
map of `authority_source` and `action`; `maps:get` raised, and the halt
monitor and the escalation server died with the caller's input. The
supervisor restarted them; health stayed 8/8 the whole time. A gate that
can be felled by what it is handed is not a gate, and a health check
that cannot see the gate fall is not watching it. The router then
pattern-matched `{:ok, decision}` on an error and turned the refusal
into a 500 of its own.
The second request, without sides, answered 200 — and the answer was
hollow. A panel of one, not three. Field names the spec does not use.
An outcome of "accepted" on zero roots and zero references examined,
under the sentence "Torah references ground this interpretation". That
is the shape the bedikah names golden_calf when a model says it: a
confident claim standing on nothing. Here the system said it in its own
voice, through its own inner court. The complexity hint was ignored, no
panel manna was spent, and nothing was engraved in L3.
Fixed in this tablet: the halt monitor refuses a non-map context as
`malformed_case` and stays standing; the router carries the sides inside
the context map, checks their shape at the kiyor (422), and answers a
Sanhedrin refusal (422 SANHEDRIN_HALTED) or failure (503
SANHEDRIN_UNAVAILABLE) instead of crashing. The spec's own example now
deliberates, and the test asserts that the gate is still there
afterwards.
Not fixed, and recorded here so it is not mistaken for fixed: the
panel of one, the empty references under a grounded-sounding sentence,
the ignored hint, the unspent manna, the unrecorded case. The row in
Tamid is still larger than the thing behind it. The lesson is tablet
37's again — a spec row is a claim to test — with one thing added: the
system's own inner court is held to the same check as a model at the
gate. When the Sanhedrin accepts on nothing, it is the calf, whoever
cast it.
@@ -1084,32 +1084,83 @@ defmodule VavPipeline.BigdeiKodesh.Router do
resubmit: true
})
else
case_ref = generate_id()
# Tablet 55: the sides are the caller's arguments, not the halt
# gate's context. The gate reads a MAP (authority_source, action);
# handing it the sides list crashed the halt monitor and escalation
# with the Tamid's own example request. The sides ride inside the
# map, their shape checked at the door; and a refusal from the
# Sanhedrin is answered, not matched into a 500.
with {:ok, sides} <- sanhedrin_sides(params["sides"]) do
case_ref = generate_id()
{:ok, decision} =
VavPipeline.SanhedrinClient.submit_case(%{
case_ref: case_ref,
complexity: :simple,
question: case_text,
context: params["sides"] || %{},
requested_by: "chatzer"
})
case VavPipeline.SanhedrinClient.submit_case(%{
case_ref: case_ref,
complexity: :simple,
question: case_text,
context: %{sides: sides},
requested_by: "chatzer"
}) do
{:ok, decision} ->
send_json(conn, 200, %{
case_ref: decision.case_ref,
deliberation: %{
panel_size: decision.panel_size,
majority: decision.majority,
minority: decision.minority,
outcome: decision.outcome,
processing_time_ms: decision.deliberation_time_ms
},
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
disclosure: disclosure()
})
send_json(conn, 200, %{
case_ref: decision.case_ref,
deliberation: %{
panel_size: decision.panel_size,
majority: decision.majority,
minority: decision.minority,
outcome: decision.outcome,
processing_time_ms: decision.deliberation_time_ms
},
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
disclosure: disclosure()
})
{:error, "Halted" <> _ = reason} ->
send_error(conn, 422, "SANHEDRIN_HALTED", %{
issue: reason,
correction: "The case was refused at the gate before deliberation. Amend it and resubmit.",
resubmit: true
})
{:error, reason} ->
send_error(conn, 503, "SANHEDRIN_UNAVAILABLE", %{
issue: reason,
correction: "The Sanhedrin did not deliberate. Retry; if it recurs, the kohanim will find it in the log.",
resubmit: true
})
end
else
{:error, issue} ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
issue: issue,
correction: "'sides' is an optional list of {\"position\": string, \"evidence\": [string]}",
resubmit: true
})
end
end
end
# The optional sides: absent → []; a list of maps each with a string
# "position" and an optional list of string "evidence" → kept as given;
# anything else → refused at the kiyor.
defp sanhedrin_sides(nil), do: {:ok, []}
defp sanhedrin_sides(sides) when is_list(sides) do
if Enum.all?(sides, &side_ok?/1),
do: {:ok, sides},
else: {:error, "'sides' must be a list of {position, evidence[]} objects"}
end
defp sanhedrin_sides(_), do: {:error, "'sides' must be a list"}
defp side_ok?(%{"position" => p} = side) when is_binary(p) do
case Map.get(side, "evidence", []) do
ev when is_list(ev) -> Enum.all?(ev, &is_binary/1)
_ -> false
end
end
defp side_ok?(_), do: false
# ============================================================
# Internal — Health and Status
# ============================================================
+37
View File
@@ -317,6 +317,43 @@ defmodule VavPipeline.BigdeiKodeshTest do
assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"]
assert body["note"] =~ "not a binding ruling"
end
# Tablet 55: the Tamid's own example — a case WITH sides — returned
# 500 and took the halt monitor and escalation down with it.
test "POST /chatzer/sanhedrin with sides (the Tamid's example) deliberates" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{
case: "A borrower repaid before one witness; the lender denies receipt. Who bears the burden?",
sides: [
%{position: "the borrower is believed", evidence: ["the money was handed over before a witness"]},
%{position: "the lender is believed", evidence: ["one witness does not establish payment"]}
],
complexity_hint: "monetary"
}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"]
# and the gate is still standing afterwards
assert Process.whereis(:halt_monitor) != nil
assert Process.whereis(:escalation) != nil
end
test "POST /chatzer/sanhedrin with malformed sides returns 422, not 500" do
for bad <- ["not a list", [%{"no_position" => 1}], [%{"position" => "p", "evidence" => "not a list"}]] do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "x", sides: bad}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer chatzer-test-token")
|> Router.call(@opts)
assert conn.status == 422, "expected 422 for sides=#{inspect(bad)}, got #{conn.status}"
assert Jason.decode!(conn.resp_body)["error"]["code"] == "KIYOR_VALIDATION_FAILED"
end
end
end
# ============================================================