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:
Scott KarpandClaude Opus 4.6 committed 2026-03-12 20:29:24 -04:00
1 parent 5a93056af4
commit af7522e977
5 files changed
+176 -30

No files matched your search

+5 -5
View File
@@ -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
+91 -12
View File
@@ -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
+17 -7
View File
@@ -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
+17
View File
@@ -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)
+46 -6
View File
@@ -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