tablet 55, second: the judge consults — the Sanhedrin no longer rolls dice
judge.erl chose one of six canned positions by a hash over the question,
its own pid and the clock; a panel of one "accepted" on zero roots and
zero references under "Torah references ground this interpretation" —
the calf, cast by the court itself. Architecture §3.2 says a judge
"consults the specification (L0/L1)"; Construction §3.2 says the
pipeline hands the case roots and torah_refs. Neither happened.
Sanhedrin (Erlang):
- judge.erl: seat N performs step N of the dialectic — interpret,
challenge, resolve, cycling — through a consult {M, F} in the app env.
resolved → accept with the verses; challenged (a kushya stands) →
reject; halt → reject; teiku → abstain. No consult, an error, or a
consult that raises → abstain, and the reasoning says so; the judge
stands. judge:start_link/2 takes the seat; panel.erl seats its judges.
- EUnit 54/0: seats cycle; no consult → abstain; resolved → accept with
#torah_ref{}; a raising consult → abstain and alive; a panel of three
with a kushya at seat 2 → accepted 2–1, minority preserved with verse.
Pipeline (Elixir):
- VavPipeline.SanhedrinConsult: what a judge asks — TalmudicEngineSocket
interpret/challenge/resolve over the case's roots, direct (the client's
stub fallbacks are NOT used: a court that accepts on a stub is the
calf). Engine absent → {:error, _} → the judge abstains. Registered in
the sanhedrin env at application start.
- router process_sanhedrin: the case is heard before it is submitted —
ScopeResolver → Shema context (cloud honoured) → tohu → vayomer
(Shoresh Engine over the verse) → roots; torah_refs = the scope;
complexity_hint names the panel (422 on an unknown one); one unit of
sanhedrin_panels_max per case (503 MANNA_DEPLETED when spent); the
decision engraved in L3 as an interpretation (panel_size, votes
for/against, processing_component "sanhedrin", coupling root→verse)
with the minority beside it (grounding_type achorai); the reply in
the Tamid's shape (position, reasoning, sources, votes) plus
grounding {scope, roots_consulted, recorded}.
- Tests: Elixir 628/0 (router 30, incl. monetary → panel 3; no engine →
teiku with 0 votes, never accepted; fake consult → 2–1 with sources;
manna → 503; unknown hint → 422); L3 test: interpretation + minority
rows land.
Pre-existing and untouched: with --include l3 the gate and hakhel
tests fail 17 times on main as well (the gate's local upstream double
refuses connections under the L3 flag) — a separate finding.
Doorpost pin unchanged (no @spec_files touched).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c40200dfc
commit
046010f0ed
8 files changed
+734
-107
No files matched your search
@@ -26,6 +26,11 @@ defmodule VavPipeline.Application do
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
# Tablet 55: the Sanhedrin's judges consult the specification through
|
||||
# this callback (the Talmudic Engine behind it). Registered before the
|
||||
# supervision tree so the first panel convened can already consult.
|
||||
Application.put_env(:sanhedrin, :consult, {VavPipeline.SanhedrinConsult, :consult})
|
||||
|
||||
shoresh_binary = Application.get_env(:vav_pipeline, :shoresh_engine_binary, "shoresh-engine")
|
||||
shoresh_cwd = Application.get_env(:vav_pipeline, :shoresh_engine_cwd, nil)
|
||||
talmudic_socket = Application.get_env(:vav_pipeline, :talmudic_socket_path, "/tmp/bikoret/talmudic.sock")
|
||||
|
||||
@@ -1090,26 +1090,49 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
# 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
|
||||
#
|
||||
# And the case is RESOLVED before it is submitted — Construction
|
||||
# §3.2: the pipeline hands the Sanhedrin roots and torah_refs. The
|
||||
# question is heard the way pe'ah hears it (scope → verse → Shoresh
|
||||
# Engine → roots); the complexity hint sets the panel; a panel
|
||||
# spends one unit of the day's panel manna; the decision is
|
||||
# engraved in L3; the answer takes the Tamid's shape.
|
||||
with {:ok, sides} <- sanhedrin_sides(params["sides"]),
|
||||
{:ok, complexity} <- sanhedrin_complexity(params["complexity_hint"]),
|
||||
{:ok, scope, resolved_by} <- sanhedrin_scope(case_text),
|
||||
{:ok, resolved} <- sanhedrin_resolve(case_text, scope, resolved_by),
|
||||
:ok <- sanhedrin_gather_panel() do
|
||||
case_ref = generate_id()
|
||||
|
||||
case VavPipeline.SanhedrinClient.submit_case(%{
|
||||
case_ref: case_ref,
|
||||
complexity: :simple,
|
||||
complexity: complexity,
|
||||
question: case_text,
|
||||
context: %{sides: sides},
|
||||
context: %{
|
||||
sides: sides,
|
||||
roots: resolved.roots,
|
||||
torah_refs: [resolved.torah_ref],
|
||||
authority_source: :torah
|
||||
},
|
||||
requested_by: "chatzer"
|
||||
}) do
|
||||
{:ok, decision} ->
|
||||
recorded = sanhedrin_record_l3(case_ref, case_text, decision, resolved)
|
||||
|
||||
send_json(conn, 200, %{
|
||||
case_ref: decision.case_ref,
|
||||
deliberation: %{
|
||||
panel_size: decision.panel_size,
|
||||
majority: decision.majority,
|
||||
minority: decision.minority,
|
||||
majority: sanhedrin_side(decision.majority),
|
||||
minority: sanhedrin_side(decision.minority),
|
||||
outcome: decision.outcome,
|
||||
processing_time_ms: decision.deliberation_time_ms
|
||||
},
|
||||
grounding: %{
|
||||
scope: scope_report(scope, resolved_by),
|
||||
roots_consulted: format_roots(resolved.roots_full),
|
||||
recorded: recorded
|
||||
},
|
||||
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
|
||||
disclosure: disclosure()
|
||||
})
|
||||
@@ -1129,12 +1152,47 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
})
|
||||
end
|
||||
else
|
||||
{:error, issue} ->
|
||||
{:error, :sides, issue} ->
|
||||
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
|
||||
issue: issue,
|
||||
correction: "'sides' is an optional list of {\"position\": string, \"evidence\": [string]}",
|
||||
resubmit: true
|
||||
})
|
||||
|
||||
{:error, :complexity, issue} ->
|
||||
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
|
||||
issue: issue,
|
||||
correction: "'complexity_hint' is one of simple | monetary | capital | novel (panels of 1 | 3 | 23 | 71)",
|
||||
resubmit: true
|
||||
})
|
||||
|
||||
{:error, {:invalid_reference, ref}} ->
|
||||
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
|
||||
issue: "INVALID_SCOPE: #{ref} is not a verse in L1",
|
||||
correction: "Name a verse that exists — chapter and verse within the five books.",
|
||||
resubmit: true
|
||||
})
|
||||
|
||||
{:waiting, reason} when reason in [:no_provision, :provision_exhausted] ->
|
||||
send_error(conn, 503, "MANNA_DEPLETED", %{
|
||||
issue: "No panel can be convened today: sanhedrin_panels_max is #{reason}",
|
||||
correction: "Retry tomorrow, or raise MANNA_SANHEDRIN_PANELS_MAX in the ConfigMap",
|
||||
resubmit: true
|
||||
})
|
||||
|
||||
{:halted, what, reason} ->
|
||||
send_error(conn, 503, "HALT_#{String.upcase(to_string(what))}", %{
|
||||
issue: "The case could not be heard: #{inspect(reason)}",
|
||||
correction: "Retry when the halt clears",
|
||||
resubmit: true
|
||||
})
|
||||
|
||||
{:transition, signal} ->
|
||||
send_error(conn, 503, "CLOUD_MOVING", %{
|
||||
issue: "The cloud is in transition (#{signal})",
|
||||
correction: "Retry when the cloud settles",
|
||||
resubmit: true
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1147,10 +1205,184 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
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"}
|
||||
else: {:error, :sides, "'sides' must be a list of {position, evidence[]} objects"}
|
||||
end
|
||||
|
||||
defp sanhedrin_sides(_), do: {:error, "'sides' must be a list"}
|
||||
defp sanhedrin_sides(_), do: {:error, :sides, "'sides' must be a list"}
|
||||
|
||||
# The complexity hint names the panel (sanhedrin.hrl): absent → simple.
|
||||
@sanhedrin_complexities %{"simple" => :simple, "monetary" => :monetary, "capital" => :capital, "novel" => :novel}
|
||||
defp sanhedrin_complexity(nil), do: {:ok, :simple}
|
||||
|
||||
defp sanhedrin_complexity(hint) when is_binary(hint) do
|
||||
case Map.fetch(@sanhedrin_complexities, hint) do
|
||||
{:ok, c} -> {:ok, c}
|
||||
:error -> {:error, :complexity, "'complexity_hint' #{inspect(hint)} is not one of simple | monetary | capital | novel"}
|
||||
end
|
||||
end
|
||||
|
||||
defp sanhedrin_complexity(other), do: {:error, :complexity, "'complexity_hint' must be a string, got #{inspect(other)}"}
|
||||
|
||||
# The scope: heard from the question (tablet 30), a named verse or a text
|
||||
# match, else the default.
|
||||
defp sanhedrin_scope(case_text), do: VavPipeline.ScopeResolver.resolve(case_text)
|
||||
|
||||
# The roots: the first two stages of the Bereishit cycle on the question —
|
||||
# tohu (decompose) and vayomer (the Shoresh Engine over the verse in scope,
|
||||
# or the question's own Hebrew). Nishma first: the Shema context is
|
||||
# received, and the cloud is honoured, exactly as process/1 does.
|
||||
defp sanhedrin_resolve(case_text, scope, resolved_by) do
|
||||
case VavPipeline.Shema.receive_context(scope) do
|
||||
{:error, reason} ->
|
||||
{:halted, :context_failed, reason}
|
||||
|
||||
{:ok, context} ->
|
||||
case context.cloud_state do
|
||||
:camped ->
|
||||
input = %{source: "chatzer", verse_scope: scope, scope_resolution: resolved_by, content: case_text}
|
||||
|
||||
state =
|
||||
input
|
||||
|> VavPipeline.BereishitCycle.tohu(context)
|
||||
|> VavPipeline.BereishitCycle.vayomer(context)
|
||||
|
||||
roots_full = Map.get(state, :roots_activated, [])
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
roots: Enum.map(roots_full, & &1.shoresh),
|
||||
roots_full: roots_full,
|
||||
torah_ref: {String.to_atom(scope.book), scope.chapter, scope.verse},
|
||||
read_from: Map.get(state, :roots_read_from)
|
||||
}}
|
||||
|
||||
:moving ->
|
||||
{:transition, :cloud_lifted}
|
||||
|
||||
{:major_transition, signal, _} ->
|
||||
{:transition, signal}
|
||||
|
||||
other ->
|
||||
{:transition, other}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# One panel per case from the day's manna (Vayakhel §3.7 — the ledger
|
||||
# provisions sanhedrin_panels_max). The provision is loaded first, as
|
||||
# Gate.gather_call does (idempotent per date). Without a ledger
|
||||
# (dev/test) nothing is gathered.
|
||||
defp sanhedrin_gather_panel do
|
||||
if Process.whereis(VavPipeline.MannaLedger) do
|
||||
case VavPipeline.MannaLedger.load_provision() do
|
||||
{:ok, :no_provision} ->
|
||||
{:waiting, :no_provision}
|
||||
|
||||
{:error, _} ->
|
||||
{:waiting, :no_provision}
|
||||
|
||||
{:ok, _} ->
|
||||
case VavPipeline.MannaLedger.consume(:sanhedrin_panels_max, 1) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, :provision_exhausted} -> {:waiting, :provision_exhausted}
|
||||
{:error, :no_provision} -> {:waiting, :no_provision}
|
||||
end
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# The Tamid's shape for a side: position, reasoning, sources, votes.
|
||||
defp sanhedrin_side(side) when is_map(side) do
|
||||
%{
|
||||
position: Map.get(side, :position, Map.get(side, "position", "")),
|
||||
reasoning: Map.get(side, :reasoning, Map.get(side, "reasoning", "")),
|
||||
sources: side |> Map.get(:torah_refs, Map.get(side, "torah_refs", [])) |> Enum.map(&sanhedrin_ref_string/1),
|
||||
votes: Map.get(side, :vote_count, Map.get(side, "vote_count", 0))
|
||||
}
|
||||
end
|
||||
|
||||
defp sanhedrin_side(_), do: %{position: "", reasoning: "", sources: [], votes: 0}
|
||||
|
||||
# #torah_ref{} arrives from Erlang as {:torah_ref, book, ch, vs}; a tuple
|
||||
# {book, ch, vs} or a map may also appear.
|
||||
defp sanhedrin_ref_string({:torah_ref, book, ch, vs}), do: "#{book} #{ch}:#{vs}"
|
||||
defp sanhedrin_ref_string({book, ch, vs}), do: "#{book} #{ch}:#{vs}"
|
||||
defp sanhedrin_ref_string(%{book: b, chapter: c, verse: v}), do: "#{b} #{c}:#{v}"
|
||||
defp sanhedrin_ref_string(other), do: to_string(inspect(other))
|
||||
|
||||
# Devarim 17:9 — the inquiry is recorded or it did not accumulate. The
|
||||
# decision goes to L3 as an interpretation (panel_size, votes for and
|
||||
# against, processing_component "sanhedrin"), the minority beside it
|
||||
# when it exists. Never turns a served decision into a 500: the record
|
||||
# failing is logged and reported in the reply as `recorded`.
|
||||
defp sanhedrin_record_l3(case_ref, case_text, decision, resolved) do
|
||||
if Process.whereis(VavPipeline.Repo) do
|
||||
maj = decision.majority
|
||||
min = decision.minority
|
||||
{book, ch, vs} = resolved.torah_ref
|
||||
verse_s = "#{book} #{ch}:#{vs}"
|
||||
|
||||
coupling =
|
||||
case resolved.roots do
|
||||
[] -> ["coupled(case(#{case_ref}), verse(#{book}, #{ch}, #{vs}))"]
|
||||
roots -> Enum.map(roots, fn r -> "coupled(root(#{r}), verse(#{book}, #{ch}, #{vs}))" end)
|
||||
end
|
||||
|
||||
attrs = %{
|
||||
case_ref: case_ref,
|
||||
interpretation: to_string(Map.get(maj, :position, "")),
|
||||
status: to_string(decision.outcome),
|
||||
reasoning: "#{case_text} — #{Map.get(maj, :reasoning, "")}",
|
||||
torah_references: Enum.uniq([verse_s | Enum.map(Map.get(maj, :torah_refs, []), &sanhedrin_ref_string/1)]),
|
||||
roots_involved: Enum.map(resolved.roots, &to_string/1),
|
||||
panel_size: decision.panel_size,
|
||||
vote_count_for: Map.get(maj, :vote_count, 0),
|
||||
vote_count_against: Map.get(min, :vote_count, 0),
|
||||
processing_component: "sanhedrin",
|
||||
coupling_trace: coupling
|
||||
}
|
||||
|
||||
try do
|
||||
case VavPipeline.L3.Store.record_interpretation(attrs) do
|
||||
{:ok, interp} ->
|
||||
Logger.info("L3: interpretation #{interp.id} recorded for sanhedrin #{case_ref} (#{decision.outcome})")
|
||||
|
||||
minority_recorded =
|
||||
if Map.get(min, :vote_count, 0) > 0 do
|
||||
case VavPipeline.L3.Store.record_minority(%{
|
||||
interpretation_id: interp.id,
|
||||
dissenting_reasoning: "#{Map.get(min, :position, "")} — #{Map.get(min, :reasoning, "")}",
|
||||
torah_references: Enum.map(Map.get(min, :torah_refs, []), &sanhedrin_ref_string/1),
|
||||
grounding_type: "achorai"
|
||||
}) do
|
||||
{:ok, _} ->
|
||||
"recorded"
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("L3: sanhedrin #{case_ref} minority NOT recorded: #{inspect(reason)}")
|
||||
"failed"
|
||||
end
|
||||
else
|
||||
"none"
|
||||
end
|
||||
|
||||
%{l3: "recorded", interpretation_id: interp.id, minority: minority_recorded}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("L3: sanhedrin #{case_ref} NOT recorded: #{inspect(reason)}")
|
||||
%{l3: "failed", reason: inspect(reason)}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("L3: sanhedrin #{case_ref} NOT recorded: #{Exception.message(e)}")
|
||||
%{l3: "failed", reason: Exception.message(e)}
|
||||
end
|
||||
else
|
||||
%{l3: "absent"}
|
||||
end
|
||||
end
|
||||
|
||||
defp side_ok?(%{"position" => p} = side) when is_binary(p) do
|
||||
case Map.get(side, "evidence", []) do
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
defmodule VavPipeline.SanhedrinConsult do
|
||||
@moduledoc """
|
||||
What a judge consults (Architecture §3.2: "consults the specification
|
||||
(L0/L1)"). Registered in the `sanhedrin` application env as
|
||||
`{__MODULE__, :consult}` at pipeline start; `judge.erl` calls it once
|
||||
per vote with the step its seat performs — `:interpret`, `:challenge`
|
||||
or `:resolve` — and the case's roots and question.
|
||||
|
||||
Tablet 55: before this the judge chose among six canned positions by a
|
||||
hash of the clock. Now the only ground a vote can stand on is the
|
||||
Talmudic Engine's answer over the case's roots — the same engine, the
|
||||
same rules, the same verses the bedikah and the Bereishit cycle use.
|
||||
When the engine's socket is not running, or answers with an error, the
|
||||
judge is told `{:error, reason}` and abstains. Nothing here is answered
|
||||
from a stub (tablet 41): `TalmudicEngineClient`'s stub fallbacks are
|
||||
deliberately NOT used — a court that accepts on a stub is the calf.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
alias VavPipeline.TalmudicEngineSocket
|
||||
|
||||
@books ~w(bereishit shemot vayikra bamidbar devarim)a
|
||||
|
||||
@typedoc "What the judge receives: a status the engine gave, the reading, its verses."
|
||||
@type consultation :: %{
|
||||
required(:status) => String.t(),
|
||||
required(:text) => String.t(),
|
||||
required(:verses) => [{atom(), pos_integer(), pos_integer()}],
|
||||
optional(:kushya) => String.t(),
|
||||
optional(:grounding) => [String.t()]
|
||||
}
|
||||
|
||||
@doc """
|
||||
Called from the judge (Erlang): `consult(Step, CaseRef, Roots, Question, Context)`.
|
||||
Roots are the case's shoresh names as the cycle produces them; the
|
||||
Erlang side hands binaries or charlists — both are accepted.
|
||||
"""
|
||||
@spec consult(:interpret | :challenge | :resolve, binary() | charlist(), list(), binary() | charlist(), map()) ::
|
||||
{:ok, consultation()} | {:error, term()}
|
||||
def consult(step, case_ref, roots, question, _context) when step in [:interpret, :challenge, :resolve] do
|
||||
if Process.whereis(TalmudicEngineSocket) do
|
||||
ctx = %{
|
||||
context_roots: Enum.map(roots, &to_string/1),
|
||||
question: to_string(question)
|
||||
}
|
||||
|
||||
ref = "#{to_string(case_ref)}-#{step}"
|
||||
|
||||
case apply(TalmudicEngineSocket, step, [ref, ctx]) do
|
||||
{:ok, %{"status" => status} = reply} when is_binary(status) ->
|
||||
{:ok, normalize(status, reply)}
|
||||
|
||||
{:ok, other} ->
|
||||
{:error, {:unexpected_reply, other}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("SanhedrinConsult: engine #{step} failed for #{ref}: #{inspect(reason)}")
|
||||
{:error, {:engine_unavailable, reason}}
|
||||
end
|
||||
else
|
||||
{:error, :engine_socket_not_started}
|
||||
end
|
||||
end
|
||||
|
||||
def consult(step, _ref, _roots, _q, _ctx), do: {:error, {:unknown_step, step}}
|
||||
|
||||
@doc "Parse the engine's verse strings (`verse_ref(bereishit,1,1,...)`, `verse(shemot,3,14)`) into `{book, ch, vs}`."
|
||||
@spec verses_from([String.t()]) :: [{atom(), pos_integer(), pos_integer()}]
|
||||
def verses_from(strings) when is_list(strings) do
|
||||
strings
|
||||
|> Enum.flat_map(fn s ->
|
||||
case Regex.run(~r/verse(?:_ref)?\((bereishit|shemot|vayikra|bamidbar|devarim)\s*,\s*(\d+)\s*,\s*(\d+)/, to_string(s)) do
|
||||
[_, book, ch, vs] -> [{String.to_existing_atom(book), String.to_integer(ch), String.to_integer(vs)}]
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
def verses_from(_), do: []
|
||||
|
||||
@doc false
|
||||
def books, do: @books
|
||||
|
||||
defp normalize(status, reply) do
|
||||
interp = Map.get(reply, "interpretation") || %{}
|
||||
chall = Map.get(reply, "challenge") || %{}
|
||||
|
||||
base = %{
|
||||
status: status,
|
||||
text: to_string(Map.get(interp, "text", "")),
|
||||
verses: verses_from(List.wrap(Map.get(interp, "verses", []))),
|
||||
grounding: List.wrap(Map.get(interp, "grounding", [])) |> Enum.map(&to_string/1)
|
||||
}
|
||||
|
||||
case Map.get(chall, "kushya") do
|
||||
k when is_binary(k) and k != "" -> Map.put(base, :kushya, k)
|
||||
_ -> base
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user