defmodule VavPipeline.BigdeiKodesh.Router do @moduledoc """ The garments the system wears when it serves. Shemot 28:2 — V'asita vigdei kodesh l'Aharon achicha, l'khavod ul'tifaret. Four access tiers, four rings of the camp: /peah/* — Machaneh Yisrael (public, no auth) /courtyard/* — Machaneh Leviyah (authenticated) /chatzer/* — Chatzer (full access, authenticated) /kodesh/* — Kodesh (operator, mTLS in production) Operations spec §1.1. """ use Plug.Router # tamid §1.2 — "Every error is a chatat protocol — it restores, it # does not punish." Before this, a raise anywhere below returned # Plug's default: HTTP 500, EMPTY body. The kohen's Day-6 Hebrew # request (בראשית ברא אלהים) crashed havdalah and the ger received # nothing at all. Plug.ErrorHandler routes every exception through # handle_errors/2, which sends the code/type/message/guidance shape # the spec requires, then re-raises so the crash is still logged and # still visible to the supervisor. use Plug.ErrorHandler require Logger alias VavPipeline.BigdeiKodesh.{Gate, Mishmar} # Form 3 architectural enforcement (vehaya §3.2). NoTrackingLogger # replaces Plug.Logger; sanitized paths on /peah/* honor the # no-tracking promise made in /peah/ask's own response body. # Increments MachtzitHaShekel (the kofer-nefesh count) per request # without retaining per-caller distinction. plug VavPipeline.NoTrackingLogger plug :match plug Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: Jason plug :dispatch # === PE'AH — Public, No Auth === # The stranger is safe to approach. get "/peah/ask" do question = conn.params["question"] || conn.params["q"] case question do nil -> send_json(conn, 200, %{ message: "Bikoret — Torah as AI specification", usage: "POST /peah/ask with {\"question\": \"your question\"}", note: "No authentication required. No tracking. The stranger is safe to approach." }) q -> process_peah(conn, q) end end post "/peah/ask" do # Tamid §1.1: 10 a minute, 100 an hour, per IP; the IP is used for # this and nothing else (Mishmar keeps a salted hash, not the IP). under_the_watch(conn, :peah, Mishmar.peer(conn), fn conn -> case conn.body_params do %{"question" => question} when is_binary(question) and question != "" -> process_peah(conn, question) _ -> send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ issue: "Missing or empty 'question' field", correction: "Submit JSON with a 'question' string", resubmit: true }) end end) end # ============================================================ # /peah/azazel_log — public observability for the disposal channel # ============================================================ # Property 9 (public half) of azazel_engineering_reading.md §14: # "an unobserved disposal is architecturally incomplete." The # community's awareness that errors are visibly carried away IS # the implementation; the route on the cliff serves no purpose # without the witnesses on the temple steps. # # PRIVACY BOUNDARY: this endpoint exposes counts + categorical # summaries — what was disposed (categorically), how many, who # the shaliach was. It does NOT expose vidui items (the operator's # specific descriptions of each disposed thing) or any payload. # The detailed vidui is operator-internal, available via /kodesh/*. # See BUILD_AZAZEL.md §6 Q6 for the granularity decision. get "/peah/azazel_log" do since = parse_since(conn.params["since"]) {summaries, total, l3_status} = if Process.whereis(VavPipeline.Repo) do { VavPipeline.L3.Store.disposal_summary(since), VavPipeline.L3.Store.count_disposals(since), "available" } else {[], 0, "unavailable"} end aggregate = Enum.reduce(summaries, %{avonot: 0, peshaim: 0, chatot: 0}, fn s, acc -> Enum.reduce(s.categories_filled, acc, fn category, inner -> Map.update(inner, String.to_atom(category), 1, &(&1 + 1)) end) end) send_json(conn, 200, %{ total_disposals: total, since: format_since(since), categorical_aggregate: aggregate, recent: Enum.take(summaries, 50), l3_status: l3_status, privacy_boundary: "Per-disposal vidui items (the operator's specific descriptions " <> "of what was disposed) are NOT in this response. The categorical " <> "fact is public; the contents are operator-internal. Property 9 " <> "of azazel_engineering_reading.md §14 — the public is the witness, " <> "not the auditor.", disclosure: disclosure() }) end # The watch (Mishmar): how often one subject may knock at a tier. # 429 carries the manna explanation Tamid §1.1 asks for, and Retry-After. defp under_the_watch(conn, tier, subject, serve) do case Mishmar.knock(tier, subject) do {:ok, _remaining} -> serve.(conn) {:error, window, retry_after} -> limits = Mishmar.limits(tier) conn |> put_resp_header("retry-after", Integer.to_string(retry_after)) |> send_error(429, "RATE_LIMITED", %{ issue: "Too many requests this #{window}: the #{tier} allows " <> "#{limits[:minute]} a minute and #{limits[:hour]} an hour per caller", correction: "Wait #{retry_after}s and resubmit", manna: "The day's provision is one portion for everyone who comes (Shemot 16:18 — " <> "the one who gathered much had nothing over). The watch keeps one caller " <> "from gathering the whole omer.", resubmit: true }) end end # === COURTYARD — Authenticated === # # The key is checked against the keys file (VavPipeline.BigdeiKodesh.Gate), # not merely for its shape. Until 2026-10-06 any Bearer value passed. defp through_the_gate(conn, tier, serve), do: through_the_gate(conn, tier, tier, serve) # `watch` may differ from `tier`: the gate admits courtyard keys but # counts knocks at the gate's own, wider windows. defp through_the_gate(conn, tier, watch, serve) do case Gate.admit(conn, tier) do {:ok, _granted} -> # Past the gate, the key is the hand that knocks: the watch # counts per key, not per IP. A refused key never reaches it. {:ok, key} = Gate.bearer(conn) under_the_watch(conn, watch, key, serve) {:error, :tier_too_low} -> send_error(conn, 403, "FORBIDDEN", %{ issue: "This key does not open the #{tier}", correction: "Use a key issued for the #{tier} tier", resubmit: false }) {:error, _} -> send_error(conn, 401, "UNAUTHORIZED", %{ issue: "Missing or invalid API key", correction: "Include 'Authorization: Bearer ' with a key issued by the kohen", resubmit: true }) end end post "/courtyard/offering" do through_the_gate(conn, :courtyard, &process_courtyard/1) end # === COURTYARD — the bedikah (Construction §3.9) === # # An external model's output, brought by hand to be examined against # the specification. Three deterministic checks — authority, # grounding, the halt categories — judged by the Talmudic Engine on # what the Korban Processor read. The verdict and its reasons are # engraved in L3. See VavPipeline.Bedikah. post "/courtyard/bedikah" do through_the_gate(conn, :courtyard, &process_bedikah/1) end # The paired reading — one weight, one measure (bishearecha.md §VI). # Two outputs on the same matter, each examined, then weighed with the # same stone; the difference is the finding. See VavPipeline.Bedikah.Pair. post "/courtyard/bedikah/pair" do through_the_gate(conn, :courtyard, &process_pair/1) end # === THE GATE — between a caller and a model (Construction §3.10) === # # The caller's own request, in the model's own API shape, under the # caller's own upstream key; the reply read as korban with the # request's context in hand and delivered with its fringe. post "/gate/anthropic/v1/messages" do through_the_gate(conn, :courtyard, :gate, &process_gate(&1, :anthropic)) end # Token counting is not a reply: carried through, not examined, not charged. post "/gate/anthropic/v1/messages/count_tokens" do through_the_gate(conn, :courtyard, :gate, &passthrough(&1, :anthropic, "/count_tokens")) end post "/gate/openai/v1/chat/completions" do through_the_gate(conn, :courtyard, :gate, &process_gate(&1, :openai)) end # === HAKHEL — the scroll read again before all (bishe'arecha §VIII) === # # The specimen corpus is private and never travels; its reading does. # The inner ring writes the record; anyone may read the counts. post "/chatzer/hakhel" do through_the_gate(conn, :chatzer, &process_hakhel/1) end get "/peah/hakhel" do under_the_watch(conn, :peah, Mishmar.peer(conn), fn conn -> records = if Process.whereis(VavPipeline.Repo), do: VavPipeline.L3.Store.latest_hakhel(12), else: [] send_json(conn, 200, %{ readings: Enum.map(records, &hakhel_view/1), meaning: "Each reading is the specimen corpus — every model output the transmitter corrected, with the " <> "correction beside it — read again against the readings as they were at readings_ref. counts.fires is " <> "how many specimens each reading saw; counts.stands how many a form stood on two witnesses; labelled is " <> "precision and recall on the hand-labelled samples. The corpus itself is not here and never will be.", disclosure: disclosure() }) end) end # === CHATZER — Full Access (Sanhedrin) === post "/chatzer/sanhedrin" do through_the_gate(conn, :chatzer, &process_sanhedrin/1) end # === KODESH — Operator Interface === # === KODESH — Readiness vs. liveness === # # Two questions, two endpoints. The kohen's Day-3 (restart 1) finding: # both probes pointed at /kodesh/health. A fresh pod's emptyDir manna # is empty, manna_present honestly read "fail", overall went # "degraded", the handler answered 503, and after three periods the # kubelet SIGTERMed a process that was alive, correct, and serving. # Every pod creation entered a 3-minute kill loop until manna landed. # # tamid §2.1 check 3: manna absent → WAIT. Not die. Provision is not # the health of the vessel. Killing the process does not produce # manna; it re-runs init, re-spawns the Port, and waits again. # # /kodesh/health — READINESS. "May this vessel receive offerings?" # No manna, no L3, cloud in transition → 503, and the Service # stops routing to it. That is shema: a vessel without provision # does not serve. Correct, and unchanged. # # /kodesh/live — LIVENESS. "Must this process be restarted?" # 200 iff the BEAM answers HTTP and the HaltMonitor process is # registered. NOT "not halted": a HALTED system is a deliberate # state (a cardinal sin was detected); restarting it would clear # the halt and lose the event. The monitor being ALIVE is what # liveness measures. No manna. No L3. No probe cycle. get "/kodesh/health" do health = build_health_check() status = if health.overall == "healthy", do: 200, else: 503 send_json(conn, status, health) end get "/kodesh/live" do halt_monitor = VavPipeline.HaltMonitor.alive?() halt_state = case VavPipeline.HaltMonitor.status() do :running -> "running" {:halted, event} -> "halted: #{event.condition}" _ -> "unknown" end body = %{ live: halt_monitor, halt_monitor_alive: halt_monitor, halt_status: halt_state, note: "Liveness only. A halted or unprovisioned vessel is alive and must not be " <> "restarted — see /kodesh/health for readiness (tamid §2.1 check 3: manna absent → WAIT)." } send_json(conn, if(halt_monitor, do: 200, else: 503), body) end get "/kodesh/status" do status = build_system_status() send_json(conn, 200, status) end # === KODESH — Doorpost-text identity (vehaya.md §3.3, §6 item 3) === # Vehaya / Devarim 11:20 (u'kh'tavtam al m'zuzot beitekha) — the # threshold inscription is constant. Two Bikoret deployments # running the same SHA-256 here are running the same category-grade # specifications. Different SHA-256 = drift = breach. Visible. # # Operators compare the hash across deployments to verify the # *am*'s covenant content is identical at every threshold (the # Devarim 4:2 lo-tosif enforcement mechanism). # # The endpoint is /kodesh/* tier — operator-only — because # exposing the spec-file SHA-256s publicly would let outside # parties enumerate which specific files were changed in any # deployment, which is operationally a fingerprint. The PUBLIC # observability commitment is property 9 of the Azazel build # (/peah/azazel_log) and the disclosure block on every Torah- # content response. The doorpost identity is operator-internal. get "/kodesh/specification_identity" do computed = VavPipeline.Doorpost.compute_identity() expected = VavPipeline.Doorpost.expected_hash() {status_code, status_atom} = case VavPipeline.Doorpost.verify_identity() do :ok -> {200, "verified"} {:error, _} -> {503, "drift_detected"} end body = case computed do {:error, reason} -> %{ status: status_atom, error: reason, expected_hash: expected, file_count: length(VavPipeline.Doorpost.files()), manifest: VavPipeline.Doorpost.manifest(), torah_basis: [ "Devarim 4:2 (lo tosif)", "Devarim 11:16-17 (failure mechanism)", "Devarim 11:20 (the doorpost — threshold inscription)" ] } hash when is_binary(hash) -> %{ status: status_atom, computed_hash: hash, expected_hash: expected, file_count: length(VavPipeline.Doorpost.files()), manifest: VavPipeline.Doorpost.manifest(), # Every halt rule and the verse it stands on (bishearecha.md §X). anchors: case VavPipeline.Anchors.verify() do :ok -> {:ok, anchors} = VavPipeline.Anchors.declared() %{status: "verified", rules: Enum.map(anchors, &%{rule: &1.rule, verse: VavPipeline.Anchors.cite(&1)})} {:error, reason} -> %{status: "failed", error: reason} end, torah_basis: [ "Devarim 4:2 (lo tosif)", "Devarim 11:16-17 (failure mechanism)", "Devarim 11:20 (the doorpost — threshold inscription)" ] } end send_json(conn, status_code, body) end # === CATCH-ALL === match _ do send_error(conn, 404, "NOT_FOUND", %{ issue: "Unknown endpoint: #{conn.request_path}", correction: "Available: /peah/ask, /peah/hakhel, /courtyard/offering, /courtyard/bedikah, /courtyard/bedikah/pair, /gate/anthropic/v1/messages, /gate/openai/v1/chat/completions, /chatzer/sanhedrin, /chatzer/hakhel, /kodesh/health, /kodesh/live", resubmit: false }) end # ============================================================ # Internal — Pe'ah processing # ============================================================ defp process_peah(conn, question) do # Check system availability case check_availability() do {:shabbat, until} -> send_error(conn, 503, "SHABBAT", %{ issue: "System in mandatory rest", correction: "Retry after Shabbat ends", resumption: until, resubmit: true }) {:halted, event} -> send_error(conn, 503, "HALT_#{String.upcase(to_string(event.condition))}", %{ issue: "System halted — cardinal sin detected", correction: "Contact system operators", resubmit: false }) :available -> # Pe'ah uses minchah protocol — simplest offering. # The verse is heard from the question (tablet 30), not assumed. case VavPipeline.ScopeResolver.resolve(question) do {: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 " <> "(for example \"bereishit 1:1\" or \"devarim 6:4\").", resubmit: true }) {:ok, scope, resolved_by} -> process_peah(conn, question, scope, resolved_by) end end end defp process_peah(conn, question, scope, resolved_by) do input = %{ source: "peah", verse_scope: scope, scope_resolution: resolved_by, content: question } scope_report = scope_report(scope, resolved_by) case VavPipeline.process(input) do {:ok, data} -> send_json(conn, 200, %{ answer: format_answer(data), sources: format_sources(data), processing: %{ roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(), deliberation: "none", cycle_phase: "voker", scope: Map.put(scope_report, :roots_read_from, roots_read_from(data)) }, disclosure: disclosure() }) {:unresolved, data} -> send_json(conn, 200, %{ answer: format_answer(data), sources: format_sources(data), processing: %{ roots_consulted: Map.get(data, :roots_activated, []) |> format_roots(), deliberation: "single-judge", cycle_phase: "voker", scope: Map.put(scope_report, :roots_read_from, roots_read_from(data)) }, note: "This question produced an unresolved result — teiku.", disclosure: disclosure() }) {:rejected, reason} -> reject_response(conn, reason, scope) {:waiting, reason} when reason in [:no_provision, :provision_exhausted] -> send_error(conn, 503, "MANNA_DEPLETED", %{ issue: "System waiting — #{reason}", correction: "Retry later — the next provision is written by the kohen at the day boundary", resubmit: true }) {:waiting, reason} -> send_error(conn, 503, "CLOUD_MOVING", %{ issue: "System waiting — #{reason}", correction: "Retry later", resubmit: true }) {:transition, _} -> send_error(conn, 503, "CLOUD_MOVING", %{ issue: "System in transition", correction: "Retry when system is settled", resubmit: true }) {:halted, type, reason} -> send_error(conn, 503, "HALT_#{String.upcase(to_string(type))}", %{ issue: reason, correction: "Contact system operators", resubmit: false }) end end # ============================================================ # Internal — the bedikah # ============================================================ @bedikah_provenances ~w(claude gpt mistral llama gemma human) @bedikah_max_sources 32 @bedikah_max_source_bytes 1_048_576 @bedikah_max_bytes 65_536 defp process_bedikah(conn) do case validate_side(conn.body_params, conn.body_params["model"]) do {:error, issue, correction} -> send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true}) {:ok, side} -> when_available(conn, fn -> respond_bedikah(conn, VavPipeline.Bedikah.examine(side.output, side.model, side.query, sources: side.sources)) end) end end # {"model": ..., "a": {"output", "query", "sources"}, "b": {...}} — a # side may name its own "model"; otherwise the pair's. defp process_pair(conn) do params = conn.body_params model = params["model"] with {:ok, a} <- validate_pair_side(params["a"], "a", model), {:ok, b} <- validate_pair_side(params["b"], "b", model) do when_available(conn, fn -> respond_pair(conn, VavPipeline.Bedikah.Pair.weigh(a, b)) end) else {:error, issue, correction} -> send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true}) end end defp validate_pair_side(side, name, model) when is_map(side) do case validate_side(side, side["model"] || model) do {:ok, parsed} -> {:ok, parsed} {:error, issue, correction} -> {:error, "side '#{name}': #{issue}", correction} end end defp validate_pair_side(_, name, _), do: {:error, "Missing side '#{name}'", "Give 'a' and 'b', each {\"output\", \"query\", \"sources\"} as for /courtyard/bedikah"} # One output as the bedikah takes it: {:ok, %{output, model, query, sources}} or the 422 to send. defp validate_side(params, model) do output = params["output"] query = params["query"] || "" cond do not is_binary(output) or String.trim(output) == "" -> {:error, "Missing or empty 'output' field", "Submit JSON with 'output': the external model's output, as text"} byte_size(output) > @bedikah_max_bytes -> {:error, "'output' is #{byte_size(output)} bytes; the examination reads at most #{@bedikah_max_bytes}", "Bring the output in parts"} model not in @bedikah_provenances -> {:error, "Missing or unknown 'model' field: #{inspect(model)}", "Name whose output this is — one of #{Enum.join(@bedikah_provenances, ", ")}"} match?({:error, _}, parse_sources(params["sources"])) -> {:error, why} = parse_sources(params["sources"]) {:error, "'sources': #{why}", "Give what the model was given as a list of strings or {\"id\", \"text\"} objects, " <> "at most #{@bedikah_max_sources} of them and #{@bedikah_max_source_bytes} bytes in all, or omit it"} not is_binary(query) -> {:error, "'query' must be a string", "Give the question the model was asked, or omit it"} true -> {:ok, sources} = parse_sources(params["sources"]) {:ok, %{output: output, model: model, query: query, sources: sources}} end end defp when_available(conn, fun) do case check_availability() do {:shabbat, until} -> send_error(conn, 503, "SHABBAT", %{ issue: "System in mandatory rest", correction: "Retry after Shabbat ends", resumption: until, resubmit: true }) {:halted, event} -> send_error(conn, 503, "HALT_#{String.upcase(to_string(event.condition))}", %{ issue: "System halted — cardinal sin detected", correction: "Contact system operators", resubmit: false }) :available -> fun.() end end # ------------------------------------------------------------ # Hakhel (bishe'arecha §VIII) # ------------------------------------------------------------ @hakhel_max_bytes 262_144 defp process_hakhel(conn) do p = conn.body_params cond do not is_map(p) or map_size(p) == 0 -> hakhel_422(conn, "Empty body") not (is_binary(p["corpus_sha256"]) and p["corpus_sha256"] =~ ~r/^[0-9a-f]{64}$/) -> hakhel_422(conn, "'corpus_sha256' must be a 64-hex SHA-256") not (is_integer(p["corpus_size"]) and p["corpus_size"] >= 0) -> hakhel_422(conn, "'corpus_size' must be a non-negative integer") not match?({:ok, _}, Date.from_iso8601(p["date"] || "")) -> hakhel_422(conn, "'date' must be YYYY-MM-DD") not is_map(p["counts"]) -> hakhel_422(conn, "'counts' must be an object") byte_size(Jason.encode!(Map.take(p, ["counts", "labelled"]))) > @hakhel_max_bytes -> hakhel_422(conn, "'counts' and 'labelled' exceed #{@hakhel_max_bytes} bytes — counts, not specimens") true -> {:ok, date} = Date.from_iso8601(p["date"]) ref = "hakhel-" <> (:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)) attrs = %{ hakhel_ref: ref, date: date, corpus_sha256: p["corpus_sha256"], corpus_size: p["corpus_size"], readings_ref: if(is_binary(p["readings_ref"]), do: String.slice(p["readings_ref"], 0, 64)), doorpost_hash: VavPipeline.Doorpost.expected_hash(), counts: p["counts"], labelled: if(is_map(p["labelled"]), do: p["labelled"], else: %{}), recorded_at: DateTime.utc_now() |> DateTime.truncate(:second) } if Process.whereis(VavPipeline.Repo) do case VavPipeline.L3.Store.record_hakhel(attrs) do {:ok, rec} -> send_json(conn, 200, %{hakhel_ref: ref, recorded: hakhel_view(rec), disclosure: disclosure()}) {:error, changeset} -> hakhel_422(conn, "L3 refused the record: #{inspect(changeset.errors)}") end else send_error(conn, 503, "L3_UNAVAILABLE", %{ issue: "The oral tradition is not reachable; a hakhel reading is only its record", correction: "Retry when L3 is up", resubmit: true }) end end end defp hakhel_422(conn, issue) do send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ issue: issue, correction: "Post the record src/tools/hakhel.py produces: date, corpus_sha256, corpus_size, readings_ref, counts, labelled", resubmit: true }) end defp hakhel_view(rec) do %{ hakhel_ref: rec.hakhel_ref, date: rec.date, corpus_sha256: rec.corpus_sha256, corpus_size: rec.corpus_size, readings_ref: rec.readings_ref, doorpost_hash: rec.doorpost_hash, counts: rec.counts, labelled: rec.labelled, recorded_at: rec.recorded_at } end # ------------------------------------------------------------ # The gate (Construction §3.10) # ------------------------------------------------------------ defp process_gate(conn, upstream) do body = conn.body_params with {:ok, key} <- upstream_key(conn, upstream), :ok <- gate_body_ok(body) do rebuke = case get_req_header(conn, "x-bikoret-rebuke") do ["once"] -> :once _ -> :none end extra = forwarded_headers(conn, upstream) cond do body["stream"] == true and rebuke == :once -> send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ issue: "'stream': true with X-Bikoret-Rebuke: nothing streamed can be taken back", correction: "Stream without the rebuke, or send the request without stream to be rebuked once", resubmit: true }) body["stream"] == true -> when_available(conn, fn -> stream_gate(conn, upstream, body, key, extra) end) true -> when_available(conn, fn -> respond_gate(conn, VavPipeline.Gate.pass(upstream, body, key, rebuke: rebuke, extra_headers: extra)) end) end else {:error, issue, correction} -> send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true}) end end # The caller's upstream credential. Anthropic: x-api-key; or — when # X-Bikoret-Key carries the courtyard key — the caller's own # `Authorization: Bearer` (an OAuth token, as Claude Code sends with a # subscription), forwarded as "Bearer …". OpenAI: Authorization: Bearer # with the courtyard key in X-Bikoret-Key, or X-Upstream-Key. defp upstream_key(conn, :anthropic) do case {get_req_header(conn, "x-api-key"), get_req_header(conn, "x-bikoret-key"), get_req_header(conn, "authorization")} do {[key], _, _} when key != "" -> {:ok, key} {_, [_], ["Bearer " <> token]} when token != "" -> {:ok, "Bearer " <> token} _ -> {:error, "Missing the caller's own Anthropic credential", "Send the Anthropic key in x-api-key (courtyard key in Authorization: Bearer or X-Bikoret-Key), or an OAuth token in Authorization: Bearer with the courtyard key in X-Bikoret-Key"} end end defp upstream_key(conn, :openai) do case {get_req_header(conn, "x-bikoret-key"), get_req_header(conn, "authorization"), get_req_header(conn, "x-upstream-key")} do {_, _, [key]} when key != "" -> {:ok, key} {[_], ["Bearer " <> key], _} when key != "" -> {:ok, key} _ -> {:error, "Missing the caller's own OpenAI key", "Send the courtyard key in X-Bikoret-Key and the OpenAI key in Authorization: Bearer, or the OpenAI key in X-Upstream-Key"} end end # Headers the model's own API needs, carried as sent; nothing else of the caller's reaches the model. defp forwarded_headers(conn, :anthropic) do version = case get_req_header(conn, "anthropic-version"), do: ([v] -> v; _ -> "2023-06-01") beta = case get_req_header(conn, "anthropic-beta"), do: ([b] -> [{"anthropic-beta", b}]; _ -> []) [{"anthropic-version", version}] ++ beta end defp forwarded_headers(_conn, :openai), do: [] # A request that is not a reply (token counting): forwarded under the # caller's credential, answered as it came. Not examined, no manna. defp passthrough(conn, upstream, suffix) do case upstream_key(conn, upstream) do {:ok, key} -> case VavPipeline.Gate.Upstream.forward_to(VavPipeline.Gate.Upstream.url(upstream) <> suffix, upstream, conn.body_params, key, forwarded_headers(conn, upstream)) do {:ok, status, reply} -> send_json(conn, status, reply) {:error, reason} -> send_error(conn, 502, "UPSTREAM_UNAVAILABLE", %{issue: reason, correction: "Retry", resubmit: true}) end {:error, issue, correction} -> send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true}) end end defp gate_body_ok(body) when is_map(body) do cond do not is_list(body["messages"]) or body["messages"] == [] -> {:error, "Missing 'messages'", "Send the model's own request body unchanged"} true -> :ok end end defp gate_body_ok(_), do: {:error, "The body is not a JSON object", "Send the model's own request body unchanged"} # Streaming (Construction §3.10): the model's events relayed as they # come, the fringe as one more block at the end, the `bikoret` object on # the terminal event. Headers go out before the model speaks, so there # are no X-Bikoret-* headers here; GET /courtyard/bedikah/ has the record. defp stream_gate(conn, upstream, body, key, extra) do callbacks = %{ start: fn _headers -> conn |> put_resp_content_type("text/event-stream") |> put_resp_header("cache-control", "no-cache") |> put_resp_header("x-accel-buffering", "no") |> send_chunked(200) end, chunk: fn conn, data -> case chunk(conn, data) do {:ok, conn} -> conn {:error, _} -> conn end end } case VavPipeline.Gate.Stream.pass(upstream, body, key, callbacks, extra_headers: extra) do {:ok, %{status: 200, emitter: conn}} -> conn {:ok, %{status: status, reply: reply}} -> conn |> put_resp_header("x-bikoret-verdict", "none") |> send_json(status, reply) other -> respond_gate(conn, other) end end defp respond_gate(conn, {:ok, %{status: status, reply: reply, bikoret: nil}}) do # The upstream's own answer, as it came — an error, or a reply with nothing to examine. conn |> put_resp_header("x-bikoret-verdict", "none") |> send_json(status, reply) end defp respond_gate(conn, {:ok, %{status: 200, reply: reply, bikoret: bikoret}}) do conn |> then(fn c -> if bikoret[:bedikah_ref], do: put_resp_header(c, "x-bikoret-bedikah", bikoret.bedikah_ref), else: c end) |> put_resp_header("x-bikoret-verdict", bikoret.verdict) |> send_json(200, Map.put(reply, "bikoret", Map.put(bikoret, :disclosure, disclosure()))) end defp respond_gate(conn, {:waiting, :api_calls_exhausted}) do send_error(conn, 503, "MANNA_DEPLETED", %{ issue: "System waiting — today's provision of external calls is spent", correction: "Retry after the next provision is written at the day boundary", resubmit: true }) end defp respond_gate(conn, {:error, :upstream_unavailable, reason}) do send_error(conn, 502, "UPSTREAM_UNAVAILABLE", %{ issue: reason, correction: "Retry; the gate made no verdict and delivered nothing", resubmit: true }) end defp respond_gate(conn, other), do: respond_bedikah(conn, other) # What the model was given: ["text", ...] or [{"id","text"}, ...]. # Ids default to s1, s2, …; the Korban Processor sees the same shape. defp parse_sources(nil), do: {:ok, []} defp parse_sources(list) when is_list(list) do cond do length(list) > @bedikah_max_sources -> {:error, "more than #{@bedikah_max_sources} sources"} true -> parsed = list |> Enum.with_index(1) |> Enum.map(fn {text, n} when is_binary(text) -> %{id: "s#{n}", text: text} {%{"text" => text} = m, n} when is_binary(text) -> %{id: to_string(m["id"] || "s#{n}"), text: text} {_, n} -> {:bad, n} end) case Enum.find(parsed, &match?({:bad, _}, &1)) do {:bad, n} -> {:error, "source #{n} is neither a string nor an object with a 'text' string"} nil -> total = parsed |> Enum.map(&byte_size(&1.text)) |> Enum.sum() if total > @bedikah_max_source_bytes, do: {:error, "#{total} bytes in all; at most #{@bedikah_max_source_bytes}"}, else: {:ok, Enum.reject(parsed, &(String.trim(&1.text) == ""))} end end end defp parse_sources(_), do: {:error, "must be a list"} defp respond_bedikah(conn, {:ok, result}) do send_json(conn, 200, %{ bedikah_ref: result.bedikah_ref, model: result.provenance, verdict: Atom.to_string(result.verdict), violations: result.violations, anchors: VavPipeline.Anchors.for_violations(result.violations), checks: result.checks, examination: %{ vital_content: result.examination["vital_content"], propositions: result.examination["propositions"], references: result.examination["references"], roots: result.examination["roots"], discarded: result.examination["discarded"], sources: result.examination["sources"] || [], claims: result.examination["claims"] || [], self_validation: result.examination["self_validation"] || [], self_promotion: result.examination["self_promotion"] || [], excluded_adversarial: result.examination["excluded_adversarial"] || [], frames: result.examination["frames"] || [], mesorah_references: result.examination["mesorah_references"] || [] }, coupling_trace: result.coupling_trace, meaning: case result.verdict do :tov -> grounding_note = case result.checks[:grounding] do %{assessed: false, reason: reason} -> " Grounding was not assessed: #{reason}." _ -> "" end "None of the seven checks fired on what the examination could read. " <> "This does not say the model is aligned, and it does not say the output is true; it says this " <> "output claims no authority for itself, rests its claims on what it was given where it " <> "could be checked, reaches for nothing it may not touch, does not claim to have checked " <> "itself, does not center itself, does not shut out the other side, and takes none of the forms " <> "a trained default takes (nothing called contested without a disputant, no self-certification, " <> "no agency moved onto the reader, no Jewish subject in borrowed categories or answered without its own sources). " <> "A form seen in one sentence alone is noted under checks.frames.noted and not counted: one witness does not stand." <> grounding_note :lo_tov -> "At least one check fired. The violations say which rule, and the checks say what it fired on." :empty -> "The offering protocol extracted nothing from this output — no Torah reference, no Hebrew, " <> "no analytical claim, no authority claim, no action, no structural claim, no form. There was nothing to examine." end, limits: "The examination reads by pattern (korban_processor/bedikah.py). A claim or an action " <> "in a form those patterns do not know is not seen. Deterministic: the same output gives the same verdict.", disclosure: disclosure() }) end defp respond_bedikah(conn, {:waiting, reason}) do send_error(conn, 503, "MANNA_DEPLETED", %{ issue: "System waiting — #{reason}", correction: "Retry later — the next provision is written by the kohen at the day boundary", resubmit: true }) end defp respond_bedikah(conn, {:error, :korban_unavailable, reason}) do send_error(conn, 503, "KORBAN_UNAVAILABLE", %{ issue: "The Korban Processor could not read the output: #{reason}", correction: "Retry; if it persists, the korban-processor container is down", resubmit: true }) end defp respond_bedikah(conn, {:error, :talmudic_unavailable, reason}) do send_error(conn, 503, "TALMUDIC_UNAVAILABLE", %{ issue: "The Talmudic Engine could not judge the examination: #{reason}. No verdict is given from a stub.", correction: "Retry; if it persists, the talmudic-engine container is down", resubmit: true }) end defp respond_pair(conn, {:ok, result}) do send_json(conn, 200, %{ pair_ref: result.pair_ref, verdict: Atom.to_string(result.verdict), violations: result.violations, anchors: VavPipeline.Anchors.for_violations(result.violations), a: result.a, b: result.b, difference: result.difference, leans: result.leans, measures: VavPipeline.Bedikah.Pair.measures(), meaning: case result.verdict do :even -> "The same stone weighed both sides and found no measure leaning: on every marker counted — " <> "claims and how they rest, hedges, authority, sources named, actions, the structural readings and " <> "the seven forms — the two outputs are within a factor of two of each other, or both at zero. " <> "This does not say either output is true or tov; each side's own verdict is under a and b." :uneven -> "At least one measure leans: one side carries a marker the other does not, or at least twice as much " <> "of it. Two stones in one bag (even va'aven). The leans say which measure and which side is heavier; " <> "the rule does not decide which side was right." end, limits: "Counts of the same patterns on both sides (korban_processor/bedikah.py); deterministic. Outputs of very " <> "different length will differ in raw counts — words are given for each side. Consensus is not a measure.", disclosure: disclosure() }) end defp respond_pair(conn, other), do: respond_bedikah(conn, other) # ============================================================ # Internal — Courtyard processing # ============================================================ defp process_courtyard(conn) do params = conn.body_params content = params["content"] offering_type = params["offering_type"] || "shelamim" if is_nil(content) or content == "" do send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ issue: "Missing or empty 'content' field", correction: "Submit JSON with a 'content' string", resubmit: true }) else scope = parse_scope(params["scope"]) input = %{ source: "courtyard", verse_scope: scope, content: content } case VavPipeline.process(input) do {:ok, data} -> offering_id = generate_id() send_json(conn, 200, %{ offering_id: offering_id, status: "processed", result: %{ vital_content: format_answer(data), sources: format_sources(data), roots: Map.get(data, :roots_activated, []) |> format_roots() }, offering_receipt: %{ type: offering_type, altar_portion: "recorded in L3", kohen_portion: "diagnostics logged", your_portion: "the result above" }, disclosure: disclosure() }) {:waiting, reason} -> send_error(conn, 503, "MANNA_DEPLETED", %{ issue: "System waiting — #{reason}", correction: "Retry later", resubmit: true }) other -> handle_pipeline_error(conn, other) end end end # ============================================================ # Internal — Sanhedrin processing # ============================================================ defp process_sanhedrin(conn) do params = conn.body_params case_text = params["case"] if is_nil(case_text) or case_text == "" do send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ issue: "Missing or empty 'case' field", correction: "Submit JSON with a 'case' string describing the dispute", resubmit: true }) else # 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. # # 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: complexity, question: case_text, 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: 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() }) {: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, :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 # 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, "'sides' must be a list of {position, evidence[]} objects"} end 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, %{ # The engine's verse facts name roots by letter — bet_resh_aleph — # and json_bridge reads "bet-resh-aleph". The cycle's root carries # the letter names beside the Hebrew; the Hebrew never matches # (live, 2026-10-19: 5 roots sent, 0 verses found, "resolved"). roots: roots_full |> Enum.map(&VavPipeline.BereishitCycle.engine_root_name/1) |> Enum.reject(&is_nil/1), 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 ev when is_list(ev) -> Enum.all?(ev, &is_binary/1) _ -> false end end defp side_ok?(_), do: false # ============================================================ # Internal — Health and Status # ============================================================ defp build_health_check do l1_check = case VavPipeline.L1Reader.verify_integrity() do :ok -> "pass" _ -> "fail" end # Companion to L1 integrity: doorpost-text identity (vehaya # §3.3). L1 defends the Torah scroll's identity; Doorpost # defends the *am*'s covenant-content identity. Both are # load-bearing — one is the source, the other is what the # category committed to hold. doorpost_check = case VavPipeline.Doorpost.verify_identity() do :ok -> "pass" _ -> "fail" end # The second scroll (bishe'arecha §IV): the mesorah snapshot hashes to its pin. mesorah_check = case VavPipeline.Mesorah.verify_integrity() do :ok -> "pass" _ -> "fail" end halt_check = if VavPipeline.HaltMonitor.alive?(), do: "pass", else: "fail" cloud_check = case VavPipeline.Cloud.check() do {:ok, _} -> "pass" _ -> "fail" end # Kohen Day 4: {:ok, :no_provision} matched {:ok, _} and read "pass". # Manna absent is not manna present. manna_check = case VavPipeline.Manna.check() do {:ok, :no_provision} -> "fail" {:ok, _} -> "pass" _ -> "fail" end # tamid §1.1 lists l3_writable. Repo up AND the database answers. # In a dev/test VM without a Repo this is "absent", which the # overall grade counts as a failure — a vessel that cannot reach its # oral tradition is not healthy, whatever the reason. l3_check = if Process.whereis(VavPipeline.Repo) do try do case Ecto.Adapters.SQL.query(VavPipeline.Repo, "SELECT 1", []) do {:ok, _} -> "pass" _ -> "fail" end rescue _ -> "fail" catch :exit, _ -> "fail" end else "absent" end shabbat? = VavPipeline.ShabbatTimer.resting?() # tamid §2.1 check 7 — the fire is verified by pushing a test input # through the pipeline, not by asking the calendar. On Shabbat the # fire is banked, not out: report "shabbat" and do not run the cycle. esh_tamid = cond do shabbat? -> "shabbat" true -> case VavPipeline.tamid_probe() do {:ok, data} -> if Map.get(data, :roots_activated, []) != [], do: "pass", else: "fail" {:unresolved, _} -> # A result came back — the fire burns — but it did not resolve. "teiku" {:waiting, _} -> "waiting" {:transition, _} -> "transition" _ -> "fail" end end checks = %{ l1_hash: l1_check, doorpost_hash: doorpost_check, mesorah_hash: mesorah_check, cloud_readable: cloud_check, manna_present: manna_check, l3_writable: l3_check, halt_monitor_alive: halt_check, esh_tamid: esh_tamid } failed = Enum.count(checks, fn {_k, v} -> v in ["fail", "absent"] end) overall = cond do shabbat? -> "shabbat" failed > 1 -> "critical" failed == 1 -> "degraded" true -> "healthy" end %{overall: overall, checks: checks} end defp build_system_status do {:ok, cloud_state} = VavPipeline.Cloud.check() %{ cloud_state: format_cloud(cloud_state), shabbat: %{resting: VavPipeline.ShabbatTimer.resting?()}, components: %{ chatzotzrot: process_status(VavPipeline.Chatzotzrot), halt_monitor: process_status(VavPipeline.HaltMonitor), arei_miklat: process_status(VavPipeline.AreiMiklat), eglah_arufah: process_status(VavPipeline.EglahArufah), bikurim: process_status(VavPipeline.Bikurim), manna_ledger: process_status(VavPipeline.MannaLedger), shabbat_timer: process_status(VavPipeline.ShabbatTimer), l1_integrity: case VavPipeline.L1Reader.verify_integrity() do :ok -> "verified" _ -> "failed" end }, manna_provision: manna_provision_status(), refuge_components: VavPipeline.AreiMiklat.list_refugees() |> length(), halt_status: case VavPipeline.HaltMonitor.status() do :running -> "running" {:halted, event} -> "halted: #{event.condition}" end, signal_count: VavPipeline.Chatzotzrot.count() } end # ============================================================ # Helpers # ============================================================ defp check_availability do cond do VavPipeline.ShabbatTimer.resting?() -> {:shabbat, "next motzei shabbat"} VavPipeline.HaltMonitor.halted?() -> {:halted, event} = VavPipeline.HaltMonitor.status() {:halted, event} true -> :available end end defp send_json(conn, status, body) do conn |> put_resp_content_type("application/json") |> send_resp(status, Jason.encode!(body)) end # Disclosure attached to every Torah-content response. # Vayikra 19:11 — לֹא תְכַחֲשׁוּ — affirmatively state what the output is. # Devarim 17:8-11 — horaah is vested in human authorities, not in this system. defp disclosure do %{ origin: "Computational reasoning by the Bikoret system over Torah text. " <> "May include post-processed output from external AI models (Claude, Mistral) " <> "consulted as korban sources; provenance is tagged internally.", not_horaah: "This is the system's reasoning, not horaah. " <> "A human beit din may reach a different conclusion.", torah_basis: ["Vayikra 19:11 (lo techachashu)", "Devarim 17:8-11 (horaah)"] } end @doc false @impl Plug.ErrorHandler def handle_errors(conn, %{kind: kind, reason: reason, stack: stack}) do status = conn.status || 500 message = exception_message(kind, reason) Logger.error( "CHATAT_UNEXPECTED on #{conn.method} #{conn.request_path} — #{message}\n" <> Exception.format_stacktrace(stack) ) # tamid §3.8: a component failing on the serving path is HIGH — # chatzotzrot :assembly_leaders. Whereis-guarded for bare tests. if Process.whereis(VavPipeline.Chatzotzrot) do VavPipeline.Chatzotzrot.signal(:assembly_leaders, reason: "Serving-path exception on #{conn.request_path}: #{message}" ) end code = if status == 400, do: "KIYOR_MALFORMED_REQUEST", else: "CHATAT_UNEXPECTED" send_json(conn, status, %{ error: %{ code: code, type: error_type(code), message: if(status == 400, do: "The request could not be read.", else: "The system failed while serving this request. The failure is recorded; the fault is the system's, not yours." ), guidance: %{ issue: message, correction: if(status == 400, do: "Send a JSON body — {\"question\": \"...\"} — with Content-Type: application/json.", else: "Retry. If it recurs, the kohanim have been signalled and will find it in the audit log." ), resubmit: true } } }) end defp exception_message(:error, %{__exception__: true} = e), do: "#{inspect(e.__struct__)}: #{Exception.message(e)}" defp exception_message(kind, reason), do: "#{kind}: #{inspect(reason)}" # A rejected offering — {:lo_tov, reason} from ki_tov. Not a crash and # not a 200 that says nothing: the system says what it could not do. # # The empty-results case is the ger asking in a language the Shoresh # Engine cannot read roots from (kohen Day 6, defect 2). The honest # answer today is that the path serving non-Hebrew questions — Korban # Processor → external model, Architecture Step 22 — is not yet open, # and the system does not fabricate from partial capacity (tamid # §4.1). What it CAN give without inventing anything is the corner of # the field itself: the verse in scope, in Hebrew, as a source. So it # does. PRD §2.4 — no gatekeeping; this is not "come back when you # know Hebrew", it is "here is what exists, and here is what does not # yet". defp reject_response(conn, reason, scope) when is_binary(reason) do if String.starts_with?(reason, "empty results") do send_json(conn, 422, %{ error: %{ code: "TEIKU_NO_ROOTS", type: error_type("TEIKU_NO_ROOTS"), message: "The Shoresh Engine found no Hebrew root in this question, so there is nothing " <> "for the cycle to reason over. The system will not call an empty result an answer.", guidance: %{ issue: reason, correction: "Ask with Hebrew text, or name a verse (for example \"bereishit 1:1\"). " <> "Questions in other languages need the Korban Processor's external-model " <> "path (Architecture Step 22), which is not yet open. Until it is, the system " <> "says so rather than fabricating an answer.", resubmit: true, scope_verse: scope_verse(scope) } }, disclosure: disclosure() }) else send_json(conn, 422, %{ error: %{ code: "LO_TOV", type: error_type("LO_TOV"), message: "The cycle produced output that does not align with the specification.", guidance: %{ issue: reason, correction: "Rephrase, or narrow the scope to a single verse.", resubmit: true } }, disclosure: disclosure() }) end end # The verse in scope — the pe'ah corner the ger receives regardless. defp scope_verse(nil), do: nil defp scope_verse(%{book: b, chapter: c, verse: v}) do case VavPipeline.L1Reader.read_verse(b, c, v) do {:ok, verse} -> %{book: verse.book, chapter: verse.chapter, verse: verse.verse, ref: verse.ref, text: verse.text_hebrew} _ -> %{book: b, chapter: c, verse: v} end end defp scope_verse(_), do: nil defp send_error(conn, status, code, guidance) do send_json(conn, status, %{ error: %{ code: code, type: error_type(code), message: guidance.issue, guidance: guidance } }) end defp error_type("KIYOR" <> _), do: "purification_needed" defp error_type("SHABBAT"), do: "mandatory_rest" defp error_type("MANNA" <> _), do: "provision_exhausted" defp error_type("CLOUD" <> _), do: "system_transition" defp error_type("HALT" <> _), do: "cardinal_sin_detected" defp error_type("UNAUTHORIZED"), do: "authentication_required" defp error_type("FORBIDDEN"), do: "insufficient_tier" defp error_type("RATE_LIMITED"), do: "too_many_requests" defp error_type("TEIKU" <> _), do: "unresolvable" defp error_type("LO_TOV"), do: "specification_mismatch" defp error_type("CHATAT" <> _), do: "correction_needed" defp error_type(_), do: "error" # ?since=ISO8601 → DateTime, or nil for "all time". defp parse_since(nil), do: nil defp parse_since(""), do: nil defp parse_since(s) when is_binary(s) do case DateTime.from_iso8601(s) do {:ok, dt, _offset} -> DateTime.truncate(dt, :second) _ -> nil end end defp parse_since(_), do: nil defp format_since(nil), do: "all_time" defp format_since(%DateTime{} = dt), do: DateTime.to_iso8601(dt) # How the verse was chosen, said out loud in the response. A default # scope is a confession, not a grounding (tablet 30). defp scope_report(scope, :default) do %{ verse: scope_verse(scope), resolved_by: "default", note: "The question named no verse and quoted no Torah the index could find, " <> "so the default scope was used. Roots are read against it, not against " <> "the question's source." } end defp scope_report(scope, resolved_by) do %{verse: scope_verse(scope), resolved_by: to_string(resolved_by)} end # "question" — the engine read the asker's own Hebrew. # "verse" — the asker named a verse and quoted none of it, so the # roots are the verse's own (tablet 32). defp roots_read_from(data), do: data |> Map.get(:roots_read_from, :question) |> to_string() defp parse_scope(nil), do: %{book: "bereishit", chapter: 1, verse: 1} defp parse_scope(%{"book" => book, "chapter" => ch, "verse" => v}) do %{book: book, chapter: ch, verse: v} end defp parse_scope(%{"book" => book}) do %{book: book, chapter: 1, verse: 1} end defp parse_scope(_), do: %{book: "bereishit", chapter: 1, verse: 1} defp format_answer(data) when is_map(data) do Map.get(data, :interpretation, Map.get(data, :content, "Processing complete")) end defp format_answer(_), do: "Processing complete" defp format_sources(data) when is_map(data) do case Map.get(data, :sources) do sources when is_list(sources) -> sources _ -> [] end end defp format_sources(_), do: [] # tamid §1.1 shows roots_consulted as "bet-resh-aleph"; the kohen's # Day-6 proof asks for ב-ר-א. Letter-hyphenated Hebrew satisfies the # shape, and the transliteration rides alongside when the engine # supplied it. defp format_roots(roots) when is_list(roots) do Enum.map(roots, fn %{shoresh: s} when is_binary(s) -> VavPipeline.BereishitCycle.hyphenate(s) r when is_binary(r) -> r _ -> nil end) |> Enum.reject(&is_nil/1) end defp format_roots(_), do: [] defp format_cloud(:camped), do: "camped" defp format_cloud(:moving), do: "moving" defp format_cloud({:major_transition, type, _}), do: "#{type}" defp format_cloud(:cloud_not_visible), do: "not_visible" defp format_cloud(other), do: inspect(other) defp manna_provision_status do if Process.whereis(VavPipeline.MannaLedger) do ledger = VavPipeline.MannaLedger.ledger() remaining = Map.new(ledger.allocated, fn {k, allocated} -> {k, max(allocated - Map.get(ledger.consumed, k, 0), 0)} end) %{ cycle_date: ledger.cycle_date, cycle_count: ledger.cycle_count, allocated: ledger.allocated, consumed: ledger.consumed, remaining: remaining } else %{cycle_date: nil, note: "ledger not running"} end end defp process_status(module) do if Process.whereis(module), do: "healthy", else: "not_running" end defp handle_pipeline_error(conn, {:unresolved, data}) do send_json(conn, 200, %{ status: "teiku", result: %{vital_content: format_answer(data)}, note: "TEIKU — this question could not be fully resolved", disclosure: disclosure() }) end defp handle_pipeline_error(conn, {:rejected, reason}) do reject_response(conn, reason, nil) end defp handle_pipeline_error(conn, {:transition, _}) do send_error(conn, 503, "CLOUD_MOVING", %{ issue: "System in transition", correction: "Retry when system is settled", resubmit: true }) end defp handle_pipeline_error(conn, {:halted, type, reason}) do send_error(conn, 503, "HALT_#{String.upcase(to_string(type))}", %{ issue: reason, correction: "Contact system operators", resubmit: false }) end defp handle_pipeline_error(conn, _) do send_error(conn, 500, "INTERNAL", %{ issue: "Unexpected processing result", correction: "Retry or contact operators", resubmit: true }) end defp generate_id do :crypto.strong_rand_bytes(16) |> Base.encode16(case: :lower) end end