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>
347 lines
10 KiB
Elixir
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
|