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>
224 lines
6.6 KiB
Elixir
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
|