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
|
signal
|
||||||
end
|
end
|
||||||
|
|
||||||
# Signal type → trumpet pattern mapping (Bamidbar 10)
|
# Signal type → trumpet pattern mapping (Bamidbar 10, Architecture §3.12)
|
||||||
defp pattern_for(:assembly_all), do: :tekiah_tekiah
|
defp pattern_for(:assembly_all), do: :tekiah
|
||||||
defp pattern_for(:assembly_leaders), do: :tekiah
|
defp pattern_for(:assembly_leaders), do: :teruah
|
||||||
defp pattern_for(:breaking_camp), do: :teruah
|
defp pattern_for(:breaking_camp), do: :tekiah_teruah
|
||||||
defp pattern_for(:war), do: :teruah_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(:breaking_camp), do: :immediate
|
||||||
defp urgency_for(:war), do: :immediate
|
defp urgency_for(:war), do: :immediate
|
||||||
|
|||||||
@@ -86,20 +86,26 @@ defmodule VavPipeline.Cloud do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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.
|
Torah specifies these rest days (Vayikra 23):
|
||||||
Production will need a proper Hebrew calendar library.
|
- Pesach: 15 and 21 Nisan (first and last day)
|
||||||
For Phase 2 dev, returns false — festivals will be added in Phase 3
|
- Shavuot: 6 Sivan
|
||||||
with a proper luach implementation.
|
- 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()
|
@spec festival?(DateTime.t()) :: boolean()
|
||||||
def festival?(_now) do
|
def festival?(now) do
|
||||||
# Placeholder — requires Hebrew calendar conversion
|
date = DateTime.to_date(now)
|
||||||
# Festivals: Pesach (15-21 Nisan), Shavuot (6 Sivan),
|
year = date.year
|
||||||
# Rosh Hashanah (1-2 Tishrei), Yom Kippur (10 Tishrei),
|
|
||||||
# Sukkot (15-21 Tishrei), Shemini Atzeret (22 Tishrei)
|
festival_dates(year)
|
||||||
false
|
|> Enum.any?(fn festival_date -> Date.compare(date, festival_date) == :eq end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -122,7 +128,80 @@ defmodule VavPipeline.Cloud do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@spec current_festival(DateTime.t()) :: atom()
|
@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()
|
@spec next_motzei_shabbat(DateTime.t()) :: DateTime.t()
|
||||||
defp next_motzei_shabbat(now) do
|
defp next_motzei_shabbat(now) do
|
||||||
|
|||||||
@@ -137,9 +137,8 @@ defmodule VavPipeline.Manna do
|
|||||||
defp validate_manna(data, now) do
|
defp validate_manna(data, now) do
|
||||||
with {:ok, date} <- validate_date(data["date"], now),
|
with {:ok, date} <- validate_date(data["date"], now),
|
||||||
{:ok, provision} <- validate_provision(data["provision"]),
|
{:ok, provision} <- validate_provision(data["provision"]),
|
||||||
{:ok, expires_at} <- parse_datetime(data["expires_at"]),
|
{:ok, expires_at} <- parse_expires_at(data["expires_at"]),
|
||||||
:ok <- check_expiry(expires_at, now),
|
:ok <- check_expiry(expires_at, now) do
|
||||||
{:ok, written_at} <- parse_datetime(data["written_at"]) do
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
date: date,
|
date: date,
|
||||||
@@ -147,7 +146,7 @@ defmodule VavPipeline.Manna do
|
|||||||
type: parse_type(data["type"]),
|
type: parse_type(data["type"]),
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
written_by: data["written_by"] || "unknown",
|
written_by: data["written_by"] || "unknown",
|
||||||
written_at: written_at
|
written_at: parse_written_at(data["written_at"])
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -187,12 +186,23 @@ defmodule VavPipeline.Manna do
|
|||||||
end
|
end
|
||||||
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
|
case DateTime.from_iso8601(str) do
|
||||||
{:ok, dt, _offset} -> {:ok, dt}
|
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,23 @@ defmodule Step15ShemaTest do
|
|||||||
assert {:transition, :shabbat_rest} = result
|
assert {:transition, :shabbat_rest} = result
|
||||||
end
|
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
|
test "cloud_not_visible triggers waiting" do
|
||||||
# Manually construct context with cloud_not_visible
|
# Manually construct context with cloud_not_visible
|
||||||
{:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon)
|
{: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")
|
{:ok, non_shmitah, _} = DateTime.from_iso8601("2026-06-15T12:00:00Z")
|
||||||
assert VavPipeline.Cloud.shmitah_year?(non_shmitah) == false
|
assert VavPipeline.Cloud.shmitah_year?(non_shmitah) == false
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -235,22 +264,33 @@ defmodule VavPipelineTest do
|
|||||||
# Chatzotzrot Tests
|
# Chatzotzrot Tests
|
||||||
# ============================================================
|
# ============================================================
|
||||||
describe "Chatzotzrot" do
|
describe "Chatzotzrot" do
|
||||||
test "breaking camp signal" do
|
test "breaking camp signal — tekiah_teruah (Architecture §3.12)" do
|
||||||
signal = VavPipeline.Chatzotzrot.signal(:breaking_camp)
|
signal = VavPipeline.Chatzotzrot.signal(:breaking_camp)
|
||||||
assert signal.signal_type == :breaking_camp
|
assert signal.signal_type == :breaking_camp
|
||||||
assert signal.pattern == :teruah
|
assert signal.pattern == :tekiah_teruah
|
||||||
assert signal.details.urgency == :immediate
|
assert signal.details.urgency == :immediate
|
||||||
end
|
end
|
||||||
|
|
||||||
test "assembly signal" do
|
test "assembly_all signal — tekiah (Architecture §3.12)" do
|
||||||
signal = VavPipeline.Chatzotzrot.signal(:assembly_all)
|
signal = VavPipeline.Chatzotzrot.signal(:assembly_all)
|
||||||
assert signal.signal_type == :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
|
assert signal.pattern == :tekiah_tekiah
|
||||||
end
|
end
|
||||||
|
|
||||||
test "moadim signal" do
|
test "war signal — teruah_teruah (Architecture §3.12)" do
|
||||||
signal = VavPipeline.Chatzotzrot.signal(:moadim)
|
signal = VavPipeline.Chatzotzrot.signal(:war)
|
||||||
assert signal.pattern == :tekiah_teruah
|
assert signal.pattern == :teruah_teruah
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user