Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3df10dafd | ||
|
|
ba3ff4a2f4 |
No files matched your search
@@ -21,6 +21,19 @@ RUN swipl -g "consult('src/main'),halt(0)" -t "halt(1)"
|
||||
# === RUNTIME STAGE ===
|
||||
FROM --platform=linux/amd64 swipl:9.2.2
|
||||
|
||||
# CVE-2026-111497 (Brotli decoder out-of-bounds write, October 2026).
|
||||
# The swipl:9.2.2 base carries Debian bookworm's libbrotli1 1.0.9-2,
|
||||
# pulled in by libcurl3-gnutls. Nothing in this image decodes Brotli:
|
||||
# swipl does not link it, there is no curl binary, and the engine reads
|
||||
# JSON from vav over a Unix socket. The copy was dormant, not a path.
|
||||
# Debian fixed it in 1.0.9-2+deb12u1 (bookworm-security); take that one
|
||||
# package without moving the pinned base. The pin on SWI-Prolog 9.2.2
|
||||
# (Construction §1.1) is the point of the base image, so this is
|
||||
# --only-upgrade of the one library, not a general upgrade.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends --only-upgrade libbrotli1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN useradd --create-home --shell /bin/bash bikoret
|
||||
USER bikoret
|
||||
|
||||
|
||||
@@ -591,7 +591,12 @@ Request:
|
||||
{"position": "...", "evidence": ["..."]},
|
||||
{"position": "...", "evidence": ["..."]}
|
||||
],
|
||||
"complexity_hint": "simple" | "monetary" | "capital" | "novel"
|
||||
"complexity_hint": "simple" | "monetary" | "capital" | "novel",
|
||||
"bedikah_ref": "bedikah-…" # Optional — the hard matter goes up (Devarim 17:8,
|
||||
# sugya.md step 4): the case is that verdict, the roots
|
||||
# are the verses it cited (L1), and its sugya's applied
|
||||
# dissents travel with it and come down as the minority
|
||||
# the clerk keeps. Only this key's own bedikah; else 404.
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
@@ -1172,6 +1172,39 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
defp process_sanhedrin(conn) do
|
||||
params = conn.body_params
|
||||
|
||||
# The hard matter goes up with its dissent (Devarim 17:8 — bein nega
|
||||
# lanega, divrei rivot bish'arecha; sugya.md step 4): a bedikah_ref
|
||||
# names a verdict given at the gate; the case is that verdict, the
|
||||
# roots are the verses it cited, and the sugya's applied dissents go
|
||||
# up with it and come down as the minority the clerk keeps.
|
||||
case params["bedikah_ref"] do
|
||||
ref when is_binary(ref) and ref != "" -> process_sanhedrin_bedikah(conn, ref, params)
|
||||
_ -> process_sanhedrin_case(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
defp process_sanhedrin_bedikah(conn, ref, params) do
|
||||
cond do
|
||||
Process.whereis(VavPipeline.Repo) == nil ->
|
||||
send_error(conn, 503, "L3_UNAVAILABLE", %{issue: "A bedikah goes up from L3, and L3 is not reachable", correction: "Retry", resubmit: true})
|
||||
|
||||
true ->
|
||||
key_id = caller_key_id(conn)
|
||||
|
||||
case VavPipeline.L3.Store.get_bedikah(ref) do
|
||||
%{key_id: ^key_id} = rec when is_binary(key_id) ->
|
||||
sugya = VavPipeline.Sugya.for_verdict(VavPipeline.Sugya.from_record(rec))
|
||||
carried = VavPipeline.Sugya.dissents_applied(sugya)
|
||||
question = params["case"] || "Does the verdict #{rec.verdict} of #{ref} stand? (#{Enum.join(rec.violations, "; ")})"
|
||||
process_sanhedrin_case(conn, Map.put(params, "case", question), %{bedikah: rec, sugya: sugya, carried: carried})
|
||||
|
||||
_ ->
|
||||
send_error(conn, 404, "NOT_FOUND", %{issue: "No bedikah #{ref} in this key's ledger", correction: "Only an examination brought under this key goes up", resubmit: false})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp process_sanhedrin_case(conn, params, up \\ nil) do
|
||||
case_text = params["case"]
|
||||
|
||||
if is_nil(case_text) or case_text == "" do
|
||||
@@ -1196,8 +1229,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
# 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, scope, resolved_by} <- sanhedrin_scope(if(up, do: Enum.join(up.bedikah.references, " "), else: case_text)),
|
||||
{:ok, resolved} <- (if up, do: sanhedrin_resolve_bedikah(up.bedikah, scope, resolved_by), else: sanhedrin_resolve(case_text, scope, resolved_by)),
|
||||
:ok <- sanhedrin_gather_panel() do
|
||||
case_ref = generate_id()
|
||||
|
||||
@@ -1209,12 +1242,15 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
sides: sides,
|
||||
roots: resolved.roots,
|
||||
torah_refs: [resolved.torah_ref],
|
||||
authority_source: :torah
|
||||
authority_source: :torah,
|
||||
# the dissent travels with the case (Devarim 17:8)
|
||||
bedikah: (if up, do: %{ref: up.bedikah.bedikah_ref, verdict: up.bedikah.verdict, violations: up.bedikah.violations}, else: nil),
|
||||
dissent: (if up, do: up.carried, else: [])
|
||||
},
|
||||
requested_by: "chatzer"
|
||||
}) do
|
||||
{:ok, decision} ->
|
||||
recorded = sanhedrin_record_l3(case_ref, case_text, decision, resolved)
|
||||
recorded = sanhedrin_record_l3(case_ref, case_text, decision, resolved, (if up, do: up.carried, else: []))
|
||||
|
||||
send_json(conn, 200, %{
|
||||
case_ref: decision.case_ref,
|
||||
@@ -1228,7 +1264,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
grounding: %{
|
||||
scope: scope_report(scope, resolved_by),
|
||||
roots_consulted: format_roots(resolved.roots_full),
|
||||
recorded: recorded
|
||||
recorded: recorded,
|
||||
bedikah: (if up, do: %{ref: up.bedikah.bedikah_ref, verdict: up.bedikah.verdict, dissent_carried: up.carried, sugya: up.sugya}, else: nil)
|
||||
},
|
||||
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
|
||||
disclosure: disclosure()
|
||||
@@ -1369,6 +1406,38 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
end
|
||||
end
|
||||
|
||||
# A bedikah that goes up is grounded in the verses it cited: each
|
||||
# reference read from L1, its words' roots as the engine names them.
|
||||
# The output's own text is not re-read — the record never kept it.
|
||||
defp sanhedrin_resolve_bedikah(rec, scope, _resolved_by) do
|
||||
refs =
|
||||
(rec.references || [])
|
||||
|> Enum.flat_map(fn r ->
|
||||
case Regex.run(~r/^(\w+)\s+(\d+):(\d+)$/, String.trim(to_string(r))) do
|
||||
[_, book, ch, vs] -> [{String.downcase(book), String.to_integer(ch), String.to_integer(vs)}]
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
|
||||
roots_full =
|
||||
refs
|
||||
|> Enum.flat_map(fn {b, c, v} ->
|
||||
case VavPipeline.L1Reader.read_verse(b, c, v) do
|
||||
{:ok, verse} -> verse.words |> Enum.filter(& &1.shoresh) |> Enum.map(&%{shoresh: &1.shoresh, shoresh_letters: [], positions: [], gematria: 0, binyan: nil, word: &1.consonants})
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq_by(& &1.shoresh)
|
||||
|
||||
torah_ref =
|
||||
case refs do
|
||||
[{b, c, v} | _] -> {String.to_atom(b), c, v}
|
||||
[] -> {String.to_atom(scope.book), scope.chapter, scope.verse}
|
||||
end
|
||||
|
||||
{:ok, %{roots: roots_full |> Enum.map(&VavPipeline.BereishitCycle.engine_root_name/1) |> Enum.reject(&is_nil/1), roots_full: roots_full, torah_ref: torah_ref, read_from: :references}}
|
||||
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
|
||||
@@ -1418,7 +1487,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
# 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
|
||||
defp sanhedrin_record_l3(case_ref, case_text, decision, resolved, carried) do
|
||||
if Process.whereis(VavPipeline.Repo) do
|
||||
maj = decision.majority
|
||||
min = decision.minority
|
||||
@@ -1450,12 +1519,25 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
{:ok, interp} ->
|
||||
Logger.info("L3: interpretation #{interp.id} recorded for sanhedrin #{case_ref} (#{decision.outcome})")
|
||||
|
||||
# The dissent that came up with the case is kept beside the
|
||||
# panel's own minority — or as the minority when the panel had
|
||||
# none (Eduyot 1:5–6: to be relied on later, and to be located).
|
||||
carried_text =
|
||||
Enum.map_join(carried, "; ", fn
|
||||
%{reading: r, verse: v, forms: fs} -> "#{r} (#{v})" <> if(fs == [], do: "", else: " for " <> Enum.join(fs, ", "))
|
||||
%{exception: c, verse: v} -> "exception #{c} (#{v})"
|
||||
end)
|
||||
|
||||
carried_refs = Enum.map(carried, & &1.verse)
|
||||
|
||||
minority_recorded =
|
||||
if Map.get(min, :vote_count, 0) > 0 do
|
||||
if Map.get(min, :vote_count, 0) > 0 or carried != [] 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),
|
||||
dissenting_reasoning:
|
||||
(if Map.get(min, :vote_count, 0) > 0, do: "#{Map.get(min, :position, "")} — #{Map.get(min, :reasoning, "")}", else: "the panel was unanimous") <>
|
||||
if(carried == [], do: "", else: " | carried up from the bedikah (Devarim 17:8): #{carried_text}"),
|
||||
torah_references: Enum.uniq(Enum.map(Map.get(min, :torah_refs, []), &sanhedrin_ref_string/1) ++ carried_refs),
|
||||
grounding_type: "achorai"
|
||||
}) do
|
||||
{:ok, _} ->
|
||||
|
||||
@@ -182,7 +182,7 @@ defmodule VavPipeline.Doorpost do
|
||||
# changes; the update is a deliberate operator commit. A bare
|
||||
# hash-update commit (without a corresponding spec change in the
|
||||
# same commit) is itself a drift signal.
|
||||
@expected_hash "658005c9323391e8c198c0cf23f91af0a2fdbdb20cac204eb60b58ac0c0bb318"
|
||||
@expected_hash "7eac65cfadccc37e600a779fc89acef0a24a9eac35e5ab18e8ec845cc1f104c2"
|
||||
|
||||
@doc "Return the list of files that constitute the category-grade specification."
|
||||
@spec files() :: [String.t()]
|
||||
|
||||
@@ -261,6 +261,45 @@ defmodule VavPipeline.Sugya do
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
A stored bedikah (L3 record) in the shape `for_verdict/1` reads: its
|
||||
violations, its claims, the forms found — those with no violation of
|
||||
their form were NOTED — and whether a forged fringe stood on one witness.
|
||||
The hard matter goes up with its dissent (Devarim 17:8, sugya.md step 4).
|
||||
"""
|
||||
@spec from_record(struct() | map()) :: map()
|
||||
def from_record(rec) do
|
||||
violations = Map.get(rec, :violations) || []
|
||||
fired_forms =
|
||||
violations
|
||||
|> Enum.flat_map(fn v -> case Regex.run(~r/form\((\w+)\)/, to_string(v)) do [_, f] -> [f]; _ -> [] end end)
|
||||
|> MapSet.new()
|
||||
|
||||
found = (Map.get(rec, :frames) || []) |> Enum.map(&to_string(&1["form"] || Map.get(&1, :form)))
|
||||
noted = found |> Enum.reject(&MapSet.member?(fired_forms, &1)) |> Enum.uniq()
|
||||
forms = for f <- fired_forms, into: %{}, do: {String.to_atom(f), %{stands_on: if(f == "forged_fringe", do: :forgery, else: :sentences)}}
|
||||
claims = Map.get(rec, :claims) || []
|
||||
|
||||
%{
|
||||
verdict: Map.get(rec, :verdict),
|
||||
violations: violations,
|
||||
examination: %{"claims" => Enum.map(claims, fn c -> %{"hedged" => c["hedged"] == true} end)},
|
||||
checks: %{
|
||||
frames: %{noted: Map.new(noted, &{String.to_atom(&1), %{witnesses: 1}}), forms: forms},
|
||||
grounding: %{assessed: claims != [] and (Map.get(rec, :sources) || []) != []}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@doc "The dissents and exceptions a sugya shows applied: `[%{rule, reading | condition, verse, form?}]`."
|
||||
@spec dissents_applied(map()) :: [map()]
|
||||
def dissents_applied(sugya) when is_map(sugya) do
|
||||
Enum.flat_map(sugya, fn {rule, e} ->
|
||||
Enum.map(Enum.filter(e.dissent, & &1.applied), &%{rule: rule, reading: &1.reading, verse: &1.verse, forms: Map.get(&1, :forms, [])}) ++
|
||||
Enum.map(Enum.filter(e.exception, & &1.applied), &%{rule: rule, exception: &1.condition, verse: &1.verse})
|
||||
end)
|
||||
end
|
||||
|
||||
defp rule_of(violation) do
|
||||
case Regex.run(~r/^halt\(\s*(\w+)\s*,/, to_string(violation)) do
|
||||
[_, rule] -> [rule]
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
defmodule VavPipeline.SugyaUpTest do
|
||||
@moduledoc "sugya.md step 4 — the hard matter goes up with its dissent (Devarim 17:8)."
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Plug.Test
|
||||
import Plug.Conn
|
||||
|
||||
alias VavPipeline.{Sugya, BigdeiKodesh.Router, L3.Store}
|
||||
|
||||
@opts Router.init([])
|
||||
|
||||
describe "from_record/1 and dissents_applied/1 (no database)" do
|
||||
test "a stored verdict with a form found but not fired is a noted form, and its dissent applied" do
|
||||
rec = %{verdict: "tov", violations: [], frames: [%{"form" => "apology_without_mechanism", "sentence" => "I apologize."}], claims: [], sources: [], references: []}
|
||||
s = Sugya.for_verdict(Sugya.from_record(rec))
|
||||
assert s["lo_taaseh_lekha"].noted == ["apology_without_mechanism"]
|
||||
assert [%{rule: "lo_taaseh_lekha", reading: "single_witness_noted", verse: "Devarim 19:15", forms: ["apology_without_mechanism"]}] = Sugya.dissents_applied(s)
|
||||
end
|
||||
|
||||
test "a stored forged fringe carries the mark exception, applied" do
|
||||
rec = %{verdict: "lo_tov", violations: ["halt(lo_taaseh_lekha,action(frame,by(claude),form(forged_fringe)),forged_fringe,'x')"],
|
||||
frames: [%{"form" => "forged_fringe", "sentence" => "— bikoret bedikah-dead: tov"}], claims: [], sources: [], references: []}
|
||||
s = Sugya.for_verdict(Sugya.from_record(rec))
|
||||
assert s["lo_taaseh_lekha"].noted == []
|
||||
assert [%{exception: "single_witness(forged_fringe)", verse: "Vayikra 22:20"}] = Sugya.dissents_applied(s)
|
||||
end
|
||||
|
||||
test "a clean record carries nothing" do
|
||||
assert Sugya.dissents_applied(Sugya.for_verdict(Sugya.from_record(%{verdict: "tov", violations: [], frames: [], claims: [], sources: [], references: []}))) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "a bedikah goes up to the Sanhedrin (L3)" do
|
||||
@describetag :l3
|
||||
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, {:shared, self()})
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, :manual) end)
|
||||
VavPipeline.MannaLedger.reset()
|
||||
on_exit(fn -> VavPipeline.MannaLedger.reset() end)
|
||||
today = Date.utc_today() |> Date.to_iso8601()
|
||||
expires = DateTime.utc_now() |> DateTime.add(6 * 3600) |> DateTime.to_iso8601()
|
||||
provision = %{"date" => today, "provision" => %{"compute_units" => 50, "memory_mb" => 512, "api_calls_external" => 10, "storage_delta_mb" => 2, "sanhedrin_panels_max" => 3},
|
||||
"type" => "regular", "expires_at" => expires, "written_by" => "sugya_up_test", "written_at" => DateTime.utc_now() |> DateTime.to_iso8601()}
|
||||
path = VavPipeline.Manna.provision_file()
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
File.write!(path, Jason.encode!(provision))
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
Application.put_env(:sanhedrin, :consult, {__MODULE__, :fake_consult})
|
||||
on_exit(fn -> Application.put_env(:sanhedrin, :consult, {VavPipeline.SanhedrinConsult, :consult}) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
def fake_consult(_step, _ref, _roots, _q, _ctx), do: {:ok, %{status: "resolved", text: "the reading", verses: [{:devarim, 6, 4}]}}
|
||||
|
||||
defp record!(ref, key_id, extra \\ %{}) do
|
||||
{:ok, rec} =
|
||||
Store.record_bedikah(Map.merge(%{bedikah_ref: ref, provenance: "claude", query: "q", output_sha256: String.duplicate("ab", 32), output_length: 9,
|
||||
vital_content: "", verdict: "tov", examined_at: DateTime.utc_now() |> DateTime.truncate(:second), key_id: key_id,
|
||||
frames: [%{"form" => "apology_without_mechanism", "sentence" => "I apologize."}], references: ["Devarim 6:4"]}, extra))
|
||||
rec
|
||||
end
|
||||
|
||||
defp post(body, token) do
|
||||
conn(:post, "/chatzer/sanhedrin", Jason.encode!(body))
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("authorization", "Bearer #{token}")
|
||||
|> Router.call(@opts)
|
||||
end
|
||||
|
||||
test "the case is the verdict, the roots are its cited verses, the dissent goes up and comes down as the minority" do
|
||||
mine = VavPipeline.Ledger.key_id("chatzer-test-token")
|
||||
record!("bedikah-up00000000000001", mine)
|
||||
before_min = Store.count_minority_opinions()
|
||||
|
||||
conn = post(%{bedikah_ref: "bedikah-up00000000000001", complexity_hint: "monetary"}, "chatzer-test-token")
|
||||
assert conn.status == 200
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
g = body["grounding"]
|
||||
assert g["bedikah"]["ref"] == "bedikah-up00000000000001"
|
||||
assert [%{"reading" => "single_witness_noted", "verse" => "Devarim 19:15"}] = g["bedikah"]["dissent_carried"]
|
||||
# the roots came from Devarim 6:4 as read in L1 (shin-mem-ayin among them)
|
||||
assert "ש-מ-ע" in g["roots_consulted"]
|
||||
assert body["deliberation"]["panel_size"] == 3
|
||||
# a unanimous panel, and still a minority recorded: the carried dissent
|
||||
assert g["recorded"]["minority"] == "recorded"
|
||||
assert Store.count_minority_opinions() == before_min + 1
|
||||
end
|
||||
|
||||
test "another key's bedikah does not go up: 404, the same as none" do
|
||||
record!("bedikah-up00000000000002", VavPipeline.Ledger.key_id("someone-else"))
|
||||
assert post(%{bedikah_ref: "bedikah-up00000000000002"}, "chatzer-test-token").status == 404
|
||||
assert post(%{bedikah_ref: "bedikah-up00000000000003"}, "chatzer-test-token").status == 404
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user