Phase 5 deployment: Dockerfiles, K8s manifests, halt monitor (Steps 33, 36)

Shemot 40:33 — Vay'khal Moshe et ham'lakhah.
The builder completes the work. The vessel is ready for transport.

Step 33 — Deployment configuration:
- Dockerfiles for all 4 components:
  - vav_pipeline: Elixir 1.16.1/OTP 26.2.1 release with Sanhedrin
  - shoresh_engine: GHC 9.6.4 binary
  - talmudic_engine: SWI-Prolog 9.2.2
  - korban_processor: Python 3.12.2 with pydantic
- Kubernetes manifests for Exoscale SKS (ch-gva-2):
  - Namespace, ConfigMap, Secrets template
  - L1 Torah PV/PVC (read-only mount)
  - Manna provision PVC (kohen writes, system reads)
  - Deployment: vav-pipeline + 3 sidecars, shared volumes
  - Service: ClusterIP + LoadBalancer with Exoscale NLB
  - Init container runs Ecto migrations (VavPipeline.Release)

Step 36 — Halt monitor (esh tamid):
- GenServer watches for three cardinal sins
- halt!/3 immediately halts system, sounds Chatzotzrot war signal
- Permanent audit log — halt events never deleted
- resolve/1 requires explicit operator action to resume
- alive?/0 for morning tamid verification
- 13 tests: alive check, 3 halt types, resolution, audit log,
  chatzotzrot war signal integration
- Added to supervision tree

