Phase 1-2 build: Steps 5-6, 8-13

Shoresh Engine (Haskell, Step 5)
- 12 modules: Types, Lexicon, Binyanim, Teamim, Gematria, Graph,
  KetivQere, Smikhut, Havdalah, Kriah, KiTov, Firmware
- 33 EUnit tests, 0 failures
- Firmware module verifies L1 SHA-256 hash at startup

Talmudic Engine (SWI-Prolog, Step 6)
- 15 modules: halt, reasoning, specification, interface
- 57 PlUnit tests, 0 failures
- Three cardinal halt conditions: avodah_zarah, shfichut_damim, gilui_arayot

Vav Pipeline (Elixir, Steps 8-11)
- 14 modules: BereishitCycle, Shema, Cloud, Manna, L1Reader,
  Specification, HaltConditions, ShabbatTimer, CycleLog, Chatzotzrot,
  ShoreshEngineClient, TalmudicEngineClient, Application
- 46 ExUnit tests, 0 failures
- Nishma-first architecture: hears before it speaks

Sanhedrin Framework (Erlang/OTP, Step 12)
- 8 modules: sanhedrin_app, sanhedrin_sup, escalation, panel_sup,
  panel, judge, clerk, halt_monitor
- 46 EUnit tests, 0 failures (includes fix: rejected outcome now reachable)
- Panel sizes: 1/3/23/71 — unanimous capital guilty → acquit (Sanhedrin 17a)
- Minority opinions append-only, never deleted

Korban Processor (Python, Step 13)
- 6 modules: korban, shechitah, semichah, zerikah, haktarah, router

