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:
Scott KarpandClaude Opus 4.7 committed 2026-04-28 09:22:40 -04:00
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
+141
View File
@@ -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
# ============================================================