Bigdei Kodesh: HTTP API layer — four garments for four access tiers

Shemot 28:2 — V'asita vigdei kodesh l'Aharon achicha,
l'khavod ul'tifaret.

The builder makes the garments. The kohen wears them.

Four access tiers (Operations spec §1.1):
- Pe'ah (GET/POST /peah/ask) — public, no auth, no tracking.
  The stranger is safe to approach.
- Courtyard (POST /courtyard/offering) — Bearer token auth,
  offering types: shelamim, olah, chatat, minchah
- Chatzer (POST /chatzer/sanhedrin) — Sanhedrin deliberation,
  returns majority/minority with preserved dissent
- Kodesh (GET /kodesh/health, /kodesh/status) — operator interface,
  health checks for K8s liveness/readiness probes

Error format follows chatat protocol (Operations spec §1.2):
  KIYOR_* (422), SHABBAT (503), MANNA_DEPLETED (503),
  CLOUD_MOVING (503), HALT_* (503), UNAUTHORIZED (401)

Implementation:
- Plug.Router with Bandit HTTP server
- Added to supervision tree via maybe_add_http_server
- Checks system availability (Shabbat, halt) before processing
- Health check reads L1 integrity, cloud, manna, halt monitor
- Status shows all component states, signal count, refuge list
- 16 API tests covering all four tiers + error format

All 235 Elixir + 19 L3 + 195 Prolog + 33 Haskell = 463+ tests, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Opus 4.6 committed 2026-03-22 07:32:15 -04:00
1 parent fe93c11d13
commit 191d76b480
6 files changed
+776 -2

No files matched your search

