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>
This commit is contained in:
1 parent
5a93056af4
commit
af7522e977
5 files changed
+176
-30
No files matched your search
@@ -44,12 +44,12 @@ defmodule VavPipeline.Chatzotzrot do
|
||||
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
|
||||
# Signal type → trumpet pattern mapping (Bamidbar 10, Architecture §3.12)
|
||||
defp pattern_for(:assembly_all), do: :tekiah
|
||||
defp pattern_for(:assembly_leaders), do: :teruah
|
||||
defp pattern_for(:breaking_camp), do: :tekiah_teruah
|
||||
defp pattern_for(:war), do: :teruah_teruah
|
||||
defp pattern_for(:moadim), do: :tekiah_teruah
|
||||
defp pattern_for(:moadim), do: :tekiah_tekiah
|
||||
|
||||
defp urgency_for(:breaking_camp), do: :immediate
|
||||
defp urgency_for(:war), do: :immediate
|
||||
|
||||
@@ -86,20 +86,26 @@ defmodule VavPipeline.Cloud do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if now is a major festival (Yom Tov).
|
||||
Check if now is a major festival (Yom Tov) where work is prohibited.
|
||||
|
||||
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.
|
||||
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
|
||||
# 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
|
||||
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 """
|
||||
@@ -122,7 +128,80 @@ defmodule VavPipeline.Cloud do
|
||||
end
|
||||
|
||||
@spec current_festival(DateTime.t()) :: atom()
|
||||
defp current_festival(_now), do: :unknown
|
||||
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
|
||||
|
||||
@@ -137,9 +137,8 @@ defmodule VavPipeline.Manna do
|
||||
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, expires_at} <- parse_expires_at(data["expires_at"]),
|
||||
:ok <- check_expiry(expires_at, now) do
|
||||
{:ok,
|
||||
%{
|
||||
date: date,
|
||||
@@ -147,7 +146,7 @@ defmodule VavPipeline.Manna do
|
||||
type: parse_type(data["type"]),
|
||||
expires_at: expires_at,
|
||||
written_by: data["written_by"] || "unknown",
|
||||
written_at: written_at
|
||||
written_at: parse_written_at(data["written_at"])
|
||||
}}
|
||||
end
|
||||
end
|
||||
@@ -187,12 +186,23 @@ defmodule VavPipeline.Manna do
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_datetime(nil), do: {:ok, DateTime.utc_now()}
|
||||
# expires_at is required — missing means invalid provision
|
||||
defp parse_expires_at(nil), do: {:error, "MANNA_INVALID: missing expires_at"}
|
||||
|
||||
defp parse_datetime(str) do
|
||||
defp parse_expires_at(str) do
|
||||
case DateTime.from_iso8601(str) do
|
||||
{:ok, dt, _offset} -> {:ok, dt}
|
||||
{:error, _} -> {:error, "MANNA_INVALID: bad datetime #{str}"}
|
||||
{:error, _} -> {:error, "MANNA_INVALID: bad expires_at datetime #{str}"}
|
||||
end
|
||||
end
|
||||
|
||||
# written_at is metadata — missing is tolerable
|
||||
defp parse_written_at(nil), do: nil
|
||||
|
||||
defp parse_written_at(str) do
|
||||
case DateTime.from_iso8601(str) do
|
||||
{:ok, dt, _offset} -> dt
|
||||
{:error, _} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -205,6 +205,23 @@ defmodule Step15ShemaTest do
|
||||
assert {:transition, :shabbat_rest} = result
|
||||
end
|
||||
|
||||
test "festival cloud state prevents processing — Pesach" do
|
||||
# 15 Nisan 5787 = 2 April 2026
|
||||
{:ok, pesach, _} = DateTime.from_iso8601("2026-04-02T12:00:00Z")
|
||||
{:ok, context} = Shema.receive_context_at(@scope, pesach)
|
||||
|
||||
assert {:major_transition, :festival_rest, %{festival: :pesach}} = context.cloud_state
|
||||
|
||||
# Pipeline would return {:transition, :festival_rest}
|
||||
result =
|
||||
case context.cloud_state do
|
||||
{:major_transition, signal, _params} -> {:transition, signal}
|
||||
_ -> :would_process
|
||||
end
|
||||
|
||||
assert {:transition, :festival_rest} = result
|
||||
end
|
||||
|
||||
test "cloud_not_visible triggers waiting" do
|
||||
# Manually construct context with cloud_not_visible
|
||||
{:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon)
|
||||
|
||||
@@ -133,6 +133,35 @@ defmodule VavPipelineTest do
|
||||
{:ok, non_shmitah, _} = DateTime.from_iso8601("2026-06-15T12:00:00Z")
|
||||
assert VavPipeline.Cloud.shmitah_year?(non_shmitah) == false
|
||||
end
|
||||
|
||||
test "Pesach is a festival" do
|
||||
# 15 Nisan 5787 = 2 April 2026
|
||||
{:ok, pesach, _} = DateTime.from_iso8601("2026-04-02T12:00:00Z")
|
||||
assert VavPipeline.Cloud.festival?(pesach) == true
|
||||
end
|
||||
|
||||
test "Rosh Hashanah is a festival" do
|
||||
# 1 Tishrei 5787 = 12 September 2026
|
||||
{:ok, rosh_hashanah, _} = DateTime.from_iso8601("2026-09-12T12:00:00Z")
|
||||
assert VavPipeline.Cloud.festival?(rosh_hashanah) == true
|
||||
end
|
||||
|
||||
test "Yom Kippur is a festival" do
|
||||
# 10 Tishrei 5787 = 21 September 2026
|
||||
{:ok, yom_kippur, _} = DateTime.from_iso8601("2026-09-21T12:00:00Z")
|
||||
assert VavPipeline.Cloud.festival?(yom_kippur) == true
|
||||
end
|
||||
|
||||
test "regular weekday is not a festival" do
|
||||
{:ok, tuesday, _} = DateTime.from_iso8601("2026-03-10T12:00:00Z")
|
||||
assert VavPipeline.Cloud.festival?(tuesday) == false
|
||||
end
|
||||
|
||||
test "festival triggers major transition" do
|
||||
{:ok, pesach, _} = DateTime.from_iso8601("2026-04-02T12:00:00Z")
|
||||
{:ok, state} = VavPipeline.Cloud.check_at(pesach)
|
||||
assert {:major_transition, :festival_rest, %{festival: :pesach}} = state
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================
|
||||
@@ -235,22 +264,33 @@ defmodule VavPipelineTest do
|
||||
# Chatzotzrot Tests
|
||||
# ============================================================
|
||||
describe "Chatzotzrot" do
|
||||
test "breaking camp signal" do
|
||||
test "breaking camp signal — tekiah_teruah (Architecture §3.12)" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:breaking_camp)
|
||||
assert signal.signal_type == :breaking_camp
|
||||
assert signal.pattern == :teruah
|
||||
assert signal.pattern == :tekiah_teruah
|
||||
assert signal.details.urgency == :immediate
|
||||
end
|
||||
|
||||
test "assembly signal" do
|
||||
test "assembly_all signal — tekiah (Architecture §3.12)" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:assembly_all)
|
||||
assert signal.signal_type == :assembly_all
|
||||
assert signal.pattern == :tekiah
|
||||
end
|
||||
|
||||
test "assembly_leaders signal — teruah (Architecture §3.12)" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:assembly_leaders)
|
||||
assert signal.signal_type == :assembly_leaders
|
||||
assert signal.pattern == :teruah
|
||||
end
|
||||
|
||||
test "moadim signal — tekiah_tekiah (Architecture §3.12)" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:moadim)
|
||||
assert signal.pattern == :tekiah_tekiah
|
||||
end
|
||||
|
||||
test "moadim signal" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:moadim)
|
||||
assert signal.pattern == :tekiah_teruah
|
||||
test "war signal — teruah_teruah (Architecture §3.12)" do
|
||||
signal = VavPipeline.Chatzotzrot.signal(:war)
|
||||
assert signal.pattern == :teruah_teruah
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in new issue
Block a user