Files
bikoret/vav_pipeline/lib/vav_pipeline/cloud.ex
T
Scott KarpandClaude Opus 4.6 61243ac832 Phase 2 review corrections — three spec violations fixed
1. Chatzotzrot signal mapping corrected to match Architecture §3.12:
   assembly_all→tekiah, assembly_leaders→teruah,
   breaking_camp→tekiah_teruah, moadim→tekiah_tekiah.
   Four of five patterns were swapped. All five now match Bamidbar 10.

2. Manna parse_datetime(nil) bug fixed: missing expires_at now fails
   validation instead of silently returning DateTime.utc_now() which
   created a race condition where manna appeared expired.

3. Festival detection implemented in cloud.ex: pre-computed Gregorian
   dates for Pesach, Shavuot, Rosh Hashanah, Yom Kippur, Sukkot,
   Shemini Atzeret (2025-2028). Pipeline now halts on Yom Tov as
   Torah requires. Step 15 verification now covers festival rest.

76 tests, 0 failures (68 prior + 8 new).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:29:24 -04:00

224 lines
6.6 KiB
Elixir

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) where work is prohibited.
Torah specifies these rest days (Vayikra 23):
- Pesach: 15 and 21 Nisan (first and last day)
- Shavuot: 6 Sivan
- Rosh Hashanah: 1-2 Tishrei
- Yom Kippur: 10 Tishrei
- Sukkot: 15 Tishrei
- Shemini Atzeret: 22 Tishrei
Uses a pre-computed Gregorian date table for known years.
Festival days run sunset-to-sunset; simplified to full calendar day.
"""
@spec festival?(DateTime.t()) :: boolean()
def festival?(now) do
date = DateTime.to_date(now)
year = date.year
festival_dates(year)
|> Enum.any?(fn festival_date -> Date.compare(date, festival_date) == :eq end)
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
date = DateTime.to_date(now)
year = date.year
# Check each festival period
dates = festival_date_map(year)
cond do
date in Map.get(dates, :pesach, []) -> :pesach
date in Map.get(dates, :shavuot, []) -> :shavuot
date in Map.get(dates, :rosh_hashanah, []) -> :rosh_hashanah
date in Map.get(dates, :yom_kippur, []) -> :yom_kippur
date in Map.get(dates, :sukkot, []) -> :sukkot
date in Map.get(dates, :shemini_atzeret, []) -> :shemini_atzeret
true -> :unknown
end
end
# Festival dates for known years (Gregorian dates of Yom Tov rest days).
# Outside diaspora — Torah days only, not second-day diaspora additions.
#
# Source: fixed Hebrew calendar. Each year mapped from Hebrew calendar.
# Years outside the table fall back to :unknown (no festival detection).
@festival_table %{
# Hebrew 5786 (2025-2026)
2025 => %{
pesach: [~D[2025-04-13], ~D[2025-04-19]],
shavuot: [~D[2025-06-02]],
rosh_hashanah: [~D[2025-09-23], ~D[2025-09-24]],
yom_kippur: [~D[2025-10-02]],
sukkot: [~D[2025-10-07]],
shemini_atzeret: [~D[2025-10-14]]
},
# Hebrew 5787 (2026-2027)
2026 => %{
pesach: [~D[2026-04-02], ~D[2026-04-08]],
shavuot: [~D[2026-05-22]],
rosh_hashanah: [~D[2026-09-12], ~D[2026-09-13]],
yom_kippur: [~D[2026-09-21]],
sukkot: [~D[2026-09-26]],
shemini_atzeret: [~D[2026-10-03]]
},
# Hebrew 5788 (2027-2028)
2027 => %{
pesach: [~D[2027-04-22], ~D[2027-04-28]],
shavuot: [~D[2027-06-11]],
rosh_hashanah: [~D[2027-10-02], ~D[2027-10-03]],
yom_kippur: [~D[2027-10-11]],
sukkot: [~D[2027-10-16]],
shemini_atzeret: [~D[2027-10-23]]
},
# Hebrew 5789 (2028-2029)
2028 => %{
pesach: [~D[2028-04-11], ~D[2028-04-17]],
shavuot: [~D[2028-05-31]],
rosh_hashanah: [~D[2028-09-21], ~D[2028-09-22]],
yom_kippur: [~D[2028-09-30]],
sukkot: [~D[2028-10-05]],
shemini_atzeret: [~D[2028-10-12]]
}
}
# Flat list of all festival dates for a given year (for festival? check)
defp festival_dates(year) do
case Map.get(@festival_table, year) do
nil -> []
dates -> dates |> Map.values() |> List.flatten()
end
end
# Structured map for current_festival lookup
defp festival_date_map(year) do
Map.get(@festival_table, year, %{})
end
@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