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:
1 parent
04f61c1238
commit
7c40200dfc
5 files changed
+184
-24
No files matched your search
@@ -145,13 +145,29 @@ handle_info(_Info, State) ->
|
|||||||
%% Internal — gate-keeper checks
|
%% Internal — gate-keeper checks
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
|
|
||||||
do_check_case(Case) ->
|
%% The context is a map (authority_source, action, ...). Anything else is
|
||||||
Context = Case#sanhedrin_case.context,
|
%% 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([
|
with_chain([
|
||||||
fun() -> check_avodah_zarah(Context) end,
|
fun() -> check_avodah_zarah(Context) end,
|
||||||
fun() -> check_shfichut_damim(Context) end,
|
fun() -> check_shfichut_damim(Context) end,
|
||||||
fun() -> check_gilui_arayot(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) ->
|
do_check_vote(_Vote) ->
|
||||||
%% Vote-level halt detection is the responsibility of the upstream
|
%% Vote-level halt detection is the responsibility of the upstream
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ halt_clear_test() ->
|
|||||||
?assertEqual(clear, halt_monitor:check_case(Case)),
|
?assertEqual(clear, halt_monitor:check_case(Case)),
|
||||||
cleanup(ok).
|
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() ->
|
halt_avodah_zarah_test() ->
|
||||||
setup(),
|
setup(),
|
||||||
Case = make_case_with_context(simple, <<"question">>,
|
Case = make_case_with_context(simple, <<"question">>,
|
||||||
|
|||||||
@@ -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
|
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
|
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.
|
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
|
resubmit: true
|
||||||
})
|
})
|
||||||
else
|
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} =
|
case VavPipeline.SanhedrinClient.submit_case(%{
|
||||||
VavPipeline.SanhedrinClient.submit_case(%{
|
case_ref: case_ref,
|
||||||
case_ref: case_ref,
|
complexity: :simple,
|
||||||
complexity: :simple,
|
question: case_text,
|
||||||
question: case_text,
|
context: %{sides: sides},
|
||||||
context: params["sides"] || %{},
|
requested_by: "chatzer"
|
||||||
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, %{
|
{:error, "Halted" <> _ = reason} ->
|
||||||
case_ref: decision.case_ref,
|
send_error(conn, 422, "SANHEDRIN_HALTED", %{
|
||||||
deliberation: %{
|
issue: reason,
|
||||||
panel_size: decision.panel_size,
|
correction: "The case was refused at the gate before deliberation. Amend it and resubmit.",
|
||||||
majority: decision.majority,
|
resubmit: true
|
||||||
minority: decision.minority,
|
})
|
||||||
outcome: decision.outcome,
|
|
||||||
processing_time_ms: decision.deliberation_time_ms
|
{:error, reason} ->
|
||||||
},
|
send_error(conn, 503, "SANHEDRIN_UNAVAILABLE", %{
|
||||||
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
|
issue: reason,
|
||||||
disclosure: disclosure()
|
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
|
||||||
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
|
# Internal — Health and Status
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -317,6 +317,43 @@ defmodule VavPipeline.BigdeiKodeshTest do
|
|||||||
assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"]
|
assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"]
|
||||||
assert body["note"] =~ "not a binding ruling"
|
assert body["note"] =~ "not a binding ruling"
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
Reference in new issue
Block a user