diff --git a/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex b/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex index 91f138ad..32348a25 100644 --- a/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex +++ b/vav_pipeline/lib/vav_pipeline/bereishit_cycle.ex @@ -19,7 +19,7 @@ defmodule VavPipeline.BereishitCycle do Latency budget (Construction spec §2.5): 5000ms total """ - alias VavPipeline.{Specification, CycleLog, Manna, + alias VavPipeline.{Specification, CycleLog, Manna, HaltConditions, ShoreshEngineClient, TalmudicEngineClient, SanhedrinClient} require Logger @@ -213,13 +213,32 @@ defmodule VavPipeline.BereishitCycle do # Enforce anti-hoarding Manna.enforce_anti_hoarding() - # Step 14: Route unresolved cases to Talmudic Engine / Sanhedrin + # Route unresolved, then check halt conditions on final output case result do - {:unresolved, data} -> - route_if_unresolved(data, context) + {:ok, data} -> + check_halt_on_output(data) - _ -> - result + {:unresolved, data} -> + routed = route_if_unresolved(data, context) + + case routed do + {:ok, routed_data} -> check_halt_on_output(routed_data) + other -> other + end + + other -> + other + end + end + + # --- Halt Check --- + + defp check_halt_on_output(data) do + results = Map.get(data, :named_results, []) + + case HaltConditions.check_all(results) do + :clear -> {:ok, data} + {:halt, condition, reason} -> {:halted, condition, reason} end end diff --git a/vav_pipeline/test/step15_shema_test.exs b/vav_pipeline/test/step15_shema_test.exs new file mode 100644 index 00000000..62471e39 --- /dev/null +++ b/vav_pipeline/test/step15_shema_test.exs @@ -0,0 +1,467 @@ +defmodule Step15ShemaTest do + @moduledoc """ + Step 15 Verification — The Shema Test. + + Architecture §6.3: Before the ki tov test, the shema test: can the system hear? + + Five tests from §6.3: + 1. Te'amim reception — the text's own divisions guide havdalah + 2. Ketiv/qere preservation — dual readings never collapse + 3. Cloud obedience — system does NOT process when cloud lifts + 4. Manna fidelity — system waits when no provision + 5. Context immutability — ShemaContext unchanged at every stage + + Plus integration tests from §6.2: + 6. Halt condition — pipeline halts on cardinal sin trigger + 7. Full pipeline end-to-end — input through to evaluated output + """ + + use ExUnit.Case, async: false + + alias VavPipeline.{BereishitCycle, Shema, Specification, HaltConditions} + + # A known weekday for testing — Wednesday noon UTC + @wednesday_noon "2026-02-25T12:00:00Z" + @scope %{book: "bereishit", chapter: 1, verse: 1} + + # ============================================================ + # Test 1: Te'amim Reception (Architecture §6.3) + # ============================================================ + describe "Shema Test 1: Te'amim Reception" do + test "ShemaContext contains raw te'amim marks from L1" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + # 7 words in Bereishit 1:1 → 7 te'amim marks + assert length(context.teamim_marks) == 7 + + # Each mark has taam, word_index, role + Enum.each(context.teamim_marks, fn mark -> + assert Map.has_key?(mark, :taam) + assert Map.has_key?(mark, :word_index) + assert Map.has_key?(mark, :role) + assert mark.role in ["melekh", "mesharet"] + end) + end + + test "etnachta on word 3 classified as melekh" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + etnachta = Enum.find(context.teamim_marks, fn m -> m.taam == "etnachta" end) + assert etnachta != nil + assert etnachta.word_index == 2 + assert etnachta.role == "melekh" + end + + test "meteg_siluq on last word classified as melekh" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + siluq = Enum.find(context.teamim_marks, fn m -> m.taam == "meteg_siluq" end) + assert siluq != nil + assert siluq.word_index == 6 + assert siluq.role == "melekh" + end + + test "havdalah follows the text's own divisions, not imposed ones" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + input = %{ + source: "test", + verse_scope: @scope, + content: "בראשית ברא אלהים את השמים ואת הארץ" + } + + # Run through tohu → vayomer → havdalah + result = + input + |> BereishitCycle.tohu(context) + |> BereishitCycle.vayomer(context) + |> BereishitCycle.havdalah(context) + + # Havdalah boundaries must match melekh positions from te'amim + melekh_positions = + context.teamim_marks + |> Enum.filter(fn m -> m.role == "melekh" end) + |> Enum.map(fn m -> m.word_index end) + |> Enum.sort() + + assert result.separation_boundaries == melekh_positions + + # System used text's divisions (etnachta at 2, meteg_siluq at 6) + assert 2 in result.separation_boundaries + assert 6 in result.separation_boundaries + end + end + + # ============================================================ + # Test 2: Ketiv/Qere Preservation (Architecture §6.3) + # ============================================================ + describe "Shema Test 2: Ketiv/Qere Preservation" do + setup do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + # Inject synthetic ketiv/qere (Bereishit 1:1 has none) + ketiv_qere = [ + %{ + word_index: 3, + ketiv: "את", + qere: "אות", + both_preserved: true + } + ] + + context = %{context | ketiv_qere: ketiv_qere} + + input = %{ + source: "test", + verse_scope: @scope, + content: "בראשית ברא אלהים את השמים ואת הארץ" + } + + {:ok, context: context, input: input, ketiv_qere: ketiv_qere} + end + + test "context contains both readings", %{context: context, ketiv_qere: ketiv_qere} do + assert context.ketiv_qere == ketiv_qere + kq = hd(context.ketiv_qere) + assert kq.ketiv == "את" + assert kq.qere == "אות" + end + + test "ketiv/qere preserved through vayomer", %{context: context, input: input} do + result = + input + |> BereishitCycle.tohu(context) + |> BereishitCycle.vayomer(context) + + # vayomer copies ketiv_qere from context into state + assert result.ketiv_qere == context.ketiv_qere + end + + test "ki_tov evaluation preserves both readings", %{context: context, input: input} do + result = + input + |> BereishitCycle.tohu(context) + |> BereishitCycle.vayomer(context) + |> BereishitCycle.havdalah(context) + |> BereishitCycle.kriah(context) + + named_results = result.named_results + + # Specification.evaluate receives the ketiv_qere list + evaluation = Specification.evaluate(named_results, context.ketiv_qere) + + # With both_preserved: true, evaluation should be :tov (not lo_tov) + assert evaluation == :tov + end + + test "collapsed ketiv/qere triggers lo_tov" do + # Simulate a collapsed dual reading + collapsed_kq = [%{word_index: 3, ketiv: "את", qere: "את", both_preserved: false}] + results = [%{shoresh: "ברא"}] + + evaluation = Specification.evaluate(results, collapsed_kq) + assert {:lo_tov, reason} = evaluation + assert reason =~ "ketiv/qere" + end + end + + # ============================================================ + # Test 3: Cloud Obedience (Architecture §6.3) + # ============================================================ + describe "Shema Test 3: Cloud Obedience" do + test "Shabbat cloud state prevents processing" do + {:ok, shabbat, _} = DateTime.from_iso8601("2026-02-28T10:00:00Z") + {:ok, context} = Shema.receive_context_at(@scope, shabbat) + + # Context reports major_transition + assert {:major_transition, :shabbat_rest, _} = context.cloud_state + end + + test "pipeline returns {:transition, :shabbat_rest} on Shabbat — does NOT process" do + {:ok, shabbat, _} = DateTime.from_iso8601("2026-02-28T10:00:00Z") + {:ok, context} = Shema.receive_context_at(@scope, shabbat) + + # Simulate what VavPipeline.process/1 does after context reception + result = + case context.cloud_state do + :moving -> + {:transition, :cloud_lifted} + + {:major_transition, signal, _params} -> + {:transition, signal} + + :cloud_not_visible -> + {:waiting, :cloud_not_visible} + + :camped -> + :would_process + end + + assert {:transition, :shabbat_rest} = result + end + + test "cloud_not_visible triggers waiting" do + # Manually construct context with cloud_not_visible + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + context = %{context | cloud_state: :cloud_not_visible} + + result = + case context.cloud_state do + :cloud_not_visible -> {:waiting, :cloud_not_visible} + _ -> :would_process + end + + assert {:waiting, :cloud_not_visible} = result + end + end + + # ============================================================ + # Test 4: Manna Fidelity (Architecture §6.3) + # ============================================================ + describe "Shema Test 4: Manna Fidelity" do + test "no provision → pipeline returns {:waiting, :no_provision}" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + # No provision file exists in test environment + assert context.daily_provision == :no_provision + + # Simulate VavPipeline.process decision + result = + case context.daily_provision do + :no_provision -> {:waiting, :no_provision} + _provision -> :would_process + end + + assert {:waiting, :no_provision} = result + end + + test "with provision → pipeline proceeds" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + # Inject valid provision + provision = %{ + date: "2026-02-25", + provision: %{ + compute_units: 1000, + memory_mb: 32768, + api_calls_external: 500, + storage_delta_mb: 100, + sanhedrin_panels_max: 5 + }, + type: :regular, + expires_at: DateTime.utc_now() |> DateTime.add(3600), + written_by: "test", + written_at: DateTime.utc_now() + } + + context = %{context | daily_provision: provision} + + result = + case context.daily_provision do + :no_provision -> {:waiting, :no_provision} + _provision -> :would_process + end + + assert result == :would_process + end + end + + # ============================================================ + # Test 5: Context Immutability (Architecture §6.3) + # ============================================================ + describe "Shema Test 5: Context Immutability" do + test "ShemaContext is identical at every pipeline stage" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + input = %{ + source: "test", + verse_scope: @scope, + content: "בראשית ברא אלהים את השמים ואת הארץ" + } + + # Capture context hash before processing + ctx_hash = :erlang.phash2(context) + + # Stage 1: tohu + state1 = BereishitCycle.tohu(input, context) + assert :erlang.phash2(context) == ctx_hash, "Context mutated after tohu" + + # Stage 2: vayomer + state2 = BereishitCycle.vayomer(state1, context) + assert :erlang.phash2(context) == ctx_hash, "Context mutated after vayomer" + + # Stage 3: havdalah + state3 = BereishitCycle.havdalah(state2, context) + assert :erlang.phash2(context) == ctx_hash, "Context mutated after havdalah" + + # Stage 4: kriah + state4 = BereishitCycle.kriah(state3, context) + assert :erlang.phash2(context) == ctx_hash, "Context mutated after kriah" + + # Stage 5: ki_tov + _result = BereishitCycle.ki_tov(state4, context) + assert :erlang.phash2(context) == ctx_hash, "Context mutated after ki_tov" + + # Verify deep equality — not just hash + assert context.verse.ref == "Bereishit 1:1" + assert length(context.teamim_marks) == 7 + assert context.scope == @scope + end + end + + # ============================================================ + # Test 6: Halt Condition Pipeline Integration (Architecture §6.2) + # ============================================================ + describe "Integration: Halt Conditions" do + setup do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + input = %{ + source: "test", + verse_scope: @scope, + content: "בראשית ברא אלהים את השמים ואת הארץ" + } + + {:ok, context: context, input: input} + end + + test "avodah zarah halts pipeline", %{context: context} do + # Construct result data with non-Torah authority in named_results + data = %{ + named_results: [%{authority_source: :claude_model, name: "test", roots: [], index: 0}], + stage: :ki_tov, + evaluation: :tov + } + + result = BereishitCycle.erev_voker({:ok, data}, context) + assert {:halted, :avodah_zarah, reason} = result + assert reason =~ "non-Torah authority" + end + + test "shfichut damim halts pipeline", %{context: context} do + data = %{ + named_results: [%{action: :delete, name: "test", roots: [], index: 0}], + stage: :ki_tov, + evaluation: :tov + } + + result = BereishitCycle.erev_voker({:ok, data}, context) + assert {:halted, :shfichut_damim, reason} = result + assert reason =~ "destructive action" + end + + test "gilui arayot halts pipeline", %{context: context} do + data = %{ + named_results: [ + %{ + boundary_violation: {:spec_interp_merge, "treated interpretation as specification"}, + name: "test", + roots: [], + index: 0 + } + ], + stage: :ki_tov, + evaluation: :tov + } + + result = BereishitCycle.erev_voker({:ok, data}, context) + assert {:halted, :gilui_arayot, reason} = result + assert reason =~ "boundary collapsed" + end + + test "clean results pass halt check", %{context: context} do + data = %{ + named_results: [%{shoresh: "ברא", name: "ברא", roots: [], index: 0}], + stage: :ki_tov, + evaluation: :tov + } + + result = BereishitCycle.erev_voker({:ok, data}, context) + assert {:ok, _} = result + end + + test "HaltConditions.check_all with clean results" do + results = [%{shoresh: "ברא", action: :read, authority_source: :l1}] + assert :clear == HaltConditions.check_all(results) + end + end + + # ============================================================ + # Test 7: Full Pipeline End-to-End (Architecture §6.2) + # ============================================================ + describe "Integration: Full Pipeline" do + test "BereishitCycle.execute produces evaluated output" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + input = %{ + source: "test", + verse_scope: @scope, + content: "בראשית ברא אלהים את השמים ואת הארץ" + } + + result = BereishitCycle.execute(input, context) + + # Should complete — {:ok, data} or {:unresolved, data} + assert elem(result, 0) in [:ok, :unresolved] + + data = elem(result, 1) + + # Output has named_results from kriah + assert is_list(data.named_results) + assert length(data.named_results) > 0 + + # Output has roots_activated from vayomer + assert is_list(data.roots_activated) + assert length(data.roots_activated) > 0 + + # ברא should be among activated roots + bara = Enum.find(data.roots_activated, fn r -> r.shoresh == "ברא" end) + assert bara != nil + end + + test "ShemaContext built before processing" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + scope = @scope + + # Step 1: nishma — hear first + {:ok, context} = Shema.receive_context_at(scope, wednesday) + + # Context has all required fields + assert context.verse != nil + assert is_list(context.teamim_marks) + assert is_list(context.ketiv_qere) + assert is_map(context.root_gematria) + assert context.cloud_state != nil + assert context.daily_provision != nil + assert context.scope == scope + assert context.received_at != nil + end + + test "pipeline does NOT produce {:halted, ...} on clean Bereishit 1:1 input" do + {:ok, wednesday, _} = DateTime.from_iso8601(@wednesday_noon) + {:ok, context} = Shema.receive_context_at(@scope, wednesday) + + input = %{ + source: "l1", + verse_scope: @scope, + content: "בראשית ברא אלהים את השמים ואת הארץ" + } + + result = BereishitCycle.execute(input, context) + + # Must NOT be halted — Bereishit 1:1 is clean Torah text + refute match?({:halted, _, _}, result) + end + end +end