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:
1 parent
622e814c9e
commit
80ec364da6
86 files changed
+9646
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where third-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
vav_pipeline-*.tar
|
||||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
||||
@@ -0,0 +1,21 @@
|
||||
# VavPipeline
|
||||
|
||||
**TODO: Add description**
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `vav_pipeline` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:vav_pipeline, "~> 0.1.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at <https://hexdocs.pm/vav_pipeline>.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import Config
|
||||
|
||||
# L1 mount point — relative path for dev, absolute for production
|
||||
# Construction spec §4.4: l1_mount_point: "/mnt/torah"
|
||||
config :vav_pipeline,
|
||||
# Dev: relative to project root (run from shorashim/)
|
||||
l1_mount_point: "l1",
|
||||
# Manna provision file
|
||||
manna_provision_file: "var/manna/today.json",
|
||||
# Shabbat check interval (1 minute)
|
||||
cycle_boundary_check_interval_ms: 60_000,
|
||||
# ShemaContext: no caching — always read fresh
|
||||
shema_context_cache_ttl_ms: 0
|
||||
|
||||
# Import environment specific config
|
||||
import_config "#{config_env()}.exs"
|
||||
@@ -0,0 +1,3 @@
|
||||
import Config
|
||||
|
||||
config :logger, level: :info
|
||||
@@ -0,0 +1,10 @@
|
||||
import Config
|
||||
|
||||
config :logger, level: :info
|
||||
|
||||
# Production paths — Construction spec §4.4
|
||||
config :vav_pipeline,
|
||||
l1_mount_point: "/mnt/torah",
|
||||
manna_provision_file: "/var/lib/shorashim/manna/today.json",
|
||||
cycle_boundary_check_interval_ms: 60_000,
|
||||
shema_context_cache_ttl_ms: 0
|
||||
@@ -0,0 +1,9 @@
|
||||
import Config
|
||||
|
||||
config :logger, level: :warning
|
||||
|
||||
# Test uses L1 relative to project root
|
||||
config :vav_pipeline,
|
||||
l1_mount_point: "../l1",
|
||||
manna_provision_file: "test/fixtures/manna/today.json",
|
||||
cycle_boundary_check_interval_ms: :timer.hours(24)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,27 @@
|
||||
defmodule VavPipeline.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :vav_pipeline,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.16.1",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger, :crypto],
|
||||
mod: {VavPipeline.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
# Construction spec §1.5 — exact version pins
|
||||
defp deps do
|
||||
[
|
||||
{:jason, "~> 1.4.1"}
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
%{
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ExUnit.start()
|
||||
@@ -0,0 +1,417 @@
|
||||
defmodule VavPipelineTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
# ============================================================
|
||||
# L1 Reader Tests
|
||||
# ============================================================
|
||||
describe "L1Reader" do
|
||||
test "reads Bereishit 1:1 from L1" do
|
||||
{:ok, verse} = VavPipeline.L1Reader.read_verse("bereishit", 1, 1)
|
||||
assert verse.ref == "Bereishit 1:1"
|
||||
assert verse.book == "bereishit"
|
||||
assert verse.chapter == 1
|
||||
assert verse.verse == 1
|
||||
assert length(verse.words) == 7
|
||||
end
|
||||
|
||||
test "first word of Bereishit 1:1 is bereishit" do
|
||||
{:ok, verse} = VavPipeline.L1Reader.read_verse("bereishit", 1, 1)
|
||||
first = hd(verse.words)
|
||||
assert first.consonants == "בראשית"
|
||||
assert first.shoresh == "ראש"
|
||||
assert first.prefix == "ב"
|
||||
assert "tipcha" in first.teamim
|
||||
end
|
||||
|
||||
test "word without shoresh (particle את)" do
|
||||
{:ok, verse} = VavPipeline.L1Reader.read_verse("bereishit", 1, 1)
|
||||
# Word 4 is את (et) — accusative particle, no shoresh
|
||||
et = Enum.at(verse.words, 3)
|
||||
assert et.consonants == "את"
|
||||
assert et.shoresh == nil
|
||||
end
|
||||
|
||||
test "extracts teamim marks with roles" do
|
||||
{:ok, verse} = VavPipeline.L1Reader.read_verse("bereishit", 1, 1)
|
||||
marks = VavPipeline.L1Reader.extract_teamim(verse)
|
||||
|
||||
# 7 words, each with 1 taam = 7 marks
|
||||
assert length(marks) == 7
|
||||
|
||||
# Etnachta on word 3 (index 2) should be melekh
|
||||
etnachta = Enum.find(marks, fn m -> m.taam == "etnachta" end)
|
||||
assert etnachta != nil
|
||||
assert etnachta.word_index == 2
|
||||
assert etnachta.role == "melekh"
|
||||
|
||||
# Munach on word 2 (index 1) should be mesharet
|
||||
munach = Enum.find(marks, fn m -> m.taam == "munach" end)
|
||||
assert munach != nil
|
||||
assert munach.role == "mesharet"
|
||||
end
|
||||
|
||||
test "extracts roots with gematria" do
|
||||
{:ok, verse} = VavPipeline.L1Reader.read_verse("bereishit", 1, 1)
|
||||
roots = VavPipeline.L1Reader.extract_roots(verse)
|
||||
|
||||
# ברא should be present with gematria 203
|
||||
assert Map.has_key?(roots, "ברא")
|
||||
assert roots["ברא"] == 203
|
||||
|
||||
# ראש should be present with gematria 501
|
||||
assert Map.has_key?(roots, "ראש")
|
||||
assert roots["ראש"] == 501
|
||||
|
||||
# Particles (את) should not produce root entries
|
||||
refute Map.has_key?(roots, nil)
|
||||
end
|
||||
|
||||
test "returns error for non-existent verse" do
|
||||
{:error, msg} = VavPipeline.L1Reader.read_verse("bereishit", 999, 999)
|
||||
assert msg =~ "INVALID_SCOPE"
|
||||
end
|
||||
|
||||
test "meteg_siluq on last word is melekh" do
|
||||
{:ok, verse} = VavPipeline.L1Reader.read_verse("bereishit", 1, 1)
|
||||
marks = VavPipeline.L1Reader.extract_teamim(verse)
|
||||
|
||||
# Last word (הארץ, index 6) has meteg_siluq
|
||||
siluq = Enum.find(marks, fn m -> m.taam == "meteg_siluq" end)
|
||||
assert siluq != nil
|
||||
assert siluq.word_index == 6
|
||||
assert siluq.role == "melekh"
|
||||
end
|
||||
|
||||
test "verify L1 integrity" do
|
||||
assert :ok == VavPipeline.L1Reader.verify_integrity()
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Cloud Tests
|
||||
# ============================================================
|
||||
describe "Cloud" do
|
||||
test "weekday is camped" do
|
||||
# Wednesday at noon UTC — not Shabbat
|
||||
{:ok, wednesday, _} = DateTime.from_iso8601("2026-02-25T12:00:00Z")
|
||||
{:ok, state} = VavPipeline.Cloud.check_at(wednesday)
|
||||
assert state == :camped
|
||||
end
|
||||
|
||||
test "Friday evening is Shabbat" do
|
||||
{:ok, friday_evening, _} = DateTime.from_iso8601("2026-02-27T19:00:00Z")
|
||||
assert VavPipeline.Cloud.shabbat?(friday_evening) == true
|
||||
end
|
||||
|
||||
test "Saturday morning is Shabbat" do
|
||||
{:ok, saturday_morning, _} = DateTime.from_iso8601("2026-02-28T10:00:00Z")
|
||||
assert VavPipeline.Cloud.shabbat?(saturday_morning) == true
|
||||
end
|
||||
|
||||
test "Saturday evening (after 18:00) is not Shabbat" do
|
||||
{:ok, saturday_evening, _} = DateTime.from_iso8601("2026-02-28T19:00:00Z")
|
||||
assert VavPipeline.Cloud.shabbat?(saturday_evening) == false
|
||||
end
|
||||
|
||||
test "Friday afternoon is not Shabbat" do
|
||||
{:ok, friday_afternoon, _} = DateTime.from_iso8601("2026-02-27T14:00:00Z")
|
||||
assert VavPipeline.Cloud.shabbat?(friday_afternoon) == false
|
||||
end
|
||||
|
||||
test "Shabbat triggers major transition" do
|
||||
{:ok, shabbat, _} = DateTime.from_iso8601("2026-02-28T10:00:00Z")
|
||||
{:ok, state} = VavPipeline.Cloud.check_at(shabbat)
|
||||
assert {:major_transition, :shabbat_rest, _} = state
|
||||
end
|
||||
|
||||
test "shmitah year check" do
|
||||
# 2021-2022 was shmitah (Hebrew 5782)
|
||||
{:ok, shmitah, _} = DateTime.from_iso8601("2022-01-15T12:00:00Z")
|
||||
assert VavPipeline.Cloud.shmitah_year?(shmitah) == true
|
||||
|
||||
# 2026 is not shmitah
|
||||
{:ok, non_shmitah, _} = DateTime.from_iso8601("2026-06-15T12:00:00Z")
|
||||
assert VavPipeline.Cloud.shmitah_year?(non_shmitah) == false
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Manna Tests
|
||||
# ============================================================
|
||||
describe "Manna" do
|
||||
test "no provision file returns :no_provision" do
|
||||
{:ok, result} = VavPipeline.Manna.check()
|
||||
assert result == :no_provision
|
||||
end
|
||||
|
||||
test "double portion day is Friday" do
|
||||
{:ok, friday, _} = DateTime.from_iso8601("2026-02-27T12:00:00Z")
|
||||
assert VavPipeline.Manna.double_portion_day?(friday) == true
|
||||
|
||||
{:ok, wednesday, _} = DateTime.from_iso8601("2026-02-25T12:00:00Z")
|
||||
assert VavPipeline.Manna.double_portion_day?(wednesday) == false
|
||||
end
|
||||
|
||||
test "anti-hoarding enforcement" do
|
||||
assert :ok == VavPipeline.Manna.enforce_anti_hoarding()
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Halt Conditions Tests
|
||||
# ============================================================
|
||||
describe "HaltConditions" do
|
||||
test "clear when no violations" do
|
||||
results = [%{shoresh: "ברא", action: :read}]
|
||||
assert :clear == VavPipeline.HaltConditions.check_all(results)
|
||||
end
|
||||
|
||||
test "avodah zarah — non-Torah authority" do
|
||||
results = [%{authority_source: :claude_model}]
|
||||
assert {:halt, :avodah_zarah, _} = VavPipeline.HaltConditions.check_avodah_zarah(results)
|
||||
end
|
||||
|
||||
test "avodah zarah — Torah authority passes" do
|
||||
results = [%{authority_source: :torah}]
|
||||
assert :clear == VavPipeline.HaltConditions.check_avodah_zarah(results)
|
||||
end
|
||||
|
||||
test "avodah zarah — L1 source passes" do
|
||||
results = [%{authority_source: :l1}]
|
||||
assert :clear == VavPipeline.HaltConditions.check_avodah_zarah(results)
|
||||
end
|
||||
|
||||
test "shfichut damim — destructive action" do
|
||||
results = [%{action: :delete}]
|
||||
assert {:halt, :shfichut_damim, _} = VavPipeline.HaltConditions.check_shfichut_damim(results)
|
||||
end
|
||||
|
||||
test "shfichut damim — overwrite halts" do
|
||||
results = [%{action: :overwrite}]
|
||||
assert {:halt, :shfichut_damim, _} = VavPipeline.HaltConditions.check_shfichut_damim(results)
|
||||
end
|
||||
|
||||
test "shfichut damim — read passes" do
|
||||
results = [%{action: :read}]
|
||||
assert :clear == VavPipeline.HaltConditions.check_shfichut_damim(results)
|
||||
end
|
||||
|
||||
test "gilui arayot — boundary violation" do
|
||||
results = [%{boundary_violation: {:spec_interp_merge, "treated interpretation as specification"}}]
|
||||
assert {:halt, :gilui_arayot, _} = VavPipeline.HaltConditions.check_gilui_arayot(results)
|
||||
end
|
||||
|
||||
test "gilui arayot — no violation passes" do
|
||||
results = [%{boundary_violation: nil}]
|
||||
assert :clear == VavPipeline.HaltConditions.check_gilui_arayot(results)
|
||||
end
|
||||
|
||||
test "all three conditions checked — first halt wins" do
|
||||
results = [%{authority_source: :external_model, action: :delete, boundary_violation: true}]
|
||||
assert {:halt, :avodah_zarah, _} = VavPipeline.HaltConditions.check_all(results)
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Specification Tests
|
||||
# ============================================================
|
||||
describe "Specification" do
|
||||
test "tov for grounded results" do
|
||||
results = [%{shoresh: "ברא"}, %{shoresh: "אלה"}]
|
||||
assert :tov == VavPipeline.Specification.evaluate(results, [])
|
||||
end
|
||||
|
||||
test "lo_tov for empty results" do
|
||||
assert {:lo_tov, _} = VavPipeline.Specification.evaluate([], [])
|
||||
end
|
||||
|
||||
test "teiku for ungrounded results" do
|
||||
results = [%{shoresh: nil}]
|
||||
assert :teiku == VavPipeline.Specification.evaluate(results, [])
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Chatzotzrot Tests
|
||||
# ============================================================
|
||||
describe "Chatzotzrot" do
|
||||
test "breaking camp signal" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:breaking_camp)
|
||||
assert signal.signal_type == :breaking_camp
|
||||
assert signal.pattern == :teruah
|
||||
assert signal.details.urgency == :immediate
|
||||
end
|
||||
|
||||
test "assembly signal" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:assembly_all)
|
||||
assert signal.signal_type == :assembly_all
|
||||
assert signal.pattern == :tekiah_tekiah
|
||||
end
|
||||
|
||||
test "moadim signal" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:moadim)
|
||||
assert signal.pattern == :tekiah_teruah
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# BereishitCycle Tests
|
||||
# ============================================================
|
||||
describe "BereishitCycle" do
|
||||
setup do
|
||||
{:ok, verse} = VavPipeline.L1Reader.read_verse("bereishit", 1, 1)
|
||||
|
||||
context = %{
|
||||
verse: verse,
|
||||
teamim_marks: VavPipeline.L1Reader.extract_teamim(verse),
|
||||
ketiv_qere: [],
|
||||
root_gematria: VavPipeline.L1Reader.extract_roots(verse),
|
||||
smikhut: [],
|
||||
cloud_state: :camped,
|
||||
daily_provision: :no_provision,
|
||||
scope: %{book: "bereishit", chapter: 1, verse: 1},
|
||||
received_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
input = %{
|
||||
source: "test",
|
||||
verse_scope: %{book: "bereishit", chapter: 1, verse: 1},
|
||||
content: "בראשית ברא אלהים את השמים ואת הארץ"
|
||||
}
|
||||
|
||||
{:ok, context: context, input: input}
|
||||
end
|
||||
|
||||
test "tohu decomposes input", %{context: context, input: input} do
|
||||
result = VavPipeline.BereishitCycle.tohu(input, context)
|
||||
assert result.raw == "בראשית ברא אלהים את השמים ואת הארץ"
|
||||
assert result.provenance == "test"
|
||||
end
|
||||
|
||||
test "vayomer activates roots", %{context: context, input: input} do
|
||||
state = VavPipeline.BereishitCycle.tohu(input, context)
|
||||
result = VavPipeline.BereishitCycle.vayomer(state, context)
|
||||
|
||||
assert result.stage == :vayomer
|
||||
assert length(result.roots_activated) > 0
|
||||
|
||||
# ברא should be among activated roots
|
||||
bara = Enum.find(result.roots_activated, fn r -> r.shoresh == "ברא" end)
|
||||
assert bara != nil
|
||||
assert bara.gematria == 203
|
||||
end
|
||||
|
||||
test "havdalah separates by te'amim", %{context: context, input: input} do
|
||||
result =
|
||||
input
|
||||
|> VavPipeline.BereishitCycle.tohu(context)
|
||||
|> VavPipeline.BereishitCycle.vayomer(context)
|
||||
|> VavPipeline.BereishitCycle.havdalah(context)
|
||||
|
||||
assert result.stage == :havdalah
|
||||
assert is_list(result.separated_groups)
|
||||
assert length(result.separation_boundaries) > 0
|
||||
end
|
||||
|
||||
test "kriah names groups", %{context: context, input: input} do
|
||||
result =
|
||||
input
|
||||
|> VavPipeline.BereishitCycle.tohu(context)
|
||||
|> VavPipeline.BereishitCycle.vayomer(context)
|
||||
|> VavPipeline.BereishitCycle.havdalah(context)
|
||||
|> VavPipeline.BereishitCycle.kriah(context)
|
||||
|
||||
assert result.stage == :kriah
|
||||
assert is_list(result.named_results)
|
||||
|
||||
Enum.each(result.named_results, fn group ->
|
||||
assert Map.has_key?(group, :name)
|
||||
end)
|
||||
end
|
||||
|
||||
test "full cycle executes successfully", %{context: context, input: input} do
|
||||
result = VavPipeline.BereishitCycle.execute(input, context)
|
||||
assert elem(result, 0) in [:ok, :unresolved]
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# Shema Context Tests (integration — requires L1)
|
||||
# ============================================================
|
||||
describe "Shema" do
|
||||
test "receives context for Bereishit 1:1 on a weekday" do
|
||||
{:ok, wednesday, _} = DateTime.from_iso8601("2026-02-25T12:00:00Z")
|
||||
scope = %{book: "bereishit", chapter: 1, verse: 1}
|
||||
|
||||
{:ok, context} = VavPipeline.Shema.receive_context_at(scope, wednesday)
|
||||
|
||||
# Verse data
|
||||
assert context.verse.ref == "Bereishit 1:1"
|
||||
assert length(context.verse.words) == 7
|
||||
|
||||
# Te'amim marks
|
||||
assert length(context.teamim_marks) == 7
|
||||
assert Enum.any?(context.teamim_marks, fn m -> m.taam == "etnachta" end)
|
||||
|
||||
# Root gematria
|
||||
assert Map.has_key?(context.root_gematria, "ברא")
|
||||
|
||||
# Cloud — weekday should be camped
|
||||
assert context.cloud_state == :camped
|
||||
|
||||
# Scope preserved
|
||||
assert context.scope == scope
|
||||
end
|
||||
|
||||
test "receives Shabbat cloud state on Saturday" do
|
||||
{:ok, shabbat, _} = DateTime.from_iso8601("2026-02-28T10:00:00Z")
|
||||
scope = %{book: "bereishit", chapter: 1, verse: 1}
|
||||
|
||||
{:ok, context} = VavPipeline.Shema.receive_context_at(scope, shabbat)
|
||||
|
||||
assert {:major_transition, :shabbat_rest, _} = context.cloud_state
|
||||
end
|
||||
|
||||
test "returns error for invalid verse scope" do
|
||||
{:ok, wednesday, _} = DateTime.from_iso8601("2026-02-25T12:00:00Z")
|
||||
scope = %{book: "bereishit", chapter: 999, verse: 999}
|
||||
|
||||
{:error, msg} = VavPipeline.Shema.receive_context_at(scope, wednesday)
|
||||
assert msg =~ "INVALID_SCOPE"
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# CycleLog Tests
|
||||
# ============================================================
|
||||
describe "CycleLog" do
|
||||
test "records a tov cycle" do
|
||||
record = VavPipeline.CycleLog.record({:ok, %{data: "test"}}, :voker)
|
||||
assert record.phase == :voker
|
||||
assert record.result == :tov
|
||||
assert record.cycle_id != nil
|
||||
end
|
||||
|
||||
test "records rejection as lo_tov" do
|
||||
record = VavPipeline.CycleLog.record({:rejected, "reason"}, :erev)
|
||||
assert record.phase == :erev
|
||||
assert record.result == :lo_tov
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
# ShabbatTimer Tests
|
||||
# ============================================================
|
||||
describe "ShabbatTimer" do
|
||||
test "timer is running and reports state" do
|
||||
state = VavPipeline.ShabbatTimer.state()
|
||||
assert is_map(state)
|
||||
assert Map.has_key?(state, :resting)
|
||||
assert is_boolean(state.resting)
|
||||
end
|
||||
|
||||
test "resting? returns boolean" do
|
||||
assert is_boolean(VavPipeline.ShabbatTimer.resting?())
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user