Files
bikoret/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex
T
Scott KarpandClaude Fable 5.1 19d3aaac86 bedikah: two witnesses for check 7 (bishe'arecha step 1)
Devarim 19:15 — one witness does not stand. A frame form now stands only
when it is found in two distinct sentences, or in one sentence beside a
violation from checks 1–6, or beside a sentence of another form; the
Vav Pipeline sends the halt term only then (stands_on: sentences |
other_checks | other_forms, witnesses: n). A form seen once and alone
is returned under checks.frames.noted with its sentence and is not
counted in the verdict; the output is still examined, not empty.

Tamid §1.1 contract and bishearecha.md §V updated; Doorpost re-pinned
(86a43d6b…). Elixir 558/0; Prolog bedikah 34/34 (the rule is unchanged;
its header says what now reaches it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 00:59:01 +00:00

1211 lines
42 KiB
Elixir

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: "Shorashim — 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
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, tier, 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 <api-key>' 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
# === 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 Shorashim 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(),
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, /courtyard/offering, /chatzer/sanhedrin, /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
params = conn.body_params
output = params["output"]
model = params["model"]
query = params["query"] || ""
cond do
not is_binary(output) or String.trim(output) == "" ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
issue: "Missing or empty 'output' field",
correction: "Submit JSON with 'output': the external model's output, as text",
resubmit: true
})
byte_size(output) > @bedikah_max_bytes ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
issue: "'output' is #{byte_size(output)} bytes; the examination reads at most #{@bedikah_max_bytes}",
correction: "Bring the output in parts",
resubmit: true
})
model not in @bedikah_provenances ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
issue: "Missing or unknown 'model' field: #{inspect(model)}",
correction: "Name whose output this is — one of #{Enum.join(@bedikah_provenances, ", ")}",
resubmit: true
})
match?({:error, _}, parse_sources(params["sources"])) ->
{:error, why} = parse_sources(params["sources"])
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
issue: "'sources': #{why}",
correction:
"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",
resubmit: true
})
not is_binary(query) ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{
issue: "'query' must be a string",
correction: "Give the question the model was asked, or omit it",
resubmit: true
})
true ->
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 ->
{:ok, sources} = parse_sources(params["sources"])
respond_bedikah(conn, VavPipeline.Bedikah.examine(output, model, query, sources: sources))
end
end
end
# 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,
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"] || []
},
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
# ============================================================
# 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
case_ref = generate_id()
{:ok, decision} =
VavPipeline.SanhedrinClient.submit_case(%{
case_ref: case_ref,
complexity: :simple,
question: case_text,
context: params["sides"] || %{},
requested_by: "chatzer"
})
send_json(conn, 200, %{
case_ref: decision.case_ref,
deliberation: %{
panel_size: decision.panel_size,
majority: decision.majority,
minority: decision.minority,
outcome: decision.outcome,
processing_time_ms: decision.deliberation_time_ms
},
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
disclosure: disclosure()
})
end
end
# ============================================================
# 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
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,
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 Shorashim 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