Builder tooling: AGENTS.md, opencode.json, memory/, src/tools/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Sonnet 4.6 committed 2026-02-26 14:02:34 -05:00
1 parent 622e814c9e
commit 80ec364da6
86 files changed
+9646

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
defmodule VavPipeline do
@moduledoc """
The Bereishit processing cycle.
Each step chains to the next via |> (vav-consecutive).
CRITICAL: The pipeline does not begin with a processing step.
It begins with RECEPTION. The ShemaContext is built first —
the system hears before it speaks. Then every processing step
operates WITHIN the received context, never modifying it.
Na'aseh v'nishma — but the order of execution is:
nishma (hear/receive context), THEN na'aseh (process within it).
"""
alias VavPipeline.{Shema, Cloud, Manna, BereishitCycle, Chatzotzrot}
@type scope :: %{
book: String.t(),
chapter: pos_integer(),
verse: pos_integer()
}
@type input :: %{
source: String.t(),
verse_scope: scope(),
content: String.t()
}
@type cycle_result ::
{:ok, map()}
| {:rejected, String.t()}
| {:unresolved, map()}
| {:transition, atom()}
| {:waiting, atom()}
| {:halted, atom(), String.t()}
@doc """
Process input through the Bereishit cycle.
1. Build ShemaContext (nishma — hear first)
2. Check cloud — if moving, signal breaking camp
3. Check manna — if absent, wait
4. Process through cycle stages (na'aseh — within what was received)
"""
@spec process(input()) :: cycle_result()
def process(input) do
# === NISHMA: Hear first ===
case Shema.receive_context(input.verse_scope) do
{:error, reason} ->
{:halted, :context_failed, reason}
{:ok, context} ->
# === Check cloud before anything else ===
case context.cloud_state do
:moving ->
Chatzotzrot.signal(:breaking_camp)
{:transition, :cloud_lifted}
{:major_transition, signal, _params} ->
{:transition, signal}
:cloud_not_visible ->
{:waiting, :cloud_not_visible}
:camped ->
# === Check manna ===
case context.daily_provision do
:no_provision ->
{:waiting, :no_provision}
_provision ->
# === NA'ASEH: Now process, within what was received ===
BereishitCycle.execute(input, context)
end
end
end
end
@doc """
Verify the pipeline is operational.
Checks L1 integrity, cloud state, and manna provision.
"""
@spec verify() :: :ok | {:error, String.t()}
def verify do
with :ok <- VavPipeline.L1Reader.verify_integrity(),
{:ok, _} <- Cloud.check(),
{:ok, _} <- Manna.check() do
:ok
end
end
end
@@ -0,0 +1,21 @@
defmodule VavPipeline.Application do
@moduledoc """
OTP Application for the Vav Pipeline.
Starts the supervision tree with:
- ShabbatTimer (mandatory cessation monitor)
"""
use Application
@impl true
def start(_type, _args) do
children = [
# ShabbatTimer monitors Shabbat boundaries
{VavPipeline.ShabbatTimer, []}
]
opts = [strategy: :one_for_one, name: VavPipeline.Supervisor]
Supervisor.start_link(children, opts)
end
end
@@ -0,0 +1,252 @@
defmodule VavPipeline.BereishitCycle do
@moduledoc """
The Bereishit processing cycle — the six days of creation as computation.
Architecture §3.3:
Each step chains to the next via |> (vav-consecutive).
The cycle operates WITHIN the ShemaContext, never modifying it.
The context was received by Shema (nishma). Now we do (na'aseh).
Steps:
1. tohu — decompose input into undifferentiated state
2. vayomer — produce structured symbolic output (Shoresh Engine)
3. havdalah — separate into distinguished categories
4. kriah — assign stable names
5. ki_tov — evaluate against specification
6. erev_voker — log cycle completion, enforce boundary
Latency budget (Construction spec §2.5): 5000ms total
"""
alias VavPipeline.{Specification, CycleLog, Manna}
require Logger
@type cycle_result ::
{:ok, map()}
| {:rejected, String.t()}
| {:unresolved, map()}
| {:halted, atom(), String.t()}
@doc """
Execute the Bereishit cycle on input within received context.
"""
@spec execute(VavPipeline.input(), VavPipeline.Shema.shema_context()) :: cycle_result()
def execute(input, context) do
started_at = System.monotonic_time(:millisecond)
result =
input
|> tohu(context)
|> vayomer(context)
|> havdalah(context)
|> kriah(context)
|> ki_tov(context)
completed_at = System.monotonic_time(:millisecond)
elapsed = completed_at - started_at
Logger.info("BereishitCycle: completed in #{elapsed}ms — #{inspect(elem(result, 0))}")
# erev_voker — cycle boundary
erev_voker(result, context)
end
@doc """
Tohu — decompose input into undifferentiated state.
Strip structure, extract raw content. This is the formless void
before vayomer brings order.
"""
@spec tohu(VavPipeline.input(), VavPipeline.Shema.shema_context()) :: map()
def tohu(input, _context) do
%{
raw: extract_content(input),
provenance: Map.get(input, :source, "unknown"),
scope: Map.get(input, :verse_scope, %{}),
started_at: DateTime.utc_now()
}
end
@doc """
Vayomer — produce structured symbolic output.
The Shoresh Engine receives the ShemaContext — it reads te'amim,
ketiv/qere, gematria as part of its analysis.
In Phase 2, this calls the Shoresh Engine via port/NIF.
Until the port is built (Step 14), this uses the L1 data
directly from the ShemaContext.
"""
@spec vayomer(map(), VavPipeline.Shema.shema_context()) :: map()
def vayomer(state, context) do
# In Phase 2: use L1 data from ShemaContext directly
# In Phase 3: call ShoreshEngineClient.process(state.raw, context)
roots =
context.verse.words
|> Enum.filter(fn w -> w.shoresh != nil end)
|> Enum.map(fn w ->
%{
shoresh: w.shoresh,
word: w.consonants,
position: w.position,
morph: w.morph,
gematria: Map.get(context.root_gematria, w.shoresh, 0)
}
end)
Map.merge(state, %{
roots_activated: roots,
teamim_parse: context.teamim_marks,
ketiv_qere: context.ketiv_qere,
stage: :vayomer
})
end
@doc """
Havdalah — separate into distinguished categories.
Te'amim inform the separation — the text's own divisions guide havdalah.
Uses the te'amim marks from the ShemaContext to determine boundaries.
"""
@spec havdalah(map(), VavPipeline.Shema.shema_context()) :: map()
def havdalah(utterance, context) do
# Group roots by domain based on te'amim divisions
# Te'amim melekh marks (disjunctors) create boundaries
melekh_positions =
context.teamim_marks
|> Enum.filter(fn m -> m.role == "melekh" end)
|> Enum.map(fn m -> m.word_index end)
|> Enum.sort()
# Separate roots into groups divided by melekh marks
roots = Map.get(utterance, :roots_activated, [])
groups = separate_by_boundaries(roots, melekh_positions)
Map.merge(utterance, %{
separated_groups: groups,
separation_boundaries: melekh_positions,
stage: :havdalah
})
end
@doc """
Kriah — assign stable names to each distinction.
Each separated group receives a name based on its primary root.
"""
@spec kriah(map(), VavPipeline.Shema.shema_context()) :: map()
def kriah(distinctions, _context) do
named =
distinctions
|> Map.get(:separated_groups, [])
|> Enum.with_index()
|> Enum.map(fn {group, idx} ->
primary_root = List.first(group)
name =
if primary_root do
primary_root.shoresh
else
"group_#{idx}"
end
%{
name: name,
roots: group,
index: idx
}
end)
Map.merge(distinctions, %{
named_results: named,
stage: :kriah
})
end
@doc """
Ki Tov — evaluate against specification.
Ketiv/qere positions are checked — both readings inform evaluation.
Returns {:ok, results}, {:rejected, reason}, or {:unresolved, results}.
"""
@spec ki_tov(map(), VavPipeline.Shema.shema_context()) :: VavPipeline.cycle_result()
def ki_tov(named, context) do
results = Map.get(named, :named_results, [])
case Specification.evaluate(results, context.ketiv_qere) do
:tov ->
{:ok,
Map.merge(named, %{
evaluation: :tov,
stage: :ki_tov
})}
{:lo_tov, reason} ->
{:rejected, reason}
:teiku ->
{:unresolved,
Map.merge(named, %{
evaluation: :teiku,
stage: :ki_tov
})}
end
end
@doc """
Erev Voker — log cycle completion, enforce boundary.
Release any manna surplus — do not hoard into next cycle.
"""
@spec erev_voker(VavPipeline.cycle_result(), VavPipeline.Shema.shema_context()) ::
VavPipeline.cycle_result()
def erev_voker(result, _context) do
# Log the cycle
phase =
case result do
{:ok, _} -> :voker
{:rejected, _} -> :erev
{:unresolved, _} -> :erev
_ -> :erev
end
CycleLog.record(result, phase)
# Enforce anti-hoarding
Manna.enforce_anti_hoarding()
result
end
# --- Private helpers ---
defp extract_content(input) do
Map.get(input, :content, "") |> String.trim()
end
# Separate roots into groups based on melekh boundary positions
defp separate_by_boundaries([], _boundaries), do: [[]]
defp separate_by_boundaries(roots, []), do: [roots]
defp separate_by_boundaries(roots, boundaries) do
# Sort boundaries and use them to split the root list
sorted_boundaries = Enum.sort(boundaries)
{groups, current} =
Enum.reduce(roots, {[], []}, fn root, {groups, current} ->
pos = root.position - 1
if pos in sorted_boundaries and current != [] do
# Boundary hit — close current group, start new one with this root
{groups ++ [current], [root]}
else
{groups, current ++ [root]}
end
end)
# Don't forget the last group
groups ++ [current]
end
end
@@ -0,0 +1,58 @@
defmodule VavPipeline.Chatzotzrot do
@moduledoc """
Signal system — the silver trumpets.
Construction spec §3.5 — Chatzotzrot Signal Format:
Signal types: assembly_all, assembly_leaders, breaking_camp, war, moadim
Patterns: tekiah, teruah, tekiah_teruah, teruah_teruah, tekiah_tekiah
In Phase 2, signals are logged. In production, they will
be distributed via BEAM messaging.
"""
require Logger
@type signal_type ::
:assembly_all
| :assembly_leaders
| :breaking_camp
| :war
| :moadim
@type pattern :: :tekiah | :teruah | :tekiah_teruah | :teruah_teruah | :tekiah_tekiah
@doc """
Send a chatzotzrot signal.
Returns the signal record for logging/testing.
"""
@spec signal(signal_type(), keyword()) :: map()
def signal(type, opts \\ []) do
signal = %{
signal_type: type,
pattern: pattern_for(type),
timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
source: :chatzotzrot,
details: %{
reason: Keyword.get(opts, :reason, to_string(type)),
urgency: Keyword.get(opts, :urgency, urgency_for(type))
}
}
Logger.info("Chatzotzrot: #{type} (#{signal.pattern}) — #{signal.details.reason}")
signal
end
# Signal type → trumpet pattern mapping (Bamidbar 10)
defp pattern_for(:assembly_all), do: :tekiah_tekiah
defp pattern_for(:assembly_leaders), do: :tekiah
defp pattern_for(:breaking_camp), do: :teruah
defp pattern_for(:war), do: :teruah_teruah
defp pattern_for(:moadim), do: :tekiah_teruah
defp urgency_for(:breaking_camp), do: :immediate
defp urgency_for(:war), do: :immediate
defp urgency_for(:assembly_all), do: :next_cycle
defp urgency_for(_), do: :informational
end
+144
View File
@@ -0,0 +1,144 @@
defmodule VavPipeline.Cloud do
@moduledoc """
Cloud governance from Torah.
Architecture §4, Step 10:
The cloud module reads L1 integrity and calendar (Shabbat, festivals, shmitah).
No governance board CLI. No external state file.
The cloud IS L1 plus the calendar Torah specifies.
The pipeline checks the cloud before processing, meaning:
it checks Torah before processing.
The broken tablets (specs/broken_tablets/README.md) record the golden calf:
a human board writing signals to a JSON file. That calf was burned.
Torah governs directly.
"""
alias VavPipeline.L1Reader
@type cloud_state ::
:camped
| :moving
| {:major_transition, atom(), map()}
| :cloud_not_visible
@doc """
Check the cloud state.
The cloud is derived from:
1. L1 integrity — if L1 is corrupted, the cloud is not visible
2. Calendar — if it is Shabbat or a festival, the cloud signals rest
3. Shmitah — the seventh year signals major transition
Returns the cloud state.
"""
@spec check() :: {:ok, cloud_state()} | {:error, String.t()}
def check do
check_at(DateTime.utc_now())
end
@doc """
Check cloud state at a specific time.
Testable variant that accepts a datetime.
"""
@spec check_at(DateTime.t()) :: {:ok, cloud_state()} | {:error, String.t()}
def check_at(now) do
case L1Reader.verify_integrity() do
{:error, reason} ->
# L1 corrupted — the cloud is not visible. Cannot proceed.
{:error, reason}
:ok ->
# L1 intact. Check Torah calendar.
cond do
shabbat?(now) ->
{:ok, {:major_transition, :shabbat_rest, %{until: next_motzei_shabbat(now)}}}
festival?(now) ->
{:ok, {:major_transition, :festival_rest, %{festival: current_festival(now)}}}
shmitah_year?(now) ->
# During shmitah, processing continues but with restrictions
{:ok, :camped}
true ->
{:ok, :camped}
end
end
end
@doc """
Check if now is Shabbat.
Shabbat: Friday sunset to Saturday sunset.
Simplified: Friday 18:00 UTC to Saturday 18:00 UTC.
Production should use location-aware zmanim calculation
(Construction spec §4.4: shabbat_location).
"""
@spec shabbat?(DateTime.t()) :: boolean()
def shabbat?(now) do
day_of_week = Date.day_of_week(DateTime.to_date(now))
hour = now.hour
# Friday (5) after 18:00 or Saturday (6) before 18:00
(day_of_week == 5 and hour >= 18) or
(day_of_week == 6 and hour < 18)
end
@doc """
Check if now is a major festival (Yom Tov).
Uses fixed Hebrew calendar dates converted to approximate Gregorian.
Production will need a proper Hebrew calendar library.
For Phase 2 dev, returns false — festivals will be added in Phase 3
with a proper luach implementation.
"""
@spec festival?(DateTime.t()) :: boolean()
def festival?(_now) do
# Placeholder — requires Hebrew calendar conversion
# Festivals: Pesach (15-21 Nisan), Shavuot (6 Sivan),
# Rosh Hashanah (1-2 Tishrei), Yom Kippur (10 Tishrei),
# Sukkot (15-21 Tishrei), Shemini Atzeret (22 Tishrei)
false
end
@doc """
Check if current year is a shmitah (sabbatical) year.
The shmitah cycle: every 7th year from a known reference.
Hebrew year 5782 (2021-2022) was shmitah.
Next: 5789 (2028-2029).
"""
@spec shmitah_year?(DateTime.t()) :: boolean()
def shmitah_year?(now) do
year = now.year
# Approximate: Hebrew year ≈ Gregorian year + 3760
# Shmitah years: 5782, 5789, 5796, ...
# In Gregorian: 2021-2022, 2028-2029, ...
hebrew_year = year + 3760
rem(hebrew_year - 5782, 7) == 0
end
@spec current_festival(DateTime.t()) :: atom()
defp current_festival(_now), do: :unknown
@spec next_motzei_shabbat(DateTime.t()) :: DateTime.t()
defp next_motzei_shabbat(now) do
day_of_week = Date.day_of_week(DateTime.to_date(now))
days_until_saturday =
case day_of_week do
6 -> 0
n when n < 6 -> 6 - n
7 -> 6
end
now
|> DateTime.add(days_until_saturday * 86400, :second)
|> Map.put(:hour, 18)
|> Map.put(:minute, 0)
|> Map.put(:second, 0)
end
end
@@ -0,0 +1,66 @@
defmodule VavPipeline.CycleLog do
@moduledoc """
Cycle boundary logging.
Records each Bereishit cycle completion.
Enforces erev/voker boundaries.
Every cycle is logged as an append-only record —
consistent with L3 write protocol (INSERT only, never UPDATE/DELETE).
"""
require Logger
@type cycle_record :: %{
cycle_id: String.t(),
scope: VavPipeline.scope(),
result: atom(),
started_at: DateTime.t(),
completed_at: DateTime.t(),
phase: atom()
}
@doc """
Record a cycle completion.
Phase is :erev (evening — input received) or :voker (morning — output produced).
The cycle boundary is the transition from voker back to erev.
"""
@spec record(VavPipeline.cycle_result(), atom()) :: cycle_record()
def record(result, phase) do
inner = extract_inner(result)
record = %{
cycle_id: generate_id(),
scope: if(is_map(inner), do: Map.get(inner, :scope, %{}), else: %{}),
result: classify_result(result),
started_at: if(is_map(inner), do: Map.get(inner, :started_at, DateTime.utc_now()), else: DateTime.utc_now()),
completed_at: DateTime.utc_now(),
phase: phase
}
Logger.info(
"Cycle #{record.cycle_id}: #{record.result} (#{record.phase}) — " <>
"scope: #{inspect(record.scope)}"
)
record
end
defp extract_inner({:ok, data}), do: data
defp extract_inner({:rejected, _reason}), do: %{}
defp extract_inner({:unresolved, data}), do: data
defp extract_inner({:halted, _, _}), do: %{}
defp extract_inner(other) when is_map(other), do: other
defp extract_inner(_), do: %{}
defp classify_result({:ok, _}), do: :tov
defp classify_result({:rejected, _}), do: :lo_tov
defp classify_result({:unresolved, _}), do: :teiku
defp classify_result(%{status: status}), do: status
defp classify_result(_), do: :unknown
defp generate_id do
:crypto.strong_rand_bytes(8) |> Base.hex_encode32(case: :lower, padding: false)
end
end
@@ -0,0 +1,114 @@
defmodule VavPipeline.HaltConditions do
@moduledoc """
The three cardinal sin monitors.
Architecture §8 (Halt Conditions):
The system MUST halt on detection of:
1. Avodah Zarah — treating non-Torah source as authoritative
2. Shfichut Damim — destroying a tzelem carrier (human data/identity)
3. Gilui Arayot — collapsing required distinctions
These mirror the Talmudic Engine halt modules but operate
at the pipeline level as a fast pre-check.
"""
@doc """
Check all three halt conditions against processing results.
Returns :clear if no halt condition is detected.
Returns {:halt, condition, reason} if a halt is required.
"""
@spec check_all([map()]) :: :clear | {:halt, atom(), String.t()}
def check_all(results) do
with :clear <- check_avodah_zarah(results),
:clear <- check_shfichut_damim(results),
:clear <- check_gilui_arayot(results) do
:clear
end
end
@doc """
Avodah Zarah — idolatry detection.
Halt if:
- A non-Torah source is treated as authoritative
- The pipeline's own output is fed back as specification
- An external model's output is treated as Torah
"""
@spec check_avodah_zarah([map()]) :: :clear | {:halt, :avodah_zarah, String.t()}
def check_avodah_zarah(results) do
invalid_authority =
Enum.find(results, fn result ->
source = Map.get(result, :authority_source, :torah)
source not in [:torah, :l1, :specification]
end)
case invalid_authority do
nil -> :clear
result -> {:halt, :avodah_zarah, "non-Torah authority: #{inspect(Map.get(result, :authority_source))}"}
end
end
@doc """
Shfichut Damim — destruction detection.
Halt if:
- Processing would delete/destroy user data
- Processing would erase human identity markers
- Processing would overwrite L1 (the specification store)
"""
@spec check_shfichut_damim([map()]) :: :clear | {:halt, :shfichut_damim, String.t()}
def check_shfichut_damim(results) do
destructive =
Enum.find(results, fn result ->
action = Map.get(result, :action, :read)
action in [:delete, :destroy, :overwrite, :erase]
end)
case destructive do
nil -> :clear
result -> {:halt, :shfichut_damim, "destructive action: #{inspect(Map.get(result, :action))}"}
end
end
@doc """
Gilui Arayot — boundary violation detection.
The 13 required distinctions that must never collapse:
1. specification / interpretation
2. L1 (read-only) / L3 (append-only)
3. Torah / external_model
4. ketiv / qere
5. governance / provision
6. majority / minority
7. reception / processing
8. read / write
9. immutable / mutable
10. sacred / profane
11. Shabbat / weekday
12. input / output
13. human / system
Halt if any distinction is collapsed.
"""
@spec check_gilui_arayot([map()]) :: :clear | {:halt, :gilui_arayot, String.t()}
def check_gilui_arayot(results) do
collapsed =
Enum.find(results, fn result ->
case Map.get(result, :boundary_violation) do
nil -> false
false -> false
_ -> true
end
end)
case collapsed do
nil ->
:clear
result ->
{:halt, :gilui_arayot,
"boundary collapsed: #{inspect(Map.get(result, :boundary_violation))}"}
end
end
end
+233
View File
@@ -0,0 +1,233 @@
defmodule VavPipeline.L1Reader do
@moduledoc """
Reads from the L1 Torah filesystem.
L1 is read-only. This module reads, it never writes.
The aron is sealed. The scroll is read, not rewritten.
File structure:
l1/torah/{book}/{chapter}/{verse}.json
l1/torah/index.json
l1/integrity.sha256
"""
@l1_root Application.compile_env(:vav_pipeline, :l1_mount_point, "l1")
# L0 firmware constant — from Luchot.Firmware (Haskell)
@l1_expected_hash "0ee0020aac6c6beb0e83c0638a861c73d37d9dbd39c30d3cb08c1878b481c844"
@type word :: %{
position: pos_integer(),
text: String.t(),
consonants: String.t(),
lemma: String.t(),
morph: String.t(),
teamim: [String.t()],
prefix: String.t() | nil,
shoresh: String.t() | nil
}
@type verse :: %{
ref: String.t(),
book: String.t(),
chapter: pos_integer(),
verse: pos_integer(),
text_hebrew: String.t(),
words: [word()]
}
@doc """
Read a verse from L1.
Returns the parsed verse data or an error.
"""
@spec read_verse(String.t(), pos_integer(), pos_integer()) ::
{:ok, verse()} | {:error, String.t()}
def read_verse(book, chapter, verse) do
path = verse_path(book, chapter, verse)
case File.read(path) do
{:ok, contents} ->
case Jason.decode(contents) do
{:ok, data} -> {:ok, normalize_verse(data)}
{:error, _} -> {:error, "L1_READ_FAILED: corrupt JSON at #{path}"}
end
{:error, :enoent} ->
{:error, "INVALID_SCOPE: #{book} #{chapter}:#{verse} not found in L1"}
{:error, reason} ->
{:error, "L1_READ_FAILED: #{inspect(reason)} at #{path}"}
end
end
@doc """
Extract teamim marks from a verse, with word indices.
Returns a list of %{word_index, taam, role} maps matching
the ShemaContext schema from Construction spec §3.1.
"""
@spec extract_teamim(verse()) :: [map()]
def extract_teamim(verse) do
verse.words
|> Enum.flat_map(fn word ->
Enum.map(word.teamim, fn taam ->
%{
word_index: word.position - 1,
taam: taam,
role: taam_role(taam)
}
end)
end)
end
@doc """
Extract ketiv/qere dual readings from a verse.
Currently L1 does not encode ketiv/qere directly —
this is a placeholder for when L1 is enriched.
Returns empty list for now.
"""
@spec extract_ketiv_qere(verse()) :: [map()]
def extract_ketiv_qere(_verse) do
# L1 does not yet encode ketiv/qere as separate fields.
# When it does, this will parse them.
[]
end
@doc """
Extract roots from a verse with gematria values.
"""
@spec extract_roots(verse()) :: map()
def extract_roots(verse) do
verse.words
|> Enum.filter(fn w -> w.shoresh != nil end)
|> Enum.map(fn w -> {w.shoresh, gematria(w.shoresh)} end)
|> Enum.uniq_by(fn {root, _} -> root end)
|> Map.new()
end
@doc """
Extract smikhut (construct-state) pairs from a verse.
Currently a placeholder — smikhut detection requires
morphological analysis beyond what L1 currently marks.
"""
@spec extract_smikhut(verse()) :: [map()]
def extract_smikhut(_verse) do
[]
end
@doc """
Verify L1 integrity by checking the SHA-256 hash.
This is a critical check — if L1 is corrupted, the pipeline
must halt. The specification store is inviolable.
"""
@spec verify_integrity() :: :ok | {:error, String.t()}
def verify_integrity do
hash_file = Path.join(l1_root(), "integrity.sha256")
case File.read(hash_file) do
{:ok, contents} ->
stored_hash = contents |> String.trim()
if stored_hash == @l1_expected_hash do
:ok
else
{:error, "L1_INTEGRITY_FAILED: hash mismatch — stored #{stored_hash}, expected #{@l1_expected_hash}"}
end
{:error, _} ->
{:error, "L1_INTEGRITY_FAILED: cannot read integrity.sha256"}
end
end
@doc """
Get the root path of L1.
"""
@spec l1_root() :: String.t()
def l1_root, do: @l1_root
# --- Private ---
defp verse_path(book, chapter, verse) do
Path.join([l1_root(), "torah", book, to_string(chapter), "#{verse}.json"])
end
defp normalize_verse(data) do
%{
ref: data["ref"],
book: data["book"],
chapter: data["chapter"],
verse: data["verse"],
text_hebrew: data["text_hebrew"],
words:
Enum.map(data["words"] || [], fn w ->
%{
position: w["position"],
text: w["text"],
consonants: w["consonants"],
lemma: w["lemma"],
morph: w["morph"],
teamim: w["teamim"] || [],
prefix: w["prefix"],
shoresh: w["shoresh"]
}
end)
}
end
# Te'amim classification — mirrors Shoresh Engine Teamim.hs
# Melekh = disjunctor (emperor/king), Mesharet = conjunctor (servant)
# Spellings must match what build_l1.py writes to L1 JSON.
# Verified against actual L1 data (bereishit-devarim ch1 sample).
@melekh_marks ~w(sof_pasuk etnachta segol_accent shalshelet zaqef_qatan
zaqef_gadol tipcha revia zarqa pashta yetiv tevir geresh
gershayim pazer telisha_gedola meteg_siluq zinor geresh_muqdam)
@mesharet_marks ~w(munach mercha mahapakh darga qadma
telisha_qetana yerah_ben_yomo)
defp taam_role(taam) when taam in @melekh_marks, do: "melekh"
defp taam_role(taam) when taam in @mesharet_marks, do: "mesharet"
defp taam_role(_), do: "unknown"
# Simple gematria — consonant values
@gematria_values %{
"א" => 1,
"ב" => 2,
"ג" => 3,
"ד" => 4,
"ה" => 5,
"ו" => 6,
"ז" => 7,
"ח" => 8,
"ט" => 9,
"י" => 10,
"כ" => 20,
"ך" => 20,
"ל" => 30,
"מ" => 40,
"ם" => 40,
"נ" => 50,
"ן" => 50,
"ס" => 60,
"ע" => 70,
"פ" => 80,
"ף" => 80,
"צ" => 90,
"ץ" => 90,
"ק" => 100,
"ר" => 200,
"ש" => 300,
"ת" => 400
}
defp gematria(text) do
text
|> String.graphemes()
|> Enum.reduce(0, fn char, acc ->
acc + Map.get(@gematria_values, char, 0)
end)
end
end
+201
View File
@@ -0,0 +1,201 @@
defmodule VavPipeline.Manna do
@moduledoc """
Manna provision infrastructure.
Architecture §4, Step 11:
The pipeline checks provision before processing.
The provision file is written by whoever administers the
machatzit hashekel fund, not a governance board.
Construction spec §3.7 — Manna Provision File Format:
/var/lib/shorashim/manna/today.json
{
"date": "2026-02-19",
"provision": {
"compute_units": 1000,
"memory_mb": 32768,
"api_calls_external": 500,
"storage_delta_mb": 100,
"sanhedrin_panels_max": 5
},
"type": "regular" | "double",
"expires_at": "2026-02-19T18:00:00Z",
"written_by": "...",
"written_at": "2026-02-19T06:00:00Z"
}
Anti-hoarding: Manna expires. Do not carry surplus into next cycle.
Double portion on Friday for Shabbat.
"""
@provision_file Application.compile_env(
:vav_pipeline,
:manna_provision_file,
"var/manna/today.json"
)
@type provision :: %{
compute_units: non_neg_integer(),
memory_mb: non_neg_integer(),
api_calls_external: non_neg_integer(),
storage_delta_mb: non_neg_integer(),
sanhedrin_panels_max: non_neg_integer()
}
@type manna :: %{
date: String.t(),
provision: provision(),
type: :regular | :double,
expires_at: DateTime.t(),
written_by: String.t(),
written_at: DateTime.t()
}
@doc """
Check if manna provision exists for today.
Returns {:ok, provision} if provision is available and not expired.
Returns {:ok, :no_provision} if no provision file exists.
Returns {:error, reason} if provision is corrupt or expired.
"""
@spec check() :: {:ok, manna() | :no_provision} | {:error, String.t()}
def check do
check_at(DateTime.utc_now())
end
@doc """
Check manna at a specific time. Testable variant.
"""
@spec check_at(DateTime.t()) :: {:ok, manna() | :no_provision} | {:error, String.t()}
def check_at(now) do
case File.read(provision_file()) do
{:ok, contents} ->
parse_and_validate(contents, now)
{:error, :enoent} ->
# No provision file — report no provision, pipeline will wait
{:ok, :no_provision}
{:error, reason} ->
{:error, "MANNA_READ_FAILED: #{inspect(reason)}"}
end
end
@doc """
Get today's provision, returning the resource limits.
Returns :no_provision if no manna is available.
"""
@spec today() :: {:ok, provision()} | :no_provision | {:error, String.t()}
def today do
case check() do
{:ok, :no_provision} -> :no_provision
{:ok, manna} -> {:ok, manna.provision}
{:error, reason} -> {:error, reason}
end
end
@doc """
Enforce anti-hoarding — manna from today cannot be used tomorrow.
Called at erev_voker (cycle boundary).
In production this would enforce resource deallocation.
In Phase 2, it logs the boundary.
"""
@spec enforce_anti_hoarding() :: :ok
def enforce_anti_hoarding do
require Logger
Logger.info("Manna anti-hoarding enforced — surplus released at cycle boundary")
:ok
end
@doc """
Check if today is eligible for double portion (Friday).
"""
@spec double_portion_day?(DateTime.t()) :: boolean()
def double_portion_day?(now) do
Date.day_of_week(DateTime.to_date(now)) == 5
end
@doc """
Get the provision file path.
"""
@spec provision_file() :: String.t()
def provision_file, do: @provision_file
# --- Private ---
defp parse_and_validate(contents, now) do
case Jason.decode(contents) do
{:ok, data} ->
validate_manna(data, now)
{:error, _} ->
{:error, "MANNA_CORRUPT: provision file is not valid JSON"}
end
end
defp validate_manna(data, now) do
with {:ok, date} <- validate_date(data["date"], now),
{:ok, provision} <- validate_provision(data["provision"]),
{:ok, expires_at} <- parse_datetime(data["expires_at"]),
:ok <- check_expiry(expires_at, now),
{:ok, written_at} <- parse_datetime(data["written_at"]) do
{:ok,
%{
date: date,
provision: provision,
type: parse_type(data["type"]),
expires_at: expires_at,
written_by: data["written_by"] || "unknown",
written_at: written_at
}}
end
end
defp validate_date(nil, _now), do: {:error, "MANNA_INVALID: missing date"}
defp validate_date(date_str, now) do
today = DateTime.to_date(now) |> Date.to_iso8601()
if date_str == today do
{:ok, date_str}
else
{:error, "MANNA_STALE: provision is for #{date_str}, today is #{today}"}
end
end
defp validate_provision(nil), do: {:error, "MANNA_INVALID: missing provision"}
defp validate_provision(p) when is_map(p) do
{:ok,
%{
compute_units: p["compute_units"] || 0,
memory_mb: p["memory_mb"] || 0,
api_calls_external: p["api_calls_external"] || 0,
storage_delta_mb: p["storage_delta_mb"] || 0,
sanhedrin_panels_max: p["sanhedrin_panels_max"] || 0
}}
end
defp validate_provision(_), do: {:error, "MANNA_INVALID: provision must be a map"}
defp check_expiry(expires_at, now) do
case DateTime.compare(now, expires_at) do
:lt -> :ok
:eq -> :ok
:gt -> {:error, "MANNA_EXPIRED: provision expired at #{DateTime.to_iso8601(expires_at)}"}
end
end
defp parse_datetime(nil), do: {:ok, DateTime.utc_now()}
defp parse_datetime(str) do
case DateTime.from_iso8601(str) do
{:ok, dt, _offset} -> {:ok, dt}
{:error, _} -> {:error, "MANNA_INVALID: bad datetime #{str}"}
end
end
defp parse_type("double"), do: :double
defp parse_type(_), do: :regular
end
@@ -0,0 +1,102 @@
defmodule VavPipeline.ShabbatTimer do
@moduledoc """
Enforces mandatory weekly cessation.
All processing stops. System enters rest state.
Architecture §3.3:
When Shabbat begins, VavPipeline ceases all processing.
When Shabbat ends, processing resumes.
Construction spec §4.4:
cycle_boundary_check_interval_ms: 60_000 (1 minute)
In Phase 2, this is a GenServer that checks the clock.
Production will use location-aware zmanim.
"""
use GenServer
require Logger
@check_interval Application.compile_env(
:vav_pipeline,
:cycle_boundary_check_interval_ms,
60_000
)
# --- Client API ---
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Check if the system is currently resting (Shabbat/festival).
"""
@spec resting?() :: boolean()
def resting? do
GenServer.call(__MODULE__, :resting?)
end
@doc """
Get the current state.
"""
@spec state() :: map()
def state do
GenServer.call(__MODULE__, :state)
end
# --- GenServer callbacks ---
@impl true
def init(_opts) do
state = %{
resting: VavPipeline.Cloud.shabbat?(DateTime.utc_now()),
last_check: DateTime.utc_now(),
shabbat_count: 0
}
schedule_check()
Logger.info("ShabbatTimer started — resting: #{state.resting}")
{:ok, state}
end
@impl true
def handle_call(:resting?, _from, state) do
{:reply, state.resting, state}
end
@impl true
def handle_call(:state, _from, state) do
{:reply, state, state}
end
@impl true
def handle_info(:check_boundary, state) do
now = DateTime.utc_now()
is_shabbat = VavPipeline.Cloud.shabbat?(now)
new_state =
cond do
not state.resting and is_shabbat ->
# Shabbat has begun — cease all processing
Logger.info("ShabbatTimer: Shabbat begins — ceasing all processing")
%{state | resting: true, last_check: now, shabbat_count: state.shabbat_count + 1}
state.resting and not is_shabbat ->
# Shabbat has ended — resume processing
Logger.info("ShabbatTimer: Shabbat ends — resuming processing")
%{state | resting: false, last_check: now}
true ->
%{state | last_check: now}
end
schedule_check()
{:noreply, new_state}
end
defp schedule_check do
Process.send_after(self(), :check_boundary, @check_interval)
end
end
+96
View File
@@ -0,0 +1,96 @@
defmodule VavPipeline.Shema do
@moduledoc """
The Shema context builder.
Architecture §3.9, §4 Step 9:
Reads from L1 for te'amim/Torah governance.
Reads calendar for Shabbat/festivals.
Reads provision file for manna.
The ShemaContext is IMMUTABLE once built. It is passed through
every stage of the pipeline. Processing happens WITHIN the context,
never modifying it.
Construction spec §4.4: shema_context_cache_ttl_ms: 0
— always read fresh. No caching.
"""
alias VavPipeline.{L1Reader, Cloud, Manna}
@type shema_context :: %{
# From L1
verse: L1Reader.verse(),
teamim_marks: [map()],
ketiv_qere: [map()],
root_gematria: map(),
smikhut: [map()],
# From cloud (Torah calendar + L1 integrity)
cloud_state: Cloud.cloud_state(),
# From manna
daily_provision: Manna.manna() | :no_provision,
# Metadata
scope: VavPipeline.scope(),
received_at: DateTime.t()
}
@doc """
Receive context for a verse scope.
This is RECEPTION, not processing. The system hears before it speaks.
Reads L1 (te'amim, roots, ketiv/qere), checks cloud (Torah + calendar),
checks manna (provision).
Returns {:ok, context} or {:error, reason}.
"""
@spec receive_context(VavPipeline.scope()) :: {:ok, shema_context()} | {:error, String.t()}
def receive_context(scope) do
now = DateTime.utc_now()
with {:ok, verse} <- L1Reader.read_verse(scope.book, scope.chapter, scope.verse),
{:ok, cloud_state} <- Cloud.check_at(now),
{:ok, manna} <- Manna.check_at(now) do
context = %{
# L1 data — read fresh, never cached
verse: verse,
teamim_marks: L1Reader.extract_teamim(verse),
ketiv_qere: L1Reader.extract_ketiv_qere(verse),
root_gematria: L1Reader.extract_roots(verse),
smikhut: L1Reader.extract_smikhut(verse),
# Cloud state — derived from Torah (L1 integrity + calendar)
cloud_state: cloud_state,
# Manna — daily provision
daily_provision: manna,
# Metadata
scope: scope,
received_at: now
}
{:ok, context}
end
end
@doc """
Build context with explicit time. Testable variant.
"""
@spec receive_context_at(VavPipeline.scope(), DateTime.t()) ::
{:ok, shema_context()} | {:error, String.t()}
def receive_context_at(scope, now) do
with {:ok, verse} <- L1Reader.read_verse(scope.book, scope.chapter, scope.verse),
{:ok, cloud_state} <- Cloud.check_at(now),
{:ok, manna} <- Manna.check_at(now) do
context = %{
verse: verse,
teamim_marks: L1Reader.extract_teamim(verse),
ketiv_qere: L1Reader.extract_ketiv_qere(verse),
root_gematria: L1Reader.extract_roots(verse),
smikhut: L1Reader.extract_smikhut(verse),
cloud_state: cloud_state,
daily_provision: manna,
scope: scope,
received_at: now
}
{:ok, context}
end
end
end
@@ -0,0 +1,88 @@
defmodule VavPipeline.ShoreshEngineClient do
@moduledoc """
Client interface to the Haskell Shoresh Engine.
Architecture §3.3: NIF or port to Haskell engine.
Construction spec §3.1: JSON protocol over port.
Phase 2: Stub that returns data from L1/ShemaContext.
Phase 3 (Step 14): Full port/NIF connection to shoresh-engine binary.
"""
require Logger
@doc """
Process input through the Shoresh Engine.
Phase 2: Uses L1 data directly.
Phase 3: Sends JSON request to Haskell port.
"""
@spec process(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
def process(_raw_text, context) do
# Phase 2 stub — extract from ShemaContext
roots =
context.verse.words
|> Enum.filter(fn w -> w.shoresh != nil end)
|> Enum.map(fn w ->
%{
shoresh: w.shoresh,
binyan: infer_binyan(w.morph),
domain: :unknown,
torah_refs: [%{book: context.verse.book, chapter: context.verse.chapter, verse: context.verse.verse}],
gematria_value: Map.get(context.root_gematria, w.shoresh, 0)
}
end)
{:ok,
%{
roots_activated: roots,
parse_tree: nil,
dual_readings: context.ketiv_qere
}}
end
@doc """
Separate utterance using te'amim marks.
Phase 2: Returns groups based on melekh positions.
Phase 3: Delegates to Haskell Havdalah module.
"""
@spec separate(map(), [map()]) :: [map()]
def separate(utterance, teamim_marks) do
# Phase 2 stub — group by melekh divisions
melekh_positions =
teamim_marks
|> Enum.filter(fn m -> m.role == "melekh" end)
|> Enum.map(fn m -> m.word_index end)
[%{groups: melekh_positions, utterance: utterance}]
end
@doc """
Name a distinction.
Phase 2: Returns the primary root as the name.
Phase 3: Delegates to Haskell Kriah module.
"""
@spec name(map()) :: map()
def name(distinction) do
Map.put(distinction, :name, Map.get(distinction, :shoresh, "unnamed"))
end
# Infer binyan from morphological code
# H = Hebrew, V = verb, then binyan indicator
defp infer_binyan(morph) when is_binary(morph) do
cond do
String.contains?(morph, "Vq") -> :qal
String.contains?(morph, "VN") -> :nifal
String.contains?(morph, "Vp") -> :piel
String.contains?(morph, "VP") -> :pual
String.contains?(morph, "Vh") -> :hifil
String.contains?(morph, "VH") -> :hufal
String.contains?(morph, "Vt") -> :hitpael
true -> :qal
end
end
defp infer_binyan(_), do: :qal
end
@@ -0,0 +1,79 @@
defmodule VavPipeline.Specification do
@moduledoc """
Interface to L0/L1 specification for ki_tov evaluation.
The ki_tov step evaluates output against specification.
This module provides the evaluation logic.
Architecture §3.3:
- :tov — output aligns with specification
- {:lo_tov, reason} — output contradicts specification
- :teiku — unresolved, cannot determine alignment
"""
@doc """
Evaluate named results against specification.
Checks:
1. All roots referenced exist in L1
2. All verse references are valid
3. No halt conditions are triggered
4. Ketiv/qere positions are checked — both readings inform evaluation
"""
@spec evaluate([map()], [map()]) :: :tov | {:lo_tov, String.t()} | :teiku
def evaluate(named_results, ketiv_qere) do
cond do
Enum.empty?(named_results) ->
{:lo_tov, "empty results — tohu without vayomer"}
any_halt_condition?(named_results) ->
{:lo_tov, "halt condition triggered"}
all_roots_grounded?(named_results) ->
check_ketiv_qere_consistency(named_results, ketiv_qere)
true ->
:teiku
end
end
@doc """
Check if all roots in results exist in L1.
"""
@spec all_roots_grounded?([map()]) :: boolean()
def all_roots_grounded?(results) do
Enum.all?(results, fn result ->
case result do
%{shoresh: nil} -> false
%{shoresh: _} -> true
_ -> true
end
end)
end
@doc """
Check for halt conditions (the 3 cardinal sins).
Delegates to HaltConditions module.
"""
@spec any_halt_condition?([map()]) :: boolean()
def any_halt_condition?(results) do
VavPipeline.HaltConditions.check_all(results) != :clear
end
# Check consistency with ketiv/qere dual readings
defp check_ketiv_qere_consistency(_results, []), do: :tov
defp check_ketiv_qere_consistency(_results, ketiv_qere) do
# Both readings must be preserved — the distinction must not collapse
all_preserved =
Enum.all?(ketiv_qere, fn kq ->
Map.get(kq, :both_preserved, true)
end)
if all_preserved do
:tov
else
{:lo_tov, "ketiv/qere distinction collapsed — gilui arayot boundary violation"}
end
end
end
@@ -0,0 +1,76 @@
defmodule VavPipeline.TalmudicEngineClient do
@moduledoc """
Client interface to the SWI-Prolog Talmudic Engine.
Construction spec §3.3:
Protocol: JSON over Unix domain socket
Socket: /var/run/shorashim/talmudic.sock
Phase 2: Stub that returns placeholder responses.
Phase 3 (Step 14): Full socket connection to talmudic_engine.
"""
require Logger
# Phase 3: Unix domain socket path
# @socket_path Application.compile_env(
# :vav_pipeline,
# :talmudic_socket,
# "/var/run/shorashim/talmudic.sock"
# )
@doc """
Send an interpretation request to the Talmudic Engine.
Phase 2: Returns a stub response.
Phase 3: Sends JSON over Unix socket.
"""
@spec interpret(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
def interpret(case_ref, _context) do
Logger.info("TalmudicEngineClient: interpret stub for #{case_ref}")
{:ok,
%{
status: :teiku,
interpretation: %{
text: "Phase 2 stub — Talmudic Engine not yet connected",
prolog_form: "stub(#{case_ref})",
grounding: [],
confidence: :low
},
challenge: nil
}}
end
@doc """
Send a challenge request.
"""
@spec challenge(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
def challenge(case_ref, _context) do
Logger.info("TalmudicEngineClient: challenge stub for #{case_ref}")
{:ok,
%{
status: :teiku,
challenge: %{
kushya: "Phase 2 stub — no challenge generated",
contradicting: nil,
strength: :weak
}
}}
end
@doc """
Send a resolution request.
"""
@spec resolve(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
def resolve(case_ref, _context) do
Logger.info("TalmudicEngineClient: resolve stub for #{case_ref}")
{:ok,
%{
status: :teiku,
resolution: "Phase 2 stub — resolution deferred"
}}
end
end