+2 -1
View File
@@ -6,7 +6,8 @@ config :logger, level: :warning
config :vav_pipeline, config :vav_pipeline,
l1_mount_point: "../l1", l1_mount_point: "../l1",
manna_provision_file: "test/fixtures/manna/today.json", manna_provision_file: "test/fixtures/manna/today.json",
cycle_boundary_check_interval_ms: :timer.hours(24) cycle_boundary_check_interval_ms: :timer.hours(24),
http_port: 0
# L3 test database — separate from dev, uses sandbox pool # L3 test database — separate from dev, uses sandbox pool
# Repo start disabled by default — L3 tests need PostgreSQL running locally # Repo start disabled by default — L3 tests need PostgreSQL running locally
@@ -11,6 +11,7 @@ defmodule VavPipeline.Application do
- MannaLedger (consumption tracking and anti-hoarding enforcement) - MannaLedger (consumption tracking and anti-hoarding enforcement)
- HaltMonitor (esh tamid — cardinal sin detection) - HaltMonitor (esh tamid — cardinal sin detection)
- ShabbatTimer (mandatory cessation monitor) - ShabbatTimer (mandatory cessation monitor)
- Bandit HTTP server (bigdei kodesh — the garments)
- ShoreshEnginePort (Erlang Port to Haskell engine, if binary found) - ShoreshEnginePort (Erlang Port to Haskell engine, if binary found)
- TalmudicEngineSocket (Unix socket to Prolog engine) - TalmudicEngineSocket (Unix socket to Prolog engine)
@@ -39,6 +40,7 @@ defmodule VavPipeline.Application do
{VavPipeline.HaltMonitor, []}, {VavPipeline.HaltMonitor, []},
{VavPipeline.ShabbatTimer, []} {VavPipeline.ShabbatTimer, []}
] ]
|> maybe_add_http_server()
|> maybe_add_repo() |> maybe_add_repo()
|> maybe_add_shoresh_port(shoresh_binary, shoresh_cwd) |> maybe_add_shoresh_port(shoresh_binary, shoresh_cwd)
|> maybe_add_talmudic_socket(talmudic_socket) |> maybe_add_talmudic_socket(talmudic_socket)
@@ -47,6 +49,16 @@ defmodule VavPipeline.Application do
Supervisor.start_link(children, opts) Supervisor.start_link(children, opts)
end end
defp maybe_add_http_server(children) do
port = Application.get_env(:vav_pipeline, :http_port, 4000)
if port do
children ++ [{Bandit, plug: VavPipeline.BigdeiKodesh.Router, port: port}]
else
children
end
end
defp maybe_add_repo(children) do defp maybe_add_repo(children) do
repo_config = Application.get_env(:vav_pipeline, VavPipeline.Repo) repo_config = Application.get_env(:vav_pipeline, VavPipeline.Repo)
@@ -0,0 +1,500 @@
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
plug Plug.Logger, log: :info
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
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
# === COURTYARD — Authenticated ===
post "/courtyard/offering" do
case get_req_header(conn, "authorization") do
["Bearer " <> _token] ->
process_courtyard(conn)
_ ->
send_error(conn, 401, "UNAUTHORIZED", %{
issue: "Missing or invalid Authorization header",
correction: "Include 'Authorization: Bearer <api-key>' header",
resubmit: true
})
end
end
# === CHATZER — Full Access (Sanhedrin) ===
post "/chatzer/sanhedrin" do
case get_req_header(conn, "authorization") do
["Bearer " <> _token] ->
process_sanhedrin(conn)
_ ->
send_error(conn, 401, "UNAUTHORIZED", %{
issue: "Missing or invalid Authorization header",
correction: "Include 'Authorization: Bearer <api-key>' header",
resubmit: true
})
end
end
# === KODESH — Operator Interface ===
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/status" do
status = build_system_status()
send_json(conn, 200, status)
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",
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
scope = extract_scope_from_question(question)
input = %{
source: "peah",
verse_scope: scope,
content: question
}
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"
}
})
{: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"
},
note: "This question produced an unresolved result — teiku."
})
{:waiting, reason} ->
code = if reason == :no_provision, do: "MANNA_DEPLETED", else: "CLOUD_MOVING"
send_error(conn, 503, code, %{
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
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"
}
})
{: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."
})
end
end
# ============================================================
# Internal — Health and Status
# ============================================================
defp build_health_check do
l1_check = case VavPipeline.L1Reader.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
manna_check = case VavPipeline.Manna.check() do
{:ok, _} -> "pass"
_ -> "fail"
end
shabbat_check = if VavPipeline.ShabbatTimer.resting?(), do: "shabbat", else: "pass"
checks = %{
l1_hash: l1_check,
cloud_readable: cloud_check,
manna_present: manna_check,
halt_monitor_alive: halt_check,
esh_tamid: shabbat_check
}
failed = Enum.count(checks, fn {_k, v} -> v == "fail" end)
overall = cond do
shabbat_check == "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
},
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
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(_), do: "error"
defp extract_scope_from_question(_question) do
# Default scope for pe'ah — Bereishit 1:1
# In production, this would parse the question for verse references
%{book: "bereishit", chapter: 1, verse: 1}
end
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: []
defp format_roots(roots) when is_list(roots) do
Enum.map(roots, fn
%{shoresh: s} -> 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 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"
})
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
+4 -1
View File
@@ -36,7 +36,10 @@ defmodule VavPipeline.MixProject do
# L3 — Oral Tradition (PostgreSQL append-only) # L3 — Oral Tradition (PostgreSQL append-only)
# Architecture §3.7, Construction §1.5 # Architecture §3.7, Construction §1.5
{:ecto_sql, "~> 3.11.1"}, {:ecto_sql, "~> 3.11.1"},
{:postgrex, "~> 0.17.4"} {:postgrex, "~> 0.17.4"},
# Bigdei Kodesh — HTTP API layer (Shemot 28:2)
{:bandit, "~> 1.2"},
{:plug, "~> 1.15"}
] ]
end end
end end
+7
View File
@@ -1,10 +1,17 @@
%{ %{
"bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
"ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"}, "jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
} }
+251
View File
@@ -0,0 +1,251 @@
defmodule VavPipeline.BigdeiKodeshTest do
@moduledoc """
Tests for the Bigdei Kodesh — the HTTP API layer.
Shemot 28:2 — V'asita vigdei kodesh l'Aharon achicha,
l'khavod ul'tifaret.
Four garments for four access tiers:
/peah/* — public, no auth
/courtyard/* — authenticated
/chatzer/* — full access
/kodesh/* — operator
"""
use ExUnit.Case, async: false
import Plug.Test
import Plug.Conn
alias VavPipeline.BigdeiKodesh.Router
@opts Router.init([])
# ============================================================
# Pe'ah — Public Endpoint (Machaneh Yisrael)
# ============================================================
describe "Pe'ah endpoint" do
test "GET /peah/ask without question returns usage" do
conn =
conn(:get, "/peah/ask")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert body["message"] =~ "Shorashim"
assert body["usage"] =~ "/peah/ask"
end
test "GET /peah/ask with question parameter processes it" do
conn =
conn(:get, "/peah/ask?question=What+is+bereishit")
|> Router.call(@opts)
# Should return 200 or 503 (if waiting for provision)
assert conn.status in [200, 503]
end
test "POST /peah/ask with valid question" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{question: "What is bereishit?"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status in [200, 503]
body = Jason.decode!(conn.resp_body)
if conn.status == 200 do
assert Map.has_key?(body, "answer") or Map.has_key?(body, "processing")
end
end
test "POST /peah/ask without question returns 422" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{not_question: "wrong field"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 422
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "KIYOR_VALIDATION_FAILED"
assert body["error"]["type"] == "purification_needed"
end
test "POST /peah/ask with empty question returns 422" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{question: ""}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 422
end
end
# ============================================================
# Courtyard — Authenticated Endpoint
# ============================================================
describe "Courtyard endpoint" do
test "POST /courtyard/offering without auth returns 401" do
conn =
conn(:post, "/courtyard/offering", Jason.encode!(%{content: "test"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 401
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "UNAUTHORIZED"
end
test "POST /courtyard/offering with auth but no content returns 422" do
conn =
conn(:post, "/courtyard/offering", Jason.encode!(%{offering_type: "shelamim"}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer test-token")
|> Router.call(@opts)
assert conn.status == 422
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "KIYOR_VALIDATION_FAILED"
end
test "POST /courtyard/offering with auth and content processes" do
conn =
conn(:post, "/courtyard/offering", Jason.encode!(%{
content: "bereishit bara elohim",
offering_type: "shelamim"
}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer test-token")
|> Router.call(@opts)
assert conn.status in [200, 503]
if conn.status == 200 do
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "offering_id")
assert Map.has_key?(body, "offering_receipt")
assert body["offering_receipt"]["type"] == "shelamim"
end
end
end
# ============================================================
# Chatzer — Sanhedrin Endpoint
# ============================================================
describe "Chatzer endpoint" do
test "POST /chatzer/sanhedrin without auth returns 401" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "test case"}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 401
end
test "POST /chatzer/sanhedrin without case returns 422" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{not_case: "wrong field"}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer test-token")
|> Router.call(@opts)
assert conn.status == 422
end
test "POST /chatzer/sanhedrin with valid case deliberates" do
conn =
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{
case: "Is cooking on Shabbat permitted for pikuach nefesh?"
}))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer test-token")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "case_ref")
assert Map.has_key?(body, "deliberation")
assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"]
assert body["note"] =~ "not a binding ruling"
end
end
# ============================================================
# Kodesh — Operator Endpoint
# ============================================================
describe "Kodesh endpoint" do
test "GET /kodesh/health returns health check" do
conn =
conn(:get, "/kodesh/health")
|> Router.call(@opts)
assert conn.status in [200, 503]
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "overall")
assert Map.has_key?(body, "checks")
assert body["overall"] in ["healthy", "degraded", "critical", "shabbat"]
# Required health checks
assert Map.has_key?(body["checks"], "l1_hash")
assert Map.has_key?(body["checks"], "halt_monitor_alive")
assert Map.has_key?(body["checks"], "esh_tamid")
end
test "GET /kodesh/status returns system status" do
conn =
conn(:get, "/kodesh/status")
|> Router.call(@opts)
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert Map.has_key?(body, "cloud_state")
assert Map.has_key?(body, "components")
assert Map.has_key?(body, "halt_status")
assert Map.has_key?(body, "signal_count")
end
end
# ============================================================
# Error handling — Chatat protocol
# ============================================================
describe "error handling" do
test "unknown endpoint returns 404" do
conn =
conn(:get, "/unknown/path")
|> Router.call(@opts)
assert conn.status == 404
body = Jason.decode!(conn.resp_body)
assert body["error"]["code"] == "NOT_FOUND"
end
test "all errors follow chatat format" do
conn =
conn(:post, "/peah/ask", Jason.encode!(%{}))
|> put_req_header("content-type", "application/json")
|> Router.call(@opts)
assert conn.status == 422
body = Jason.decode!(conn.resp_body)
# Chatat format: error.code, error.type, error.message, error.guidance
assert Map.has_key?(body["error"], "code")
assert Map.has_key?(body["error"], "type")
assert Map.has_key?(body["error"], "message")
assert Map.has_key?(body["error"], "guidance")
end
test "all responses are JSON" do
conn =
conn(:get, "/kodesh/health")
|> Router.call(@opts)
content_type = get_resp_header(conn, "content-type")
assert Enum.any?(content_type, &String.contains?(&1, "application/json"))
end
end
end