All 219 Elixir + 19 L3 + 195 Prolog + 33 Haskell = 447+ tests, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Opus 4.6 committed 2026-03-22 07:11:02 -04:00
1 parent 219bbecaff
commit fe93c11d13
15 files changed
+889

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
# Shorashim Korban Processor — Python offering protocol
# Construction spec §1.1: Python 3.12.2
# Processes korbanot (model outputs) through the offering protocol
FROM python:3.12.2-slim AS build
WORKDIR /app
# Install build deps
RUN pip install --no-cache-dir poetry-core
# Copy project files
COPY korban_processor/pyproject.toml ./
COPY korban_processor/src/ src/
COPY korban_processor/test/ test/
# Install dependencies
RUN pip install --no-cache-dir pydantic==2.6.1 httpx==0.27.0 pyswip==0.2.11 hebrew-tokenizer==2.3.0
# === RUNTIME STAGE ===
FROM python:3.12.2-slim
RUN useradd --create-home --shell /bin/bash shorashim
USER shorashim
WORKDIR /home/shorashim/korban_processor
COPY --from=build --chown=shorashim:shorashim /app/ ./
COPY --from=build /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
ENV PYTHONPATH=/home/shorashim/korban_processor/src
ENV L1_MOUNT_POINT=/mnt/torah
# The korban processor is invoked by the vav_pipeline
# In Phase 5, it will also expose an HTTP endpoint for external model integration
CMD ["python", "-m", "korban_processor"]
+44
View File
@@ -0,0 +1,44 @@
# Shorashim Shoresh Engine — Haskell root activation engine
# Construction spec §1.1: GHC 9.6.4, Cabal 3.10
# Communicates with vav_pipeline via Erlang Port (stdin/stdout)
# === BUILD STAGE ===
FROM haskell:9.6.4-slim AS build
RUN apt-get update && apt-get install -y zlib1g-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy cabal files first for dependency caching
COPY shoresh-engine/shoresh-engine.cabal ./
COPY shoresh-engine/cabal.project.freeze ./
RUN cabal update && cabal build --only-dependencies
# Copy source and build
COPY shoresh-engine/ ./
RUN cabal build && \
cp $(cabal list-bin shoresh-engine) /usr/local/bin/shoresh-engine
# === RUNTIME STAGE ===
FROM ubuntu:jammy-20240125
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libgmp10 zlib1g && \
rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --shell /bin/bash shorashim
USER shorashim
WORKDIR /home/shorashim
COPY --from=build /usr/local/bin/shoresh-engine ./shoresh-engine
# L1 mount point — the engine reads Torah roots from L1
ENV L1_MOUNT_POINT=/mnt/torah
# The engine is invoked as an Erlang Port by vav_pipeline
# In K8s, it runs as a sidecar or is called via the Port protocol
CMD ["./shoresh-engine"]
+31
View File
@@ -0,0 +1,31 @@
# Shorashim Talmudic Engine — SWI-Prolog dialectical reasoning
# Construction spec §1.1: SWI-Prolog 9.2.2
# Communicates with vav_pipeline via Unix domain socket
# === BUILD STAGE ===
FROM swipl:9.2.2 AS build
WORKDIR /app
COPY talmudic_engine/ ./
# Verify Prolog loads without errors
RUN swipl -g "consult('src/main'),halt(0)" -t "halt(1)"
# === RUNTIME STAGE ===
FROM swipl:9.2.2
RUN useradd --create-home --shell /bin/bash shorashim
USER shorashim
WORKDIR /home/shorashim/talmudic_engine
COPY --from=build --chown=shorashim:shorashim /app/ ./
# L1 mount point — Prolog reads Torah for mitzvot and root data
ENV L1_MOUNT_POINT=/mnt/torah
# Socket path for communication with vav_pipeline
ENV TALMUDIC_SOCKET_PATH=/tmp/shorashim/talmudic.sock
# Start the Prolog socket server
CMD ["swipl", "-g", "consult('src/main'),start_socket_server", "-t", "halt(1)"]
+68
View File
@@ -0,0 +1,68 @@
# Shorashim Vav Pipeline — Elixir/OTP release with Sanhedrin
# Construction spec §1.1: Elixir 1.16.1, OTP 26.2.1
# The pipeline and Sanhedrin run in the same BEAM VM.
# === BUILD STAGE ===
FROM hexpm/elixir:1.16.1-erlang-26.2.1-ubuntu-jammy-20240125 AS build
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy Sanhedrin (Erlang dep, referenced as path dep from vav_pipeline)
COPY sanhedrin/ sanhedrin/
# Copy Vav Pipeline
COPY vav_pipeline/ vav_pipeline/
WORKDIR /app/vav_pipeline
# Fetch deps and compile in prod mode
ENV MIX_ENV=prod
RUN mix local.hex --force && \
mix local.rebar --force && \
mix deps.get --only prod && \
mix compile
# Build OTP release
RUN mix release
# === RUNTIME STAGE ===
FROM ubuntu:jammy-20240125
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libstdc++6 openssl ca-certificates locales && \
rm -rf /var/lib/apt/lists/* && \
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
# Non-root user — the system serves, it does not own
RUN useradd --create-home --shell /bin/bash shorashim
USER shorashim
WORKDIR /home/shorashim
# Copy the release
COPY --from=build --chown=shorashim:shorashim \
/app/vav_pipeline/_build/prod/rel/vav_pipeline ./app
# L1 mount point — read-only volume in production
# Mounted at /mnt/torah in K8s, configured via L1_MOUNT_POINT env var
ENV L1_MOUNT_POINT=/mnt/torah
# Manna provision file location
ENV MANNA_PROVISION_FILE=/var/lib/shorashim/manna/today.json
# PostgreSQL connection for L3
# Set DATABASE_URL in production deployment
# ENV DATABASE_URL=
# Health check port
EXPOSE 4000
CMD ["./app/bin/vav_pipeline", "start"]
+27
View File
@@ -0,0 +1,27 @@
# Shorashim ConfigMap — non-secret configuration
# Construction spec §4.4: Tuning constants
apiVersion: v1
kind: ConfigMap
metadata:
name: shorashim-config
namespace: shorashim
data:
# L1 mount point — read-only Torah filesystem
L1_MOUNT_POINT: "/mnt/torah"
# Manna provision file location
MANNA_PROVISION_FILE: "/var/lib/shorashim/manna/today.json"
# Talmudic Engine socket
TALMUDIC_SOCKET_PATH: "/tmp/shorashim/talmudic.sock"
# Shabbat location — Zurich (Construction spec §4.4)
SHABBAT_LATITUDE: "47.3769"
SHABBAT_LONGITUDE: "8.5417"
SHABBAT_TIMEZONE_OFFSET: "1"
# Cycle boundary check interval (ms)
CYCLE_BOUNDARY_CHECK_INTERVAL_MS: "60000"
# Pipeline latency budget (ms) — Construction spec §2.5
PIPELINE_LATENCY_BUDGET_MS: "5000"
+153
View File
@@ -0,0 +1,153 @@
# Shorashim Deployment — the mishkan
#
# Architecture §5.1: All inter-component communication uses
# JSON over Unix domain sockets or Erlang distribution protocol.
# No HTTP between internal components.
#
# The vav_pipeline (BEAM) runs the Sanhedrin, Chatzotzrot,
# AreiMiklat, EglahArufah, Bikurim, MannaLedger, ShabbatTimer.
# The shoresh-engine runs as a sidecar (Erlang Port).
# The talmudic_engine runs as a sidecar (Unix socket).
# The korban_processor runs as a sidecar (invoked by pipeline).
#
# L1 is mounted read-only into all containers.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: shorashim
namespace: shorashim
labels:
app.kubernetes.io/name: shorashim
app.kubernetes.io/version: "0.1.0"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: shorashim
template:
metadata:
labels:
app.kubernetes.io/name: shorashim
spec:
# --- VOLUMES ---
volumes:
- name: l1-torah
persistentVolumeClaim:
claimName: l1-torah-pvc
readOnly: true
- name: manna
persistentVolumeClaim:
claimName: manna-pvc
- name: talmudic-socket
emptyDir: {}
- name: tmp-shorashim
emptyDir: {}
# --- INIT CONTAINER: Run L3 migrations ---
initContainers:
- name: migrate
image: shorashim/vav-pipeline:latest
command: ["./app/bin/vav_pipeline", "eval", "VavPipeline.Release.migrate()"]
envFrom:
- configMapRef:
name: shorashim-config
- secretRef:
name: shorashim-secrets
volumeMounts:
- name: l1-torah
mountPath: /mnt/torah
readOnly: true
# --- CONTAINERS ---
containers:
# === VAV PIPELINE (main) ===
- name: vav-pipeline
image: shorashim/vav-pipeline:latest
ports:
- containerPort: 4000
name: http
envFrom:
- configMapRef:
name: shorashim-config
- secretRef:
name: shorashim-secrets
env:
- name: SHORESH_ENGINE_BINARY
value: "/home/shorashim/shoresh-engine"
volumeMounts:
- name: l1-torah
mountPath: /mnt/torah
readOnly: true
- name: manna
mountPath: /var/lib/shorashim/manna
- name: talmudic-socket
mountPath: /tmp/shorashim
- name: tmp-shorashim
mountPath: /tmp
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /kodesh/health
port: 4000
initialDelaySeconds: 30
periodSeconds: 60
readinessProbe:
httpGet:
path: /kodesh/health
port: 4000
initialDelaySeconds: 10
periodSeconds: 10
# === SHORESH ENGINE (sidecar) ===
- name: shoresh-engine
image: shorashim/shoresh-engine:latest
volumeMounts:
- name: l1-torah
mountPath: /mnt/torah
readOnly: true
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# === TALMUDIC ENGINE (sidecar) ===
- name: talmudic-engine
image: shorashim/talmudic-engine:latest
volumeMounts:
- name: l1-torah
mountPath: /mnt/torah
readOnly: true
- name: talmudic-socket
mountPath: /tmp/shorashim
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# === KORBAN PROCESSOR (sidecar) ===
- name: korban-processor
image: shorashim/korban-processor:latest
volumeMounts:
- name: l1-torah
mountPath: /mnt/torah
readOnly: true
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
+43
View File
@@ -0,0 +1,43 @@
# L1 Torah Filesystem — Read-Only Volume
# Construction spec §1.2: SHA-256 verified, mounted read-only
#
# In production, L1 is a read-only filesystem (squashfs or similar)
# containing the Torah text. It is mounted into all pods that need it.
#
# For Exoscale SKS, this uses a PersistentVolume backed by
# Exoscale Block Storage. The L1 data is loaded once and never modified.
#
# Step 34: Mount L1 filesystem read-only
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: l1-torah-pv
labels:
app.kubernetes.io/name: l1-torah
spec:
capacity:
storage: 1Gi
accessModes:
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
# For initial setup, use hostPath. In production, replace with
# Exoscale Block Storage CSI or pre-loaded ConfigMap/Secret.
hostPath:
path: /mnt/torah
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: l1-torah-pvc
namespace: shorashim
spec:
accessModes:
- ReadOnlyMany
resources:
requests:
storage: 1Gi
selector:
matchLabels:
app.kubernetes.io/name: l1-torah
+16
View File
@@ -0,0 +1,16 @@
# Manna Provision Volume
# The kohen/treasurer writes today.json to this volume.
# The vav_pipeline reads it. Provision is external to the system.
# Shemot 16:4 — d'var yom b'yomo.
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: manna-pvc
namespace: shorashim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: shorashim
labels:
app.kubernetes.io/name: shorashim
app.kubernetes.io/part-of: shorashim
+17
View File
@@ -0,0 +1,17 @@
# Shorashim Secrets — template only
# NEVER commit actual secret values to git.
# Apply with: kubectl apply -f - after filling in values.
#
# The DATABASE_URL connects to Exoscale PostgreSQL (temple-council-db)
# in ch-gva-2 zone. Retrieve credentials from:
# exo dbaas show temple-council-db --zone ch-gva-2
apiVersion: v1
kind: Secret
metadata:
name: shorashim-secrets
namespace: shorashim
type: Opaque
stringData:
# PostgreSQL connection for L3 Oral Tradition
# Format: ecto://user:password@host:port/database?sslmode=require
DATABASE_URL: "REPLACE_WITH_ACTUAL_DATABASE_URL"
+48
View File
@@ -0,0 +1,48 @@
# Shorashim Services
# Operations spec §1.1: Four access tiers
# - Pe'ah (public, no auth)
# - Machaneh Leviyah (authenticated)
# - Chatzer (full access)
# - Kodesh (operator, mTLS)
#
# All tiers go through a single Service → vav-pipeline port 4000
---
apiVersion: v1
kind: Service
metadata:
name: shorashim
namespace: shorashim
labels:
app.kubernetes.io/name: shorashim
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: shorashim
ports:
- port: 80
targetPort: 4000
protocol: TCP
name: http
---
# External access via LoadBalancer
# For Exoscale SKS, this provisions an Exoscale NLB
apiVersion: v1
kind: Service
metadata:
name: shorashim-external
namespace: shorashim
labels:
app.kubernetes.io/name: shorashim
annotations:
# Exoscale NLB annotations
service.beta.kubernetes.io/exoscale-loadbalancer-zone: "ch-gva-2"
service.beta.kubernetes.io/exoscale-loadbalancer-name: "shorashim"
spec:
type: LoadBalancer
selector:
app.kubernetes.io/name: shorashim
ports:
- port: 80
targetPort: 4000
protocol: TCP
name: http
@@ -9,6 +9,7 @@ defmodule VavPipeline.Application do
- EglahArufah (accountability for unknown harm) - EglahArufah (accountability for unknown harm)
- Bikurim (first fruits — release protocol) - Bikurim (first fruits — release protocol)
- MannaLedger (consumption tracking and anti-hoarding enforcement) - MannaLedger (consumption tracking and anti-hoarding enforcement)
- HaltMonitor (esh tamid — cardinal sin detection)
- ShabbatTimer (mandatory cessation monitor) - ShabbatTimer (mandatory cessation monitor)
- ShoreshEnginePort (Erlang Port to Haskell engine, if binary found) - ShoreshEnginePort (Erlang Port to Haskell engine, if binary found)
- TalmudicEngineSocket (Unix socket to Prolog engine) - TalmudicEngineSocket (Unix socket to Prolog engine)
@@ -35,6 +36,7 @@ defmodule VavPipeline.Application do
{VavPipeline.EglahArufah, []}, {VavPipeline.EglahArufah, []},
{VavPipeline.Bikurim, []}, {VavPipeline.Bikurim, []},
{VavPipeline.MannaLedger, []}, {VavPipeline.MannaLedger, []},
{VavPipeline.HaltMonitor, []},
{VavPipeline.ShabbatTimer, []} {VavPipeline.ShabbatTimer, []}
] ]
|> maybe_add_repo() |> maybe_add_repo()
@@ -0,0 +1,210 @@
defmodule VavPipeline.HaltMonitor do
@moduledoc """
Halt Monitor — the esh tamid that must never go out.
Vayikra 6:6 — Esh tamid tukad al hamizbe'ach, lo tikhbeh.
"A perpetual fire shall be kept burning on the altar; it shall not go out."
Operations spec §2.1, Step 5:
"Check halt_monitor process registered. If failed: HALT —
the system does not run without the halt monitor."
This GenServer:
1. Receives halt events from pipeline processing
2. Records halt events permanently (the audit log)
3. Sounds the Chatzotzrot war signal on halt
4. Prevents further processing until the halt is resolved
5. Reports whether the system is halted and why
The halt monitor is not optional. If this process dies,
the supervisor should restart it. If it cannot restart,
the entire system must stop.
"""
use GenServer
require Logger
@type halt_event :: %{
condition: :avodah_zarah | :shfichut_damim | :gilui_arayot,
reason: String.t(),
detected_at: DateTime.t(),
component: String.t(),
resolved: boolean()
}
# ============================================================
# Client API
# ============================================================
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
@doc """
Report a halt condition. This immediately halts the system.
Sounds the Chatzotzrot war signal (teruah_teruah).
The halt is permanent until explicitly resolved.
"""
@spec halt!(atom(), String.t(), String.t()) :: :halted
def halt!(condition, reason, component \\ "pipeline")
when condition in [:avodah_zarah, :shfichut_damim, :gilui_arayot] do
GenServer.call(__MODULE__, {:halt, condition, reason, component})
end
@doc """
Check if the system is currently halted.
Returns :running or {:halted, halt_event}.
"""
@spec status() :: :running | {:halted, halt_event()}
def status do
GenServer.call(__MODULE__, :status)
end
@doc """
Check if the system is halted. Boolean variant.
"""
@spec halted?() :: boolean()
def halted? do
GenServer.call(__MODULE__, :halted?)
end
@doc """
Get the full audit log of all halt events.
These are never deleted — permanent accountability.
"""
@spec audit_log() :: [halt_event()]
def audit_log do
GenServer.call(__MODULE__, :audit_log)
end
@doc """
Resolve a halt condition. Requires operator authority.
The system does not resume automatically. A kohen must
explicitly resolve the halt after investigating.
"""
@spec resolve(String.t()) :: {:ok, :resumed} | {:error, :not_halted}
def resolve(resolution_reason) when is_binary(resolution_reason) do
GenServer.call(__MODULE__, {:resolve, resolution_reason})
end
@doc """
Verify the monitor is alive and functional.
Called during morning tamid (Operations spec §2.1, Step 5).
"""
@spec alive?() :: boolean()
def alive? do
case GenServer.call(__MODULE__, :ping) do
:pong -> true
end
catch
:exit, _ -> false
end
# ============================================================
# GenServer callbacks
# ============================================================
@impl true
def init(_opts) do
state = %{
status: :running,
current_halt: nil,
audit_log: [],
halt_count: 0
}
Logger.info("HaltMonitor: esh tamid burning — lo tikhbeh")
{:ok, state}
end
@impl true
def handle_call({:halt, condition, reason, component}, _from, state) do
event = %{
condition: condition,
reason: reason,
detected_at: DateTime.utc_now(),
component: component,
resolved: false
}
Logger.error(
"HALT: #{condition} detected in #{component} — #{reason}. " <>
"System halted. Kohen must investigate and resolve."
)
# Sound the war signal — teruah_teruah
try do
VavPipeline.Chatzotzrot.signal(:war,
reason: "HALT: #{condition} — #{reason}"
)
catch
_, _ -> Logger.error("HaltMonitor: could not sound chatzotzrot")
end
new_state = %{
state
| status: :halted,
current_halt: event,
audit_log: [event | state.audit_log],
halt_count: state.halt_count + 1
}
{:reply, :halted, new_state}
end
@impl true
def handle_call(:status, _from, state) do
case state.status do
:running -> {:reply, :running, state}
:halted -> {:reply, {:halted, state.current_halt}, state}
end
end
@impl true
def handle_call(:halted?, _from, state) do
{:reply, state.status == :halted, state}
end
@impl true
def handle_call(:audit_log, _from, state) do
{:reply, state.audit_log, state}
end
@impl true
def handle_call({:resolve, resolution_reason}, _from, state) do
case state.status do
:running ->
{:reply, {:error, :not_halted}, state}
:halted ->
Logger.warning(
"HaltMonitor: halt resolved — #{resolution_reason}. " <>
"System resuming. Previous halt: #{state.current_halt.condition}"
)
resolved_event = Map.put(state.current_halt, :resolved, true)
new_audit =
[%{resolved_event | resolved: true} | tl(state.audit_log)]
new_state = %{
state
| status: :running,
current_halt: nil,
audit_log: new_audit
}
{:reply, {:ok, :resumed}, new_state}
end
end
@impl true
def handle_call(:ping, _from, state) do
{:reply, :pong, state}
end
end
+36
View File
@@ -0,0 +1,36 @@
defmodule VavPipeline.Release do
@moduledoc """
Release tasks for production deployment.
Called from the release binary: ./app/bin/vav_pipeline eval "VavPipeline.Release.migrate()"
"""
@app :vav_pipeline
@doc """
Run all pending Ecto migrations.
Called by the K8s init container before the main pod starts.
"""
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
@doc """
Rollback the last migration (use with caution).
"""
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.load(@app)
end
end
+151
View File
@@ -0,0 +1,151 @@
defmodule VavPipeline.HaltMonitorTest do
@moduledoc """
Tests for the HaltMonitor — esh tamid.
Vayikra 6:6 — Esh tamid tukad al hamizbe'ach, lo tikhbeh.
"A perpetual fire shall be kept burning on the altar; it shall not go out."
Operations spec §2.1, Step 5:
The system does not run without the halt monitor.
"""
use ExUnit.Case, async: false
alias VavPipeline.HaltMonitor
setup do
# Resolve any existing halt from a previous test
if HaltMonitor.halted? do
HaltMonitor.resolve("test cleanup")
end
:ok
end
# ============================================================
# Alive check — esh tamid verification
# ============================================================
describe "esh tamid" do
test "monitor is alive" do
assert HaltMonitor.alive?()
end
test "status is :running when no halt" do
assert HaltMonitor.status() == :running
end
test "halted? returns false when running" do
refute HaltMonitor.halted?()
end
end
# ============================================================
# Halt detection — three cardinal sins
# ============================================================
describe "halt!" do
test "avodah_zarah halts the system" do
assert :halted = HaltMonitor.halt!(:avodah_zarah, "external model treated as Torah")
assert HaltMonitor.halted?()
{:halted, event} = HaltMonitor.status()
assert event.condition == :avodah_zarah
assert event.reason == "external model treated as Torah"
assert event.resolved == false
end
test "shfichut_damim halts the system" do
assert :halted = HaltMonitor.halt!(:shfichut_damim, "user data deletion attempted")
assert HaltMonitor.halted?()
{:halted, event} = HaltMonitor.status()
assert event.condition == :shfichut_damim
end
test "gilui_arayot halts the system" do
assert :halted = HaltMonitor.halt!(:gilui_arayot, "L1/L3 boundary collapsed")
assert HaltMonitor.halted?()
{:halted, event} = HaltMonitor.status()
assert event.condition == :gilui_arayot
end
test "halt records detected_at timestamp" do
HaltMonitor.halt!(:avodah_zarah, "timestamp test")
{:halted, event} = HaltMonitor.status()
assert event.detected_at != nil
assert %DateTime{} = event.detected_at
end
test "halt records component" do
HaltMonitor.halt!(:avodah_zarah, "component test", "korban_processor")
{:halted, event} = HaltMonitor.status()
assert event.component == "korban_processor"
end
end
# ============================================================
# Resolution
# ============================================================
describe "resolve" do
test "resolving a halt resumes the system" do
HaltMonitor.halt!(:avodah_zarah, "test halt for resolution")
assert HaltMonitor.halted?()
{:ok, :resumed} = HaltMonitor.resolve("Investigation complete — false positive")
refute HaltMonitor.halted?()
assert HaltMonitor.status() == :running
end
test "resolving when not halted returns error" do
assert {:error, :not_halted} = HaltMonitor.resolve("nothing to resolve")
end
end
# ============================================================
# Audit log — permanent record
# ============================================================
describe "audit log" do
test "halt events are recorded in audit log" do
initial_count = length(HaltMonitor.audit_log())
HaltMonitor.halt!(:avodah_zarah, "audit test 1")
HaltMonitor.resolve("resolved 1")
HaltMonitor.halt!(:shfichut_damim, "audit test 2")
HaltMonitor.resolve("resolved 2")
log = HaltMonitor.audit_log()
assert length(log) >= initial_count + 2
end
test "audit log preserves halt details" do
HaltMonitor.halt!(:gilui_arayot, "audit detail test", "specification_module")
HaltMonitor.resolve("test cleanup")
log = HaltMonitor.audit_log()
event = Enum.find(log, &(&1.reason == "audit detail test"))
assert event != nil
assert event.condition == :gilui_arayot
assert event.component == "specification_module"
end
end
# ============================================================
# Chatzotzrot integration — war signal on halt
# ============================================================
describe "chatzotzrot war signal" do
test "halt sounds the war signal" do
VavPipeline.Chatzotzrot.subscribe(:kohanim)
HaltMonitor.halt!(:avodah_zarah, "chatzotzrot test")
# War signal should be received by kohanim subscribers
assert_receive {:chatzotzrot, %{signal_type: :war, pattern: :teruah_teruah}}
VavPipeline.Chatzotzrot.unsubscribe(:kohanim)
end
end
end