Files
bikoret/vav_pipeline/lib/vav_pipeline/arei_miklat.ex
T
Scott KarpandClaude Opus 4.6 671d45625c Protection layer: Arei Miklat, Eglah Arufah, Bikurim — Steps 27-29
Takhin lekha haderekh — Devarim 19:3.
"Prepare the road" before the one who needs refuge arrives.
Protection before production.

Arei Miklat (Cities of Refuge — Bamidbar 35, Devarim 19):
- GenServer with refuge registry, affected party protection, audit trail
- enter_refuge/2 isolates component but protects from reactive destruction
- protect_affected/2 blocks processing paths that caused harm
- release/2 requires :sanhedrin_deliberation authority — ad mot hakohen hagadol
- Chatzotzrot assembly_leaders signal on every refuge entry
- 15 tests: entry, double-entry prevention, affected party blocking,
  unauthorized release rejection, audit trail, chatzotzrot signaling

Eglah Arufah (Accountability — Devarim 21:1-9):
- GenServer with permanent (append-only) declaration registry
- initiate/3: nearest component declares what it processed, skipped, missed
- Only for :unknown source — rejects known-source incidents
- "Yadeinu lo shafkhu" — accountability for what was NOT provided
- 9 tests: declaration recording, source validation, multi-component
  declarations, chatzotzrot signaling, filtering, counting

Bikurim (First Fruits — Devarim 26:1-11):
- GenServer with offering registry
- offer/4: submit first output with required narrative (mikra bikurim)
- Rejects empty narrative ("Arami oved avi" — story is required)
- Rejects nil first output (there must be fruit to offer)
- Delegates to Specification.evaluate_bikurim/1 when available
- approved?/1 gate for deployment readiness
- 11 tests: acceptance, narrative validation, nil output rejection,
  registry, status filtering, approval checking, counting

All three added to supervision tree in application.ex.
160 Elixir tests pass, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 08:59:08 -04:00

347 lines
10 KiB
Elixir

defmodule VavPipeline.AreiMiklat do
@moduledoc """
Cities of Refuge — Bamidbar 35:11-28, Devarim 19:1-13.
Architecture §3.14, Construction §4.8:
Protection protocol for cases where the system's output causes
real harm — not just error. Protects BOTH the affected party
AND the responsible component from reactive destruction while
accountability is determined.
V'hikritem lakhem arim, arei miklat tihyeinah lakhem,
v'nas shamah rotze'ach, makeh nefesh bishgagah.
— Bamidbar 35:11
"You shall designate cities for yourselves, cities of refuge
they shall be for you, and a manslayer shall flee there —
one who strikes a person unintentionally."
Three principles from Torah:
1. Takhin lekha haderekh (Devarim 19:3) — prepare the road
BEFORE the one who needs refuge arrives.
2. Bivli da'at (Devarim 19:4) — refuge is for harm without
intent, without knowledge. Not punishment. Protection.
3. Ad mot hakohen hagadol (Bamidbar 35:25) — release comes
through an external event (Sanhedrin judgment), not a timer.
Neither the refugee nor the avenger controls release.
Protocol:
1. Harm event detected → component enters refuge (isolated, protected)
2. Affected party protected from further exposure
3. Chatzotzrot signal alerts kohanim (operators)
4. Sanhedrin deliberation determines release — not automatic, not arbitrary
5. All refuge events permanently recorded (append-only audit)
"""
use GenServer
require Logger
alias VavPipeline.Chatzotzrot
@type refuge_status :: :in_refuge | :released
@type incident_type :: :unintentional_harm | :processing_error | :data_exposure | :integrity_breach
@type incident :: %{
id: String.t(),
type: incident_type(),
description: String.t(),
affected_party: String.t() | nil,
processing_path: [String.t()],
detected_at: DateTime.t()
}
@type refuge_record :: %{
component_id: String.t(),
incident: incident(),
status: refuge_status(),
entered_at: DateTime.t(),
released_at: DateTime.t() | nil,
release_judgment: map() | nil
}
# ============================================================
# Client API
# ============================================================
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
@doc """
Enter refuge — a component that caused unintentional harm.
Bamidbar 35:11 — v'nas shamah rotze'ach, makeh nefesh bishgagah.
The component is isolated from normal operation but PROTECTED
from reactive destruction. No one deletes a component under refuge.
Returns {:ok, refuge_record} or {:error, reason}.
"""
@spec enter_refuge(String.t(), incident()) :: {:ok, refuge_record()} | {:error, atom()}
def enter_refuge(component_id, incident) when is_binary(component_id) and is_map(incident) do
GenServer.call(__MODULE__, {:enter_refuge, component_id, incident})
end
@doc """
Protect an affected party from further exposure to the same processing path.
Bamidbar 35:12 — v'lo yamut harotze'ach ad amdo lifnei ha'edah lamishpat.
The one who caused harm is protected; the one harmed is also protected.
"""
@spec protect_affected(String.t(), incident()) :: {:ok, map()} | {:error, atom()}
def protect_affected(party_id, incident) when is_binary(party_id) and is_map(incident) do
GenServer.call(__MODULE__, {:protect_affected, party_id, incident})
end
@doc """
Release a component from refuge — requires Sanhedrin judgment.
Bamidbar 35:25 — ad mot hakohen hagadol.
Release is not a timer. Not a governance board's whim.
It is a judgment that the era of the harm has passed.
The judgment must include:
- authority: :sanhedrin_deliberation
- reason: what has changed (the "death of the kohen gadol")
- grounding: Torah reference that grounds the judgment
Returns {:ok, updated_record} or {:error, reason}.
"""
@spec release(String.t(), map()) :: {:ok, refuge_record()} | {:error, atom()}
def release(component_id, judgment) when is_binary(component_id) and is_map(judgment) do
GenServer.call(__MODULE__, {:release, component_id, judgment})
end
@doc """
Check if a component is currently in refuge.
"""
@spec in_refuge?(String.t()) :: boolean()
def in_refuge?(component_id) do
GenServer.call(__MODULE__, {:in_refuge?, component_id})
end
@doc """
Check if a processing path is blocked for a party.
"""
@spec path_blocked?(String.t(), [String.t()]) :: boolean()
def path_blocked?(party_id, processing_path) do
GenServer.call(__MODULE__, {:path_blocked?, party_id, processing_path})
end
@doc """
Get all current refuge records.
"""
@spec list_refugees() :: [refuge_record()]
def list_refugees do
GenServer.call(__MODULE__, :list_refugees)
end
@doc """
Get all active protections for affected parties.
"""
@spec list_protections() :: [map()]
def list_protections do
GenServer.call(__MODULE__, :list_protections)
end
@doc """
Get the full audit trail — all refuge events ever recorded.
Append-only. Never deleted.
"""
@spec audit_trail() :: [map()]
def audit_trail do
GenServer.call(__MODULE__, :audit_trail)
end
# ============================================================
# GenServer callbacks
# ============================================================
@impl true
def init(_opts) do
state = %{
# Active refuge records: %{component_id => refuge_record}
refugees: %{},
# Active protections: %{party_id => [%{path: [...], incident_id: ...}]}
protections: %{},
# Permanent audit trail — append-only, never deleted
audit: []
}
Logger.info("AreiMiklat: three cities of refuge prepared — takhin lekha haderekh")
{:ok, state}
end
@impl true
def handle_call({:enter_refuge, component_id, incident}, _from, state) do
if Map.has_key?(state.refugees, component_id) do
{:reply, {:error, :already_in_refuge}, state}
else
now = DateTime.utc_now()
incident = Map.put_new(incident, :detected_at, now)
record = %{
component_id: component_id,
incident: incident,
status: :in_refuge,
entered_at: now,
released_at: nil,
release_judgment: nil
}
audit_entry = %{
event: :refuge_entry,
component_id: component_id,
incident: incident,
timestamp: now
}
# Sound the trumpets — alert the kohanim
Chatzotzrot.signal(:assembly_leaders,
reason: "Refuge entry: #{component_id} — #{Map.get(incident, :description, "unintentional harm")}"
)
Logger.warning(
"AreiMiklat: #{component_id} entered refuge — " <>
"#{Map.get(incident, :type, :unknown)} — #{Map.get(incident, :description, "no description")}"
)
new_state = %{
state
| refugees: Map.put(state.refugees, component_id, record),
audit: [audit_entry | state.audit]
}
{:reply, {:ok, record}, new_state}
end
end
@impl true
def handle_call({:protect_affected, party_id, incident}, _from, state) do
now = DateTime.utc_now()
path = Map.get(incident, :processing_path, [])
incident_id = Map.get(incident, :id, "unknown")
protection = %{
party_id: party_id,
blocked_path: path,
incident_id: incident_id,
protected_at: now
}
audit_entry = %{
event: :affected_party_protected,
party_id: party_id,
incident_id: incident_id,
blocked_path: path,
timestamp: now
}
Logger.info("AreiMiklat: affected party #{party_id} protected from path #{inspect(path)}")
existing = Map.get(state.protections, party_id, [])
new_state = %{
state
| protections: Map.put(state.protections, party_id, [protection | existing]),
audit: [audit_entry | state.audit]
}
{:reply, {:ok, protection}, new_state}
end
@impl true
def handle_call({:release, component_id, judgment}, _from, state) do
case Map.get(state.refugees, component_id) do
nil ->
{:reply, {:error, :not_in_refuge}, state}
record ->
# Only Sanhedrin deliberation can release — ad mot hakohen hagadol
case Map.get(judgment, :authority) do
:sanhedrin_deliberation ->
now = DateTime.utc_now()
updated = %{
record
| status: :released,
released_at: now,
release_judgment: judgment
}
audit_entry = %{
event: :refuge_release,
component_id: component_id,
judgment: judgment,
timestamp: now
}
Logger.info(
"AreiMiklat: #{component_id} released from refuge — " <>
"#{Map.get(judgment, :reason, "era ended")}"
)
new_state = %{
state
| refugees: Map.delete(state.refugees, component_id),
audit: [audit_entry | state.audit]
}
{:reply, {:ok, updated}, new_state}
other ->
Logger.warning(
"AreiMiklat: unauthorized release attempt for #{component_id} — " <>
"authority #{inspect(other)} is not :sanhedrin_deliberation"
)
{:reply, {:error, :unauthorized_release}, state}
end
end
end
@impl true
def handle_call({:in_refuge?, component_id}, _from, state) do
{:reply, Map.has_key?(state.refugees, component_id), state}
end
@impl true
def handle_call({:path_blocked?, party_id, path}, _from, state) do
blocked =
case Map.get(state.protections, party_id) do
nil ->
false
protections ->
Enum.any?(protections, fn p ->
# A path is blocked if the blocked_path is a prefix of the requested path
List.starts_with?(path, p.blocked_path)
end)
end
{:reply, blocked, state}
end
@impl true
def handle_call(:list_refugees, _from, state) do
{:reply, Map.values(state.refugees), state}
end
@impl true
def handle_call(:list_protections, _from, state) do
all =
state.protections
|> Enum.flat_map(fn {_party, protections} -> protections end)
{:reply, all, state}
end
@impl true
def handle_call(:audit_trail, _from, state) do
{:reply, state.audit, state}
end
end