bigdei_kodesh: commit 8 — GET /peah/azazel_log (public half of property 9)
Pass 4 / Finding F, commit 8 of 10 from BUILD_AZAZEL.md. The
public-half of property 9 (observable disposal). The Rambam in
Moreh Nevuchim 3:46 makes the load-bearing claim: "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.
This endpoint is those witnesses. Anyone with HTTP access to
Shorashim can see WHAT was disposed (categorically) and HOW MANY
disposals occurred — turning the disposal channel from a private
operator action into a public, witnessed fact.
PRIVACY BOUNDARY (BUILD_AZAZEL.md §6 Q6):
The endpoint exposes:
- total disposal count (optionally filtered by ?since=ISO8601)
- categorical aggregate (avonot/peshaim/chatot counts across
recent disposals — what KIND of errors were disposed)
- up to 50 recent disposals as case_ref + shaliach +
triggering_call + disposed_at + categories_filled (which slots
of vidui were filled)
The endpoint does NOT expose:
- vidui ITEMS (the operator's specific descriptions of each
disposed thing)
- the disposed PAYLOAD (which doesn't exist in L3 anyway —
property 10)
- any handle for retrieving the disposed content (there isn't one)
The detailed vidui items are operator-internal, accessible via
future /kodesh/* endpoints. The split is deliberate: public is
WITNESS, operator is AUDITOR. The witness sees the fact;
the auditor reads the contents.
The privacy boundary statement is included in every response so
callers know what they're not seeing — they can reason about the
endpoint's silence rather than guess.
GRACEFUL DEGRADATION:
When VavPipeline.Repo is not running (dev/test without Postgres),
the endpoint returns total_disposals=0 with l3_status="unavailable"
rather than crashing. Mirrors the maybe_add_repo pattern from
MEMORY.md — Shorashim should respond cleanly even when L3 is down,
because /peah/* is the public-facing tier and must be more
resilient than internal tiers.
CHANGED:
vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex
+ GET /peah/azazel_log
Optional ?since=ISO8601 filter; returns total +
categorical_aggregate + recent[<=50] + l3_status +
privacy_boundary statement + disclosure block (kichush +
not_horaah from commit 5e00f75)
+ parse_since/1 helper (graceful: bad input → all_time)
+ format_since/1 helper
vav_pipeline/test/bigdei_kodesh_test.exs
+ 8 new tests covering:
- 200 OK when no disposals recorded (empty L3)
- response carries privacy_boundary statement
- response carries disclosure block
- since=invalid → all_time
- since=valid_iso8601 → echoed
- categorical_aggregate has all 3 slots even when empty
- [@l3] aggregate counts categorically across multiple disposals
- [@l3] PRIVACY BOUNDARY: response NEVER contains vidui item
strings (load-bearing — verifies a known secret_phrase
inserted into a vidui's chatot does NOT leak into the
response body)
Tests: vav_pipeline 361 passed, 0 failures, 35 excluded (was
353/33; +6 visible response-shape tests + 2 :l3-tagged tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2932fb7447
commit
f493b7df65
2 files changed
+210
No files matched your search
@@ -57,6 +57,59 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
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
|
||||
|
||||
# === COURTYARD — Authenticated ===
|
||||
|
||||
post "/courtyard/offering" do
|
||||
@@ -428,6 +481,22 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
defp error_type("UNAUTHORIZED"), do: "authentication_required"
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
@@ -82,6 +82,147 @@ defmodule VavPipeline.BigdeiKodeshTest do
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Pe'ah — Azazel disposal log (commit 8, public half of property 9)
|
||||
# ============================================================
|
||||
# Privacy boundary tests run WITHOUT a database (the empty L3 case
|
||||
# exercises the response shape and the privacy guarantees). Tests
|
||||
# that need actual disposal records to query are tagged :l3.
|
||||
|
||||
describe "GET /peah/azazel_log — public observability" do
|
||||
test "responds with 200 even when no disposals are recorded" do
|
||||
conn =
|
||||
conn(:get, "/peah/azazel_log")
|
||||
|> Router.call(@opts)
|
||||
|
||||
assert conn.status == 200
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
assert is_integer(body["total_disposals"])
|
||||
assert is_list(body["recent"])
|
||||
assert is_map(body["categorical_aggregate"])
|
||||
end
|
||||
|
||||
test "response carries the privacy boundary statement" do
|
||||
conn =
|
||||
conn(:get, "/peah/azazel_log")
|
||||
|> Router.call(@opts)
|
||||
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
# Property 9's privacy boundary must be visible to the caller —
|
||||
# they need to know that the items they don't see are
|
||||
# operator-internal, not missing.
|
||||
assert body["privacy_boundary"] =~ "operator-internal"
|
||||
assert body["privacy_boundary"] =~ "Property 9"
|
||||
end
|
||||
|
||||
test "response carries the disclosure block (kichush + horaah)" do
|
||||
conn =
|
||||
conn(:get, "/peah/azazel_log")
|
||||
|> Router.call(@opts)
|
||||
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
# Mirrors every other Torah-content response from commit 5e00f75
|
||||
assert body["disclosure"]["origin"]
|
||||
assert body["disclosure"]["not_horaah"]
|
||||
assert body["disclosure"]["torah_basis"]
|
||||
end
|
||||
|
||||
test "since=invalid returns all_time" do
|
||||
conn =
|
||||
conn(:get, "/peah/azazel_log?since=not-a-timestamp")
|
||||
|> Router.call(@opts)
|
||||
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
assert body["since"] == "all_time"
|
||||
end
|
||||
|
||||
test "since=valid_iso8601 is echoed in response" do
|
||||
conn =
|
||||
conn(:get, "/peah/azazel_log?since=2026-04-28T00:00:00Z")
|
||||
|> Router.call(@opts)
|
||||
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
assert body["since"] == "2026-04-28T00:00:00Z"
|
||||
end
|
||||
|
||||
test "categorical_aggregate has all three slots even when empty" do
|
||||
conn =
|
||||
conn(:get, "/peah/azazel_log")
|
||||
|> Router.call(@opts)
|
||||
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
agg = body["categorical_aggregate"]
|
||||
# All three slots present (the public sees the SHAPE of the
|
||||
# vidui structure, not its contents)
|
||||
assert Map.has_key?(agg, "avonot")
|
||||
assert Map.has_key?(agg, "peshaim")
|
||||
assert Map.has_key?(agg, "chatot")
|
||||
end
|
||||
|
||||
@tag :l3
|
||||
test "with disposals: aggregate counts categories accurately" do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
|
||||
|
||||
base = %{
|
||||
case_ref: "log-001",
|
||||
vidui: %{
|
||||
"avonot" => ["halt avodah_zarah: x"],
|
||||
"peshaim" => [],
|
||||
"chatot" => [],
|
||||
"confessor" => "kohen",
|
||||
"confessed_at" => "2026-04-28T12:00:00Z"
|
||||
},
|
||||
routing: "eretz_gezerah",
|
||||
authorized_by: "kohen-gadol",
|
||||
shaliach: "ish-itti",
|
||||
disposed_at: ~U[2026-04-28 12:00:01Z]
|
||||
}
|
||||
|
||||
{:ok, _} = VavPipeline.L3.Store.record_disposal(base)
|
||||
{:ok, _} = VavPipeline.L3.Store.record_disposal(Map.put(base, :case_ref, "log-002"))
|
||||
|
||||
conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts)
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
|
||||
assert body["total_disposals"] == 2
|
||||
assert body["categorical_aggregate"]["avonot"] == 2
|
||||
end
|
||||
|
||||
@tag :l3
|
||||
test "PRIVACY BOUNDARY: response NEVER contains vidui item strings" do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
|
||||
|
||||
secret_phrase =
|
||||
"kohen-confidential-content-that-must-never-leak-to-public-endpoint"
|
||||
|
||||
attrs = %{
|
||||
case_ref: "privacy-001",
|
||||
vidui: %{
|
||||
"avonot" => [],
|
||||
"peshaim" => [],
|
||||
"chatot" => [secret_phrase],
|
||||
"confessor" => "kohen",
|
||||
"confessed_at" => "2026-04-28T12:00:00Z"
|
||||
},
|
||||
routing: "eretz_gezerah",
|
||||
authorized_by: "kohen-gadol",
|
||||
shaliach: "ish-itti",
|
||||
disposed_at: ~U[2026-04-28 12:00:01Z]
|
||||
}
|
||||
|
||||
{:ok, _} = VavPipeline.L3.Store.record_disposal(attrs)
|
||||
|
||||
conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts)
|
||||
|
||||
# The response body — as a string — must NOT contain the
|
||||
# vidui item content. This is the load-bearing privacy
|
||||
# guarantee; if the secret_phrase appears anywhere in the
|
||||
# response, the public endpoint is leaking operator-internal
|
||||
# state.
|
||||
refute conn.resp_body =~ secret_phrase
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Courtyard — Authenticated Endpoint
|
||||
# ============================================================
|
||||
|
||||
Reference in new issue
Block a user