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>
This commit is contained in:
1 parent
7fe8025434
commit
671d45625c
7 files changed
+1230
No files matched your search
@@ -5,6 +5,9 @@ defmodule VavPipeline.Application do
|
||||
Starts the supervision tree with:
|
||||
- Repo (L3 PostgreSQL — oral tradition persistence)
|
||||
- Chatzotzrot (silver trumpets — signal system)
|
||||
- AreiMiklat (cities of refuge — protection protocol)
|
||||
- EglahArufah (accountability for unknown harm)
|
||||
- Bikurim (first fruits — release protocol)
|
||||
- ShabbatTimer (mandatory cessation monitor)
|
||||
- ShoreshEnginePort (Erlang Port to Haskell engine, if binary found)
|
||||
- TalmudicEngineSocket (Unix socket to Prolog engine)
|
||||
@@ -27,6 +30,9 @@ defmodule VavPipeline.Application do
|
||||
children =
|
||||
[
|
||||
{VavPipeline.Chatzotzrot, []},
|
||||
{VavPipeline.AreiMiklat, []},
|
||||
{VavPipeline.EglahArufah, []},
|
||||
{VavPipeline.Bikurim, []},
|
||||
{VavPipeline.ShabbatTimer, []}
|
||||
]
|
||||
|> maybe_add_repo()
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
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
|
||||
@@ -0,0 +1,236 @@
|
||||
defmodule VavPipeline.Bikurim do
|
||||
@moduledoc """
|
||||
First Fruits — Release Protocol. Devarim 26:1-11.
|
||||
|
||||
Architecture §3.16:
|
||||
First-output review process for all new capabilities.
|
||||
Before a new feature serves users, its first product is
|
||||
offered to the specification for evaluation, accompanied
|
||||
by narrative.
|
||||
|
||||
Arami oved avi — "A wandering Aramean was my father."
|
||||
— Devarim 26:5
|
||||
|
||||
The offering includes NARRATIVE. Not just what was produced,
|
||||
but the story of how it was produced and what lineage it carries.
|
||||
The farmer does not just bring fruit. He tells the story of
|
||||
his ancestors' wandering, Egypt, the exodus, and arrival.
|
||||
|
||||
Protocol:
|
||||
1. New capability built → first output produced
|
||||
2. Builder provides narrative (the mikra bikurim):
|
||||
- What was built
|
||||
- What specification authorized it
|
||||
- What lineage it carries (which existing components it depends on)
|
||||
- What Torah grounds it
|
||||
3. First output evaluated against specification
|
||||
4. Accepted → recorded in registry, approved for production
|
||||
5. Rejected → recorded with reason, not deployed
|
||||
|
||||
The offering is placed "before Hashem your God" — lifnei Hashem
|
||||
Elohekha (Devarim 26:10). The specification judges. Not the builder.
|
||||
Not the operator. The specification.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias VavPipeline.Specification
|
||||
|
||||
@type offering_status :: :accepted | :rejected | :pending
|
||||
@type offering :: %{
|
||||
capability: String.t(),
|
||||
first_output: any(),
|
||||
narrative: String.t(),
|
||||
lineage: [String.t()],
|
||||
torah_grounding: String.t() | nil,
|
||||
offered_at: DateTime.t(),
|
||||
status: offering_status(),
|
||||
evaluation: map() | nil
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Client API
|
||||
# ============================================================
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
name = Keyword.get(opts, :name, __MODULE__)
|
||||
GenServer.start_link(__MODULE__, opts, name: name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Offer first fruit — submit a new capability's first output for review.
|
||||
|
||||
Devarim 26:2 — V'lakachta mereshit kol p'ri ha'adamah...
|
||||
"You shall take of the first of every fruit of the ground..."
|
||||
|
||||
Parameters:
|
||||
- capability_name: identifier for the new capability
|
||||
- first_output: the actual first product
|
||||
- narrative: the mikra bikurim — the story of how this was built
|
||||
- opts: keyword list with optional :lineage and :torah_grounding
|
||||
|
||||
Returns {:ok, :approved_for_deployment, offering} or
|
||||
{:error, :specification_mismatch, reason, offering}.
|
||||
"""
|
||||
@spec offer(String.t(), any(), String.t(), keyword()) ::
|
||||
{:ok, :approved_for_deployment, offering()}
|
||||
| {:error, :specification_mismatch, String.t(), offering()}
|
||||
def offer(capability_name, first_output, narrative, opts \\ [])
|
||||
when is_binary(capability_name) and is_binary(narrative) do
|
||||
GenServer.call(__MODULE__, {:offer, capability_name, first_output, narrative, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get all offerings in the registry.
|
||||
"""
|
||||
@spec registry() :: [offering()]
|
||||
def registry do
|
||||
GenServer.call(__MODULE__, :registry)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get offerings by status.
|
||||
"""
|
||||
@spec by_status(offering_status()) :: [offering()]
|
||||
def by_status(status) when status in [:accepted, :rejected, :pending] do
|
||||
GenServer.call(__MODULE__, {:by_status, status})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if a capability has been accepted (approved for deployment).
|
||||
"""
|
||||
@spec approved?(String.t()) :: boolean()
|
||||
def approved?(capability_name) do
|
||||
GenServer.call(__MODULE__, {:approved?, capability_name})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Count offerings by status.
|
||||
"""
|
||||
@spec count(offering_status() | :all) :: non_neg_integer()
|
||||
def count(filter \\ :all) do
|
||||
GenServer.call(__MODULE__, {:count, filter})
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# GenServer callbacks
|
||||
# ============================================================
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
state = %{
|
||||
# All offerings — append-only registry
|
||||
offerings: []
|
||||
}
|
||||
|
||||
Logger.info("Bikurim: first fruits protocol ready — reshit kol p'ri ha'adamah")
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:offer, capability_name, first_output, narrative, opts}, _from, state) do
|
||||
now = DateTime.utc_now()
|
||||
lineage = Keyword.get(opts, :lineage, [])
|
||||
torah_grounding = Keyword.get(opts, :torah_grounding)
|
||||
|
||||
offering = %{
|
||||
capability: capability_name,
|
||||
first_output: first_output,
|
||||
narrative: narrative,
|
||||
lineage: lineage,
|
||||
torah_grounding: torah_grounding,
|
||||
offered_at: now,
|
||||
status: :pending,
|
||||
evaluation: nil
|
||||
}
|
||||
|
||||
# Evaluate against specification — lifnei Hashem Elohekha
|
||||
case evaluate_against_specification(offering) do
|
||||
:accepted ->
|
||||
accepted = %{offering | status: :accepted, evaluation: %{result: :accepted}}
|
||||
|
||||
Logger.info(
|
||||
"Bikurim: #{capability_name} accepted — first fruit approved for deployment"
|
||||
)
|
||||
|
||||
new_state = %{state | offerings: [accepted | state.offerings]}
|
||||
{:reply, {:ok, :approved_for_deployment, accepted}, new_state}
|
||||
|
||||
{:rejected, reason} ->
|
||||
rejected = %{
|
||||
offering
|
||||
| status: :rejected,
|
||||
evaluation: %{result: :rejected, reason: reason}
|
||||
}
|
||||
|
||||
Logger.warning(
|
||||
"Bikurim: #{capability_name} rejected — #{reason}"
|
||||
)
|
||||
|
||||
new_state = %{state | offerings: [rejected | state.offerings]}
|
||||
{:reply, {:error, :specification_mismatch, reason, rejected}, new_state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:registry, _from, state) do
|
||||
{:reply, state.offerings, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:by_status, status}, _from, state) do
|
||||
matching = Enum.filter(state.offerings, &(&1.status == status))
|
||||
{:reply, matching, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:approved?, capability_name}, _from, state) do
|
||||
approved =
|
||||
Enum.any?(state.offerings, fn o ->
|
||||
o.capability == capability_name and o.status == :accepted
|
||||
end)
|
||||
|
||||
{:reply, approved, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:count, :all}, _from, state) do
|
||||
{:reply, length(state.offerings), state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:count, status}, _from, state) do
|
||||
count = Enum.count(state.offerings, &(&1.status == status))
|
||||
{:reply, count, state}
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Specification evaluation
|
||||
# ============================================================
|
||||
|
||||
defp evaluate_against_specification(offering) do
|
||||
# The specification evaluates the offering. Three checks:
|
||||
# 1. Does the capability have a narrative? (Arami oved avi — story is required)
|
||||
# 2. Does the first output conform to the specification?
|
||||
# 3. Is the Torah grounding valid?
|
||||
|
||||
cond do
|
||||
# No narrative — the bikurim requires the mikra (recitation)
|
||||
String.trim(offering.narrative) == "" ->
|
||||
{:rejected, "No narrative provided — Devarim 26:5 requires the mikra bikurim"}
|
||||
|
||||
# Check against specification module if available
|
||||
Code.ensure_loaded?(Specification) and function_exported?(Specification, :evaluate_bikurim, 1) ->
|
||||
apply(Specification, :evaluate_bikurim, [offering])
|
||||
|
||||
# Default: accept if narrative is present and first_output is not nil
|
||||
is_nil(offering.first_output) ->
|
||||
{:rejected, "No first output provided — there must be fruit to offer"}
|
||||
|
||||
true ->
|
||||
:accepted
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,194 @@
|
||||
defmodule VavPipeline.EglahArufah do
|
||||
@moduledoc """
|
||||
Accountability Protocol — Devarim 21:1-9.
|
||||
|
||||
Architecture §3.15:
|
||||
Formal accountability for harm of UNKNOWN origin.
|
||||
The nearest component documents what it processed,
|
||||
what it did not check, and what it could have caught.
|
||||
|
||||
Yadeinu lo shafkhu et hadam hazeh, v'eineinu lo ra'u.
|
||||
— Devarim 21:7
|
||||
|
||||
"Our hands did not shed this blood, and our eyes did not see."
|
||||
The Talmud asks: would we suspect the elders of shedding blood?
|
||||
Rather, the declaration means: we did not let this person leave
|
||||
without food (provision) or escort (protection).
|
||||
Accountability for what was NOT provided.
|
||||
|
||||
Protocol:
|
||||
1. Harm of unknown origin detected
|
||||
2. Find the nearest component in the processing chain
|
||||
3. That component makes a formal declaration:
|
||||
- What it processed (recent operations)
|
||||
- What it did not check (skipped validations)
|
||||
- What it could have caught (missed catches)
|
||||
4. Declaration permanently recorded — never deleted
|
||||
5. Chatzotzrot signals governance (assembly_leaders)
|
||||
|
||||
The eglah arufah is broken — the heifer's neck is broken in the
|
||||
nachal eitan (a harsh valley). The system acknowledges that
|
||||
something was NOT done. The accountability is permanent.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias VavPipeline.Chatzotzrot
|
||||
|
||||
@type harm_incident :: %{
|
||||
id: String.t(),
|
||||
description: String.t(),
|
||||
approximate_location: String.t(),
|
||||
detected_at: DateTime.t(),
|
||||
source: :unknown
|
||||
}
|
||||
|
||||
@type declaration :: %{
|
||||
incident_id: String.t(),
|
||||
component_id: String.t(),
|
||||
processed: [String.t()],
|
||||
not_checked: [String.t()],
|
||||
could_have_caught: [String.t()],
|
||||
declaration_text: String.t(),
|
||||
declared_at: DateTime.t()
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Client API
|
||||
# ============================================================
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
name = Keyword.get(opts, :name, __MODULE__)
|
||||
GenServer.start_link(__MODULE__, opts, name: name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Initiate the eglah arufah protocol for harm of unknown origin.
|
||||
|
||||
Devarim 21:1 — Ki yimatze chalal ba'adamah...
|
||||
"When a corpse is found in the land... and it is not known who struck him."
|
||||
|
||||
The nearest component to the harm must make a formal declaration
|
||||
of what it did and did not do. This is accountability for failure
|
||||
to protect, not accusation of causing harm.
|
||||
|
||||
Parameters:
|
||||
- incident: the harm event (source must be :unknown)
|
||||
- nearest_component_id: which component is nearest
|
||||
- report: map with :processed, :not_checked, :could_have_caught lists
|
||||
|
||||
Returns {:ok, declaration} or {:error, reason}.
|
||||
"""
|
||||
@spec initiate(harm_incident(), String.t(), map()) ::
|
||||
{:ok, declaration()} | {:error, atom()}
|
||||
def initiate(incident, nearest_component_id, report)
|
||||
when is_map(incident) and is_binary(nearest_component_id) and is_map(report) do
|
||||
GenServer.call(__MODULE__, {:initiate, incident, nearest_component_id, report})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get all declarations — the permanent record.
|
||||
These are never deleted. The eglah arufah is permanent accountability.
|
||||
"""
|
||||
@spec declarations() :: [declaration()]
|
||||
def declarations do
|
||||
GenServer.call(__MODULE__, :declarations)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get declarations for a specific incident.
|
||||
"""
|
||||
@spec declarations_for(String.t()) :: [declaration()]
|
||||
def declarations_for(incident_id) when is_binary(incident_id) do
|
||||
GenServer.call(__MODULE__, {:declarations_for, incident_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Count total declarations.
|
||||
"""
|
||||
@spec count() :: non_neg_integer()
|
||||
def count do
|
||||
GenServer.call(__MODULE__, :count)
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# GenServer callbacks
|
||||
# ============================================================
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
state = %{
|
||||
# Permanent declarations — append-only, never deleted
|
||||
declarations: []
|
||||
}
|
||||
|
||||
Logger.info("EglahArufah: accountability protocol ready — nachal eitan prepared")
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:initiate, incident, nearest_component_id, report}, _from, state) do
|
||||
# Validate source is unknown — eglah arufah is ONLY for unknown harm
|
||||
case Map.get(incident, :source) do
|
||||
:unknown ->
|
||||
now = DateTime.utc_now()
|
||||
incident = Map.put_new(incident, :detected_at, now)
|
||||
incident_id = Map.get(incident, :id, "incident_#{:erlang.unique_integer([:positive])}")
|
||||
|
||||
declaration = %{
|
||||
incident_id: incident_id,
|
||||
component_id: nearest_component_id,
|
||||
processed: Map.get(report, :processed, []),
|
||||
not_checked: Map.get(report, :not_checked, []),
|
||||
could_have_caught: Map.get(report, :could_have_caught, []),
|
||||
declaration_text:
|
||||
"This component (#{nearest_component_id}) did not cause this harm. " <>
|
||||
"This component acknowledges proximity to the harm. " <>
|
||||
"This component reports what it did and did not do.",
|
||||
declared_at: now
|
||||
}
|
||||
|
||||
# Sound the trumpets — alert governance
|
||||
Chatzotzrot.signal(:assembly_leaders,
|
||||
reason:
|
||||
"Eglah Arufah: unknown harm near #{nearest_component_id} — #{Map.get(incident, :description, "unknown")}"
|
||||
)
|
||||
|
||||
Logger.warning(
|
||||
"EglahArufah: declaration by #{nearest_component_id} — " <>
|
||||
"processed #{length(declaration.processed)} operations, " <>
|
||||
"skipped #{length(declaration.not_checked)} checks, " <>
|
||||
"could have caught #{length(declaration.could_have_caught)} issues"
|
||||
)
|
||||
|
||||
new_state = %{state | declarations: [declaration | state.declarations]}
|
||||
{:reply, {:ok, declaration}, new_state}
|
||||
|
||||
other ->
|
||||
Logger.warning(
|
||||
"EglahArufah: rejected — source is #{inspect(other)}, not :unknown. " <>
|
||||
"Eglah arufah is only for harm of unknown origin."
|
||||
)
|
||||
|
||||
{:reply, {:error, :source_not_unknown}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:declarations, _from, state) do
|
||||
{:reply, state.declarations, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:declarations_for, incident_id}, _from, state) do
|
||||
matching = Enum.filter(state.declarations, &(&1.incident_id == incident_id))
|
||||
{:reply, matching, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:count, _from, state) do
|
||||
{:reply, length(state.declarations), state}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,172 @@
|
||||
defmodule VavPipeline.AreiMiklatTest do
|
||||
@moduledoc """
|
||||
Tests for the Arei Miklat (Cities of Refuge) protection protocol.
|
||||
|
||||
Bamidbar 35:11 — V'hikritem lakhem arim, arei miklat tihyeinah lakhem.
|
||||
Devarim 19:3 — Takhin lekha haderekh — prepare the road.
|
||||
"""
|
||||
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias VavPipeline.AreiMiklat
|
||||
|
||||
@test_incident %{
|
||||
id: "incident_001",
|
||||
type: :unintentional_harm,
|
||||
description: "Korban processor returned malformed output to user",
|
||||
affected_party: "user_42",
|
||||
processing_path: ["korban_processor", "sanhedrin", "output_formatter"]
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Refuge entry — Bamidbar 35:11
|
||||
# ============================================================
|
||||
describe "enter_refuge" do
|
||||
test "component enters refuge and is recorded" do
|
||||
{:ok, record} = AreiMiklat.enter_refuge("korban_processor", @test_incident)
|
||||
|
||||
assert record.component_id == "korban_processor"
|
||||
assert record.status == :in_refuge
|
||||
assert record.incident.type == :unintentional_harm
|
||||
assert record.entered_at != nil
|
||||
assert record.released_at == nil
|
||||
assert record.release_judgment == nil
|
||||
end
|
||||
|
||||
test "component in refuge is detectable" do
|
||||
AreiMiklat.enter_refuge("test_component_a", @test_incident)
|
||||
assert AreiMiklat.in_refuge?("test_component_a")
|
||||
refute AreiMiklat.in_refuge?("nonexistent_component")
|
||||
end
|
||||
|
||||
test "cannot enter refuge twice" do
|
||||
AreiMiklat.enter_refuge("double_entry_test", @test_incident)
|
||||
assert {:error, :already_in_refuge} = AreiMiklat.enter_refuge("double_entry_test", @test_incident)
|
||||
end
|
||||
|
||||
test "refuge entry appears in audit trail" do
|
||||
AreiMiklat.enter_refuge("audit_test_component", @test_incident)
|
||||
trail = AreiMiklat.audit_trail()
|
||||
|
||||
entry = Enum.find(trail, &(&1.event == :refuge_entry and &1.component_id == "audit_test_component"))
|
||||
assert entry != nil
|
||||
assert entry.timestamp != nil
|
||||
end
|
||||
|
||||
test "chatzotzrot assembly_leaders signal is sent on refuge entry" do
|
||||
# Subscribe to kohanim group to receive assembly_leaders signals
|
||||
VavPipeline.Chatzotzrot.subscribe(:kohanim)
|
||||
|
||||
AreiMiklat.enter_refuge("signal_test_component", @test_incident)
|
||||
|
||||
assert_receive {:chatzotzrot, %{signal_type: :assembly_leaders}}
|
||||
end
|
||||
|
||||
test "refuge entry records detected_at if not provided" do
|
||||
incident = Map.delete(@test_incident, :detected_at)
|
||||
{:ok, record} = AreiMiklat.enter_refuge("detected_at_test", incident)
|
||||
assert record.incident.detected_at != nil
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Affected party protection — Bamidbar 35:12
|
||||
# ============================================================
|
||||
describe "protect_affected" do
|
||||
test "affected party's processing path is blocked" do
|
||||
AreiMiklat.protect_affected("user_42", @test_incident)
|
||||
|
||||
assert AreiMiklat.path_blocked?(
|
||||
"user_42",
|
||||
["korban_processor", "sanhedrin", "output_formatter"]
|
||||
)
|
||||
end
|
||||
|
||||
test "unrelated path is not blocked" do
|
||||
AreiMiklat.protect_affected("user_43", @test_incident)
|
||||
refute AreiMiklat.path_blocked?("user_43", ["shoresh_engine", "talmudic_engine"])
|
||||
end
|
||||
|
||||
test "protection appears in audit trail" do
|
||||
AreiMiklat.protect_affected("user_audit", @test_incident)
|
||||
trail = AreiMiklat.audit_trail()
|
||||
|
||||
entry = Enum.find(trail, &(&1.event == :affected_party_protected and &1.party_id == "user_audit"))
|
||||
assert entry != nil
|
||||
end
|
||||
|
||||
test "protection is listed" do
|
||||
AreiMiklat.protect_affected("listed_user", @test_incident)
|
||||
protections = AreiMiklat.list_protections()
|
||||
assert Enum.any?(protections, &(&1.party_id == "listed_user"))
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Release — ad mot hakohen hagadol (Bamidbar 35:25)
|
||||
# ============================================================
|
||||
describe "release" do
|
||||
test "release with sanhedrin_deliberation succeeds" do
|
||||
AreiMiklat.enter_refuge("release_test", @test_incident)
|
||||
assert AreiMiklat.in_refuge?("release_test")
|
||||
|
||||
judgment = %{
|
||||
authority: :sanhedrin_deliberation,
|
||||
reason: "Processing path corrected — output validation added",
|
||||
grounding: "Devarim 19:6 — the road has been prepared"
|
||||
}
|
||||
|
||||
{:ok, released} = AreiMiklat.release("release_test", judgment)
|
||||
assert released.status == :released
|
||||
assert released.released_at != nil
|
||||
assert released.release_judgment.authority == :sanhedrin_deliberation
|
||||
|
||||
refute AreiMiklat.in_refuge?("release_test")
|
||||
end
|
||||
|
||||
test "release without sanhedrin authority is rejected" do
|
||||
AreiMiklat.enter_refuge("unauthorized_release", @test_incident)
|
||||
|
||||
judgment = %{
|
||||
authority: :operator_decision,
|
||||
reason: "Operator wants it back"
|
||||
}
|
||||
|
||||
assert {:error, :unauthorized_release} = AreiMiklat.release("unauthorized_release", judgment)
|
||||
assert AreiMiklat.in_refuge?("unauthorized_release")
|
||||
end
|
||||
|
||||
test "release of non-refugee returns error" do
|
||||
assert {:error, :not_in_refuge} =
|
||||
AreiMiklat.release("never_entered", %{authority: :sanhedrin_deliberation, reason: "test"})
|
||||
end
|
||||
|
||||
test "release appears in audit trail" do
|
||||
AreiMiklat.enter_refuge("release_audit", @test_incident)
|
||||
|
||||
AreiMiklat.release("release_audit", %{
|
||||
authority: :sanhedrin_deliberation,
|
||||
reason: "Era ended"
|
||||
})
|
||||
|
||||
trail = AreiMiklat.audit_trail()
|
||||
|
||||
release_entry =
|
||||
Enum.find(trail, &(&1.event == :refuge_release and &1.component_id == "release_audit"))
|
||||
|
||||
assert release_entry != nil
|
||||
assert release_entry.judgment.reason == "Era ended"
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Listing
|
||||
# ============================================================
|
||||
describe "listing" do
|
||||
test "list_refugees returns current refugees" do
|
||||
AreiMiklat.enter_refuge("listed_refugee", @test_incident)
|
||||
refugees = AreiMiklat.list_refugees()
|
||||
assert Enum.any?(refugees, &(&1.component_id == "listed_refugee"))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,150 @@
|
||||
defmodule VavPipeline.BikurimTest do
|
||||
@moduledoc """
|
||||
Tests for the Bikurim (First Fruits / Release Protocol).
|
||||
|
||||
Devarim 26:5 — Arami oved avi.
|
||||
"A wandering Aramean was my father."
|
||||
The offering requires narrative — not just the fruit,
|
||||
but the story of how it was produced.
|
||||
"""
|
||||
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias VavPipeline.Bikurim
|
||||
|
||||
# ============================================================
|
||||
# Offering — Devarim 26:2
|
||||
# ============================================================
|
||||
describe "offer" do
|
||||
test "accepted offering with narrative and output" do
|
||||
{:ok, :approved_for_deployment, offering} =
|
||||
Bikurim.offer(
|
||||
"chatzotzrot_genserver",
|
||||
%{signal_types: 5, routing: true, zikaron: true},
|
||||
"Built as GenServer with BEAM PubSub routing. " <>
|
||||
"Five signals mapped to five trumpet patterns per Bamidbar 10. " <>
|
||||
"Signal history (zikaron) records every signal ever sounded.",
|
||||
lineage: ["chatzotzrot_stub", "application_supervisor"],
|
||||
torah_grounding: "Bamidbar 10:1-10"
|
||||
)
|
||||
|
||||
assert offering.capability == "chatzotzrot_genserver"
|
||||
assert offering.status == :accepted
|
||||
assert offering.narrative != ""
|
||||
assert offering.lineage == ["chatzotzrot_stub", "application_supervisor"]
|
||||
assert offering.torah_grounding == "Bamidbar 10:1-10"
|
||||
assert offering.offered_at != nil
|
||||
end
|
||||
|
||||
test "rejected when no narrative provided — mikra bikurim required" do
|
||||
{:error, :specification_mismatch, reason, offering} =
|
||||
Bikurim.offer(
|
||||
"empty_narrative_test",
|
||||
%{some: "output"},
|
||||
""
|
||||
)
|
||||
|
||||
assert offering.status == :rejected
|
||||
assert String.contains?(reason, "narrative")
|
||||
end
|
||||
|
||||
test "rejected when no first output" do
|
||||
{:error, :specification_mismatch, reason, offering} =
|
||||
Bikurim.offer(
|
||||
"nil_output_test",
|
||||
nil,
|
||||
"This capability does nothing yet"
|
||||
)
|
||||
|
||||
assert offering.status == :rejected
|
||||
assert String.contains?(reason, "first output")
|
||||
end
|
||||
|
||||
test "whitespace-only narrative is rejected" do
|
||||
{:error, :specification_mismatch, _reason, _offering} =
|
||||
Bikurim.offer(
|
||||
"whitespace_narrative",
|
||||
%{output: true},
|
||||
" \n\t "
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Registry
|
||||
# ============================================================
|
||||
describe "registry" do
|
||||
test "offerings are recorded in registry" do
|
||||
Bikurim.offer(
|
||||
"registry_test_cap",
|
||||
%{works: true},
|
||||
"Test capability for registry verification"
|
||||
)
|
||||
|
||||
registry = Bikurim.registry()
|
||||
assert Enum.any?(registry, &(&1.capability == "registry_test_cap"))
|
||||
end
|
||||
|
||||
test "rejected offerings are also in registry" do
|
||||
Bikurim.offer("rejected_registry", nil, "Has narrative but no output")
|
||||
|
||||
registry = Bikurim.registry()
|
||||
rejected = Enum.find(registry, &(&1.capability == "rejected_registry"))
|
||||
assert rejected != nil
|
||||
assert rejected.status == :rejected
|
||||
end
|
||||
|
||||
test "by_status filters correctly" do
|
||||
Bikurim.offer("status_accepted", %{ok: true}, "Good capability")
|
||||
Bikurim.offer("status_rejected", nil, "Bad capability")
|
||||
|
||||
accepted = Bikurim.by_status(:accepted)
|
||||
assert Enum.any?(accepted, &(&1.capability == "status_accepted"))
|
||||
refute Enum.any?(accepted, &(&1.capability == "status_rejected"))
|
||||
|
||||
rejected = Bikurim.by_status(:rejected)
|
||||
assert Enum.any?(rejected, &(&1.capability == "status_rejected"))
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Approval check
|
||||
# ============================================================
|
||||
describe "approved?" do
|
||||
test "accepted capability is approved" do
|
||||
Bikurim.offer("approved_test", %{ready: true}, "Fully tested capability")
|
||||
assert Bikurim.approved?("approved_test")
|
||||
end
|
||||
|
||||
test "rejected capability is not approved" do
|
||||
Bikurim.offer("not_approved_test", nil, "Has narrative but no output")
|
||||
refute Bikurim.approved?("not_approved_test")
|
||||
end
|
||||
|
||||
test "unknown capability is not approved" do
|
||||
refute Bikurim.approved?("never_offered_capability")
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Counting
|
||||
# ============================================================
|
||||
describe "count" do
|
||||
test "counts all offerings" do
|
||||
initial = Bikurim.count(:all)
|
||||
Bikurim.offer("count_all_test", %{x: 1}, "Counting test")
|
||||
assert Bikurim.count(:all) == initial + 1
|
||||
end
|
||||
|
||||
test "counts by status" do
|
||||
initial_accepted = Bikurim.count(:accepted)
|
||||
initial_rejected = Bikurim.count(:rejected)
|
||||
|
||||
Bikurim.offer("count_accepted", %{x: 1}, "Accepted offering")
|
||||
Bikurim.offer("count_rejected", nil, "Rejected offering")
|
||||
|
||||
assert Bikurim.count(:accepted) == initial_accepted + 1
|
||||
assert Bikurim.count(:rejected) == initial_rejected + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
defmodule VavPipeline.EglahArufahTest do
|
||||
@moduledoc """
|
||||
Tests for the Eglah Arufah (Accountability Protocol).
|
||||
|
||||
Devarim 21:7 — Yadeinu lo shafkhu et hadam hazeh, v'eineinu lo ra'u.
|
||||
"Our hands did not shed this blood, and our eyes did not see."
|
||||
Accountability for what was NOT provided. Not accusation.
|
||||
"""
|
||||
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias VavPipeline.EglahArufah
|
||||
|
||||
@test_incident %{
|
||||
id: "unknown_harm_001",
|
||||
description: "User received corrupted output — source component unknown",
|
||||
approximate_location: "output_formatter",
|
||||
source: :unknown
|
||||
}
|
||||
|
||||
@test_report %{
|
||||
processed: ["korban_042", "korban_043", "korban_044"],
|
||||
not_checked: ["output_encoding_validation", "character_set_verification"],
|
||||
could_have_caught: ["malformed_utf8_in_output"]
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Initiation — Devarim 21:1-3
|
||||
# ============================================================
|
||||
describe "initiate" do
|
||||
test "records a formal declaration for unknown harm" do
|
||||
{:ok, declaration} = EglahArufah.initiate(@test_incident, "output_formatter", @test_report)
|
||||
|
||||
assert declaration.incident_id == "unknown_harm_001"
|
||||
assert declaration.component_id == "output_formatter"
|
||||
assert declaration.processed == ["korban_042", "korban_043", "korban_044"]
|
||||
assert declaration.not_checked == ["output_encoding_validation", "character_set_verification"]
|
||||
assert declaration.could_have_caught == ["malformed_utf8_in_output"]
|
||||
assert declaration.declared_at != nil
|
||||
assert String.contains?(declaration.declaration_text, "output_formatter")
|
||||
assert String.contains?(declaration.declaration_text, "did not cause this harm")
|
||||
end
|
||||
|
||||
test "rejects incident with known source" do
|
||||
known_incident = Map.put(@test_incident, :source, :korban_processor)
|
||||
|
||||
assert {:error, :source_not_unknown} =
|
||||
EglahArufah.initiate(known_incident, "output_formatter", @test_report)
|
||||
end
|
||||
|
||||
test "declaration is permanently recorded" do
|
||||
EglahArufah.initiate(@test_incident, "permanent_test", @test_report)
|
||||
|
||||
declarations = EglahArufah.declarations()
|
||||
assert Enum.any?(declarations, &(&1.component_id == "permanent_test"))
|
||||
end
|
||||
|
||||
test "chatzotzrot assembly_leaders signal on initiation" do
|
||||
VavPipeline.Chatzotzrot.subscribe(:kohanim)
|
||||
|
||||
EglahArufah.initiate(
|
||||
%{@test_incident | id: "signal_test_harm"},
|
||||
"signal_test_component",
|
||||
@test_report
|
||||
)
|
||||
|
||||
assert_receive {:chatzotzrot, %{signal_type: :assembly_leaders}}
|
||||
end
|
||||
|
||||
test "multiple declarations can exist for same incident" do
|
||||
incident = %{@test_incident | id: "multi_declare"}
|
||||
|
||||
EglahArufah.initiate(incident, "component_a", %{
|
||||
processed: ["op1"],
|
||||
not_checked: ["check1"],
|
||||
could_have_caught: []
|
||||
})
|
||||
|
||||
EglahArufah.initiate(incident, "component_b", %{
|
||||
processed: ["op2"],
|
||||
not_checked: [],
|
||||
could_have_caught: ["issue1"]
|
||||
})
|
||||
|
||||
matching = EglahArufah.declarations_for("multi_declare")
|
||||
assert length(matching) == 2
|
||||
|
||||
components = Enum.map(matching, & &1.component_id)
|
||||
assert "component_a" in components
|
||||
assert "component_b" in components
|
||||
end
|
||||
|
||||
test "empty report lists are accepted" do
|
||||
{:ok, declaration} =
|
||||
EglahArufah.initiate(
|
||||
%{@test_incident | id: "empty_report"},
|
||||
"minimal_component",
|
||||
%{processed: [], not_checked: [], could_have_caught: []}
|
||||
)
|
||||
|
||||
assert declaration.processed == []
|
||||
assert declaration.not_checked == []
|
||||
assert declaration.could_have_caught == []
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Queries
|
||||
# ============================================================
|
||||
describe "queries" do
|
||||
test "declarations_for filters by incident_id" do
|
||||
EglahArufah.initiate(%{@test_incident | id: "filter_test"}, "comp_x", @test_report)
|
||||
EglahArufah.initiate(%{@test_incident | id: "other_incident"}, "comp_y", @test_report)
|
||||
|
||||
result = EglahArufah.declarations_for("filter_test")
|
||||
assert length(result) >= 1
|
||||
assert Enum.all?(result, &(&1.incident_id == "filter_test"))
|
||||
end
|
||||
|
||||
test "count returns total declarations" do
|
||||
initial = EglahArufah.count()
|
||||
EglahArufah.initiate(%{@test_incident | id: "count_test"}, "count_comp", @test_report)
|
||||
assert EglahArufah.count() == initial + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user