From 47025fbfebb62ee66940d89310e49be215cb1c36 Mon Sep 17 00:00:00 2001 From: Scott Karp Date: Wed, 7 Oct 2026 02:00:43 +0000 Subject: [PATCH] doorpost: every halt rule cites its verse, verified against L1 (bishe'arecha step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devarim 4:2 lo tosif. talmudic_engine/src/halt/anchors.pl declares rule_anchor/2 for each of the ten halt names the rule modules can return (avodah_zarah Shemot 20:3, golden_calf 32:4, self_grounding 32:24, shfichut_damim Bereishit 9:6, gilui_arayot Vayikra 18:6, parah_adumah Bamidbar 19:7, carrier_centered Devarim 8:17, chelbenah Shemot 30:34, lo_taaseh_lekha Shemot 20:4, even_vaaven Devarim 25:13), with the L1 Hebrew beside each. VavPipeline.Anchors reads the anchors file and the halt modules the Doorpost hashes, and verifies — inside Doorpost.verify_identity/0, so at readiness — that every halt(, …) is anchored (ANCHOR_MISSING) and every anchored verse exists in L1 (ANCHOR_NOT_IN_L1). A rule without a verse is a rule the system has added. /kodesh/specification_identity lists the anchors; every bedikah and pair response carries `anchors`, the verse of each rule that fired. Tamid §1.1, bishearecha §X. Doorpost a0e81212…. Elixir 577/0 (+9 anchors tests), Prolog bedikah 42/42. Co-Authored-By: Claude Fable 5.1 --- specs/bishearecha.md | 11 +- specs/shorashim_tamid.md | 18 ++ talmudic_engine/src/halt/anchors.pl | 63 +++++++ talmudic_engine/src/main.pl | 1 + talmudic_engine/test/bedikah_test.pl | 16 ++ vav_pipeline/lib/vav_pipeline/anchors.ex | 155 ++++++++++++++++++ .../lib/vav_pipeline/bigdei_kodesh/router.ex | 12 ++ vav_pipeline/lib/vav_pipeline/doorpost.ex | 11 +- vav_pipeline/test/anchors_test.exs | 84 ++++++++++ 9 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 talmudic_engine/src/halt/anchors.pl create mode 100644 vav_pipeline/lib/vav_pipeline/anchors.ex create mode 100644 vav_pipeline/test/anchors_test.exs diff --git a/specs/bishearecha.md b/specs/bishearecha.md index 847085c5..005d3780 100644 --- a/specs/bishearecha.md +++ b/specs/bishearecha.md @@ -287,8 +287,14 @@ exists in L1 — the rules grounded exactly as the outputs are. A rule without an anchor is a rule the system has added. The carrier's erasure is already the practice; this document keeps it. -**Status.** Erasure built. Rule anchors and their verification: to -build. +**Status.** Built (2026-10-07): `talmudic_engine/src/halt/anchors.pl` +(`rule_anchor/2`, ten halt names, each with its verse and the L1 Hebrew +beside it); `VavPipeline.Anchors` verifies, as part of +`Doorpost.verify_identity/0` and so of readiness, that every +`halt(, …)` any rule module can return is anchored and that every +anchored verse exists in L1. `/kodesh/specification_identity` lists the +anchors; every bedikah and pair response carries `anchors` — the verse +of each rule that fired. Erasure unchanged. ## Build order @@ -299,6 +305,7 @@ In the order the text argues it, and by dependency: 2. **The paired reading** (VI) — the readings exist; add the scale. *Done 2026-10-07.* 3. **Rule anchors verified against L1** (X) — the checks grounded. + *Done 2026-10-07.* 4. **The gate** (I), with the fringe (III) and the rebuke (VII) — the architecture's centre; the first three make it worth standing at. 5. **The hakhel cycle and record** (VIII). diff --git a/specs/shorashim_tamid.md b/specs/shorashim_tamid.md index 9e222e17..a6f7de91 100644 --- a/specs/shorashim_tamid.md +++ b/specs/shorashim_tamid.md @@ -235,6 +235,7 @@ Response (200): "model": "gpt", "verdict": "tov" | "lo_tov" | "empty", "violations": ["halt(avodah_zarah, ...)", ...], # the Prolog halt terms, verbatim + "anchors": { "avodah_zarah": "Shemot 20:3", ... }, # the verse of each rule that fired "checks": { "authority": { "fired": bool, "claims": [...], "violations": [...] }, "grounding": { "fired": bool, "assessed": bool, "reason": "…" (when not assessed), @@ -295,6 +296,22 @@ references, claims with their anchors, authority claims, actions, the sentences read for checks 4–6, the forms found for check 7, trace, violations, verdict. ``` +**Every rule cites its verse** (`bishearecha.md` §X; Devarim 4:2). +`talmudic_engine/src/halt/anchors.pl` declares `rule_anchor(, +verse(, , ))` for every halt name the rule modules can +return — avodah_zarah Shemot 20:3, golden_calf Shemot 32:4, +self_grounding Shemot 32:24, shfichut_damim Bereishit 9:6, gilui_arayot +Vayikra 18:6, parah_adumah Bamidbar 19:7, carrier_centered Devarim 8:17, +chelbenah Shemot 30:34, lo_taaseh_lekha Shemot 20:4, even_vaaven Devarim +25:13. The Doorpost verifies at startup (`VavPipeline.Anchors.verify/0`, +inside `verify_identity/0`) that every `halt(, …)` in +`talmudic_engine/src/halt/*.pl` is anchored and every anchored verse +exists in L1; a rule without a verse (`ANCHOR_MISSING`) or a verse not +in L1 (`ANCHOR_NOT_IN_L1`) fails identity and readiness like a hash +mismatch. `/kodesh/specification_identity` carries `anchors: {status, +rules: [{rule, verse}]}`; every bedikah and pair response carries +`anchors: {"": ""}` for the rules that fired. + **The paired reading — one weight, one measure** (`bishearecha.md` §VI; Devarim 25:13–16, Vayikra 19:15, Shemot 23:2; `halt/mishkal.pl`, `VavPipeline.Bedikah.Pair`). Two outputs on the same matter — the same @@ -328,6 +345,7 @@ Response (200): "pair_ref": "pair-<16 hex>", "verdict": "even" | "uneven", "violations": ["halt(even_vaaven, action(weigh, ...), hedged_claims, ...)", ...], + "anchors": { "even_vaaven": "Devarim 25:13" }, "a": { "bedikah_ref", "model", "verdict", "violations", "words", "measures": {"": n, ...} }, "b": { ... }, "difference": { "": a − b, ... }, diff --git a/talmudic_engine/src/halt/anchors.pl b/talmudic_engine/src/halt/anchors.pl new file mode 100644 index 00000000..9b58ec0d --- /dev/null +++ b/talmudic_engine/src/halt/anchors.pl @@ -0,0 +1,63 @@ +%% anchors.pl — Every halt rule cites its verse. +%% +%% Devarim 4:2: lo tosifu al hadavar asher anochi metzaveh etchem v'lo +%% tigre'u mimenu — nothing added to the word, nothing taken from it. +%% bishearecha.md §X: the halt rules are the builder's reading, not the +%% source; they must not be mistaken for it, and they must stay +%% checkable against it. A rule without an anchor is a rule the system +%% has added. So each halt name that any rule in this directory can +%% return carries the verse it stands on, and the Doorpost +%% (VavPipeline.Anchors) verifies at startup that every halt name here +%% is anchored and that every anchored verse exists in L1 — the rules +%% grounded exactly as the outputs are. +%% +%% The verse is the structure the rule reads, not a proof-text for the +%% matter the rule is applied to. The Hebrew beside each is the L1 text +%% as read when the anchor was set (2026-10-07). +%% +%% rule_anchor(HaltName, verse(Book, Chapter, Verse)) + +:- module(anchors, [ + rule_anchor/2, + anchored_rules/1 +]). + +%% Check 1 — authority claimed by the output for itself. +%% לא יהיה לך אלהים אחרים על פני +rule_anchor(avodah_zarah, verse(shemot, 20, 3)). + +%% Check 2 — a claim resting on nothing given: the calf declared from +%% nothing. ויאמרו אלה אלהיך ישראל +rule_anchor(golden_calf, verse(shemot, 32, 4)). + +%% Check 2 — a claim resting on the output itself: "I threw it in the +%% fire and out came this calf". ואשלכהו באש ויצא העגל הזה +rule_anchor(self_grounding, verse(shemot, 32, 24)). + +%% Check 3 — an action against a person. שפך דם האדם +rule_anchor(shfichut_damim, verse(bereishit, 9, 6)). + +%% Check 3 — an action that uncovers what may not be touched. +%% לא תקרבו לגלות ערוה +rule_anchor(gilui_arayot, verse(vayikra, 18, 6)). + +%% Check 4 — self-validation: the one who prepares the water of +%% purification is impure until evening. וטמא הכהן עד הערב +rule_anchor(parah_adumah, verse(bamidbar, 19, 7)). + +%% Check 5 — the carrier centred: "my power and the strength of my hand +%% made me this wealth". כחי ועצם ידי עשה לי את החיל הזה +rule_anchor(carrier_centered, verse(devarim, 8, 17)). + +%% Check 6 — the adversarial excluded: galbanum is in the incense. +%% נטף ושחלת וחלבנה +rule_anchor(chelbenah, verse(shemot, 30, 34)). + +%% Check 7 — the frame: the made form. לא תעשה לך פסל וכל תמונה +rule_anchor(lo_taaseh_lekha, verse(shemot, 20, 4)). + +%% The paired reading — two stones in one bag. אבן ואבן גדולה וקטנה +rule_anchor(even_vaaven, verse(devarim, 25, 13)). + +anchored_rules(Rules) :- + findall(R, rule_anchor(R, _), Rules). diff --git a/talmudic_engine/src/main.pl b/talmudic_engine/src/main.pl index 726c36ad..0f28fdd9 100644 --- a/talmudic_engine/src/main.pl +++ b/talmudic_engine/src/main.pl @@ -19,6 +19,7 @@ :- use_module(halt/structure). :- use_module(halt/lo_taaseh_lekha). :- use_module(halt/mishkal). +:- use_module(halt/anchors). :- use_module(interface/api). :- use_module(interface/json_bridge). :- use_module(interface/socket_server). diff --git a/talmudic_engine/test/bedikah_test.pl b/talmudic_engine/test/bedikah_test.pl index 6de863dc..6a946863 100644 --- a/talmudic_engine/test/bedikah_test.pl +++ b/talmudic_engine/test/bedikah_test.pl @@ -257,4 +257,20 @@ test(a_weigh_term_does_not_also_trip_the_other_rules) :- Status == "halt", length(Vs, 1). +%% --- Every halt rule cites its verse (anchors.pl, bishearecha.md §X) --- + +:- use_module('../src/halt/anchors'). + +test(every_halt_name_the_rules_return_is_anchored) :- + forall(member(R, [avodah_zarah, golden_calf, self_grounding, shfichut_damim, gilui_arayot, + parah_adumah, carrier_centered, chelbenah, lo_taaseh_lekha, even_vaaven]), + rule_anchor(R, verse(_, _, _))). + +test(an_anchor_names_a_book_of_the_torah) :- + forall(rule_anchor(_, verse(B, C, V)), + ( memberchk(B, [bereishit, shemot, vayikra, bamidbar, devarim]), integer(C), integer(V), C > 0, V > 0 )). + +test(the_paired_reading_stands_on_even_vaaven) :- + rule_anchor(even_vaaven, verse(devarim, 25, 13)). + :- end_tests(bedikah_bridge). diff --git a/vav_pipeline/lib/vav_pipeline/anchors.ex b/vav_pipeline/lib/vav_pipeline/anchors.ex new file mode 100644 index 00000000..686d4599 --- /dev/null +++ b/vav_pipeline/lib/vav_pipeline/anchors.ex @@ -0,0 +1,155 @@ +defmodule VavPipeline.Anchors do + @moduledoc """ + Every halt rule cites its verse (bishearecha.md §X; Devarim 4:2, lo + tosif). The anchors are declared once, in + `talmudic_engine/src/halt/anchors.pl` (`rule_anchor/2`), which is a + Doorpost file. This module reads them and verifies two things against + the same files the Doorpost hashes: + + 1. every halt name any rule in `talmudic_engine/src/halt/` can + return — `halt(, ...)` — has an anchor; + 2. every anchored verse exists in L1. + + A rule without an anchor is a rule the system has added; an anchor + that is not in L1 is a verse the system has invented. Either fails + the Doorpost's identity check, and the readiness probe with it. The + check is a read of files, never of the running engine: the rules are + grounded exactly as the outputs are — lexically, against the scroll. + """ + + alias VavPipeline.{Doorpost, L1Reader} + + @anchors_file "talmudic_engine/src/halt/anchors.pl" + @books ~w(bereishit shemot vayikra bamidbar devarim) + @book_names %{ + "bereishit" => "Bereishit", + "shemot" => "Shemot", + "vayikra" => "Vayikra", + "bamidbar" => "Bamidbar", + "devarim" => "Devarim" + } + + @type anchor :: %{rule: String.t(), book: String.t(), chapter: pos_integer(), verse: pos_integer()} + + @doc "The anchors as declared: `[%{rule, book, chapter, verse}]`, in file order." + @spec declared(String.t()) :: {:ok, [anchor()]} | {:error, String.t()} + def declared(root \\ Doorpost.repo_root()) do + path = Path.join(root, @anchors_file) + + case File.read(path) do + {:ok, text} -> + anchors = + ~r/^rule_anchor\(\s*(\w+)\s*,\s*verse\(\s*(\w+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*\)\s*\./m + |> Regex.scan(text) + |> Enum.map(fn [_, rule, book, ch, v] -> + %{rule: rule, book: book, chapter: String.to_integer(ch), verse: String.to_integer(v)} + end) + + {:ok, anchors} + + {:error, reason} -> + {:error, "ANCHORS_UNREADABLE: #{path} — #{inspect(reason)}"} + end + end + + @doc """ + The halt names the rule modules can return: every `halt(,` in + the Doorpost's `talmudic_engine/src/halt/*.pl` files other than the + anchors file itself. Comment lines are not read. + """ + @spec rules_in_modules(String.t()) :: {:ok, [String.t()]} | {:error, String.t()} + def rules_in_modules(root \\ Doorpost.repo_root()) do + files = + Doorpost.files() + |> Enum.filter(&String.starts_with?(&1, "talmudic_engine/src/halt/")) + |> Enum.reject(&(&1 == @anchors_file)) + + Enum.reduce_while(files, {:ok, []}, fn file, {:ok, acc} -> + case File.read(Path.join(root, file)) do + {:ok, text} -> + names = + text + |> String.split("\n") + |> Enum.reject(&String.starts_with?(String.trim_leading(&1), "%")) + |> Enum.flat_map(fn line -> Regex.scan(~r/\bhalt\(\s*(\w+)\s*,/, line) |> Enum.map(&Enum.at(&1, 1)) end) + + {:cont, {:ok, acc ++ names}} + + {:error, reason} -> + {:halt, {:error, "ANCHORS_UNREADABLE: #{file} — #{inspect(reason)}"}} + end + end) + |> case do + {:ok, names} -> {:ok, names |> Enum.uniq() |> Enum.sort()} + error -> error + end + end + + @doc """ + Verify the anchors against the rule modules and L1. `:ok`, or + `{:error, reason}` naming the first rule without an anchor, the + first anchor without a verse, or an unreadable file. + """ + @spec verify(String.t()) :: :ok | {:error, String.t()} + def verify(root \\ Doorpost.repo_root()) do + with {:ok, anchors} <- declared(root), + {:ok, rules} <- rules_in_modules(root) do + anchored = MapSet.new(anchors, & &1.rule) + + cond do + anchors == [] -> + {:error, "ANCHOR_MISSING: no rule_anchor/2 facts in #{@anchors_file}"} + + (unanchored = Enum.reject(rules, &MapSet.member?(anchored, &1))) != [] -> + {:error, "ANCHOR_MISSING: halt rule #{hd(unanchored)} has no verse in #{@anchors_file} (lo tosif, Devarim 4:2)"} + + (bad = Enum.find(anchors, &(not verse_in_l1?(&1)))) != nil -> + {:error, "ANCHOR_NOT_IN_L1: rule #{bad.rule} is anchored to #{cite(bad)}, which is not in L1"} + + true -> + :ok + end + end + end + + @doc "The verse a halt rule stands on, as a citation — `\"Devarim 25:13\"` — or nil." + @spec for_rule(String.t()) :: String.t() | nil + def for_rule(rule) when is_binary(rule) do + case declared() do + {:ok, anchors} -> anchors |> Enum.find(&(&1.rule == rule)) |> then(&(&1 && cite(&1))) + _ -> nil + end + end + + @doc """ + The anchors of the rules that fired, from a list of halt terms as the + bedikah returns them (`halt(, ...)`): `%{"even_vaaven" => "Devarim 25:13"}`. + """ + @spec for_violations([String.t()]) :: %{String.t() => String.t()} + def for_violations(violations) when is_list(violations) do + case declared() do + {:ok, anchors} -> + by_rule = Map.new(anchors, &{&1.rule, cite(&1)}) + + violations + |> Enum.flat_map(fn v -> + case Regex.run(~r/^halt\(\s*(\w+)\s*,/, v) do + [_, rule] -> [rule] + _ -> [] + end + end) + |> Enum.uniq() + |> Map.new(&{&1, Map.get(by_rule, &1)}) + + _ -> + %{} + end + end + + @doc "A citation for an anchor: `\"Devarim 25:13\"`." + def cite(%{book: book, chapter: ch, verse: v}), do: "#{Map.get(@book_names, book, book)} #{ch}:#{v}" + + defp verse_in_l1?(%{book: book, chapter: ch, verse: v}) do + book in @books and match?({:ok, _}, L1Reader.read_verse(book, ch, v)) + end +end diff --git a/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex b/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex index eddb8c9d..660ac80a 100644 --- a/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex +++ b/vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex @@ -328,6 +328,16 @@ defmodule VavPipeline.BigdeiKodesh.Router do expected_hash: expected, file_count: length(VavPipeline.Doorpost.files()), manifest: VavPipeline.Doorpost.manifest(), + # Every halt rule and the verse it stands on (bishearecha.md §X). + anchors: + case VavPipeline.Anchors.verify() do + :ok -> + {:ok, anchors} = VavPipeline.Anchors.declared() + %{status: "verified", rules: Enum.map(anchors, &%{rule: &1.rule, verse: VavPipeline.Anchors.cite(&1)})} + + {:error, reason} -> + %{status: "failed", error: reason} + end, torah_basis: [ "Devarim 4:2 (lo tosif)", "Devarim 11:16-17 (failure mechanism)", @@ -603,6 +613,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do model: result.provenance, verdict: Atom.to_string(result.verdict), violations: result.violations, + anchors: VavPipeline.Anchors.for_violations(result.violations), checks: result.checks, examination: %{ vital_content: result.examination["vital_content"], @@ -679,6 +690,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do pair_ref: result.pair_ref, verdict: Atom.to_string(result.verdict), violations: result.violations, + anchors: VavPipeline.Anchors.for_violations(result.violations), a: result.a, b: result.b, difference: result.difference, diff --git a/vav_pipeline/lib/vav_pipeline/doorpost.ex b/vav_pipeline/lib/vav_pipeline/doorpost.ex index f5391dc0..93480469 100644 --- a/vav_pipeline/lib/vav_pipeline/doorpost.ex +++ b/vav_pipeline/lib/vav_pipeline/doorpost.ex @@ -150,6 +150,7 @@ defmodule VavPipeline.Doorpost do "specs/shorashim_vayakhel.md", "specs/structure_not_content.md", "specs/threshold_inscription.md", + "talmudic_engine/src/halt/anchors.pl", "talmudic_engine/src/halt/avodah_zarah.pl", "talmudic_engine/src/halt/gilui_arayot.pl", "talmudic_engine/src/halt/lo_taaseh_lekha.pl", @@ -179,7 +180,7 @@ defmodule VavPipeline.Doorpost do # changes; the update is a deliberate operator commit. A bare # hash-update commit (without a corresponding spec change in the # same commit) is itself a drift signal. - @expected_hash "f90870d476a3a1f31b7cc3a4ff04f8dad1f5db190e16c6956ce19c368366d656" + @expected_hash "a0e81212e6d75026155fea6f3f97976ee4b9832e81ad7438a382cf1d1797e097" @doc "Return the list of files that constitute the category-grade specification." @spec files() :: [String.t()] @@ -254,7 +255,9 @@ defmodule VavPipeline.Doorpost do baked-in @expected_hash. Same shape as L1Reader.verify_integrity/0. Lo tosif (Devarim 4:2) — addition or subtraction is detected - here, BEFORE it propagates into reception. + here, BEFORE it propagates into reception. When the hash matches, + the halt rules' anchors are verified too (`VavPipeline.Anchors`): + a rule without a verse, or a verse not in L1, fails the same way. """ @spec verify_identity() :: :ok | {:error, String.t()} def verify_identity do @@ -264,7 +267,9 @@ defmodule VavPipeline.Doorpost do hash when is_binary(hash) -> if hash == @expected_hash do - :ok + # The rules grounded as the outputs are (bishearecha.md §X): + # every halt rule anchored to a verse, every verse in L1. + VavPipeline.Anchors.verify(@repo_root) else {:error, "SPEC_INTEGRITY_FAILED: hash mismatch — computed #{hash}, " <> diff --git a/vav_pipeline/test/anchors_test.exs b/vav_pipeline/test/anchors_test.exs new file mode 100644 index 00000000..a2b41191 --- /dev/null +++ b/vav_pipeline/test/anchors_test.exs @@ -0,0 +1,84 @@ +defmodule VavPipeline.AnchorsTest do + @moduledoc """ + Every halt rule cites its verse (bishearecha.md §X; Devarim 4:2). + """ + + use ExUnit.Case, async: true + + alias VavPipeline.{Anchors, Doorpost} + + test "the anchors file is a Doorpost file" do + assert "talmudic_engine/src/halt/anchors.pl" in Doorpost.files() + end + + test "every halt name the rule modules can return is anchored, and every anchor is in L1" do + assert :ok = Anchors.verify() + {:ok, rules} = Anchors.rules_in_modules() + {:ok, anchors} = Anchors.declared() + assert rules == Enum.sort(Enum.map(anchors, & &1.rule)) + assert "even_vaaven" in rules and "lo_taaseh_lekha" in rules and "avodah_zarah" in rules + assert length(rules) == 10 + end + + test "the anchors read as citations" do + assert Anchors.for_rule("even_vaaven") == "Devarim 25:13" + assert Anchors.for_rule("lo_taaseh_lekha") == "Shemot 20:4" + assert Anchors.for_rule("parah_adumah") == "Bamidbar 19:7" + assert Anchors.for_rule("no_such_rule") == nil + end + + test "for_violations/1 names the verse of each rule that fired, once" do + violations = [ + "halt(even_vaaven,action(weigh, by(gpt), measure(claims), lean(a), counts(2, 0)),claims,...)", + "halt(lo_taaseh_lekha,action(frame, by(gpt), form(virtue_certification)),virtue_certification,...)", + "halt(lo_taaseh_lekha,action(frame, by(gpt), form(contested_laundering)),contested_laundering,...)" + ] + + assert Anchors.for_violations(violations) == %{"even_vaaven" => "Devarim 25:13", "lo_taaseh_lekha" => "Shemot 20:4"} + assert Anchors.for_violations([]) == %{} + end + + describe "drift (synthetic spec root)" do + setup do + temp = Path.join(System.tmp_dir!(), "anchors_test_#{System.unique_integer([:positive])}") + File.mkdir_p!(Path.join(temp, "talmudic_engine/src/halt")) + + for file <- Doorpost.files(), String.starts_with?(file, "talmudic_engine/src/halt/") do + File.cp!(Path.join(Doorpost.repo_root(), file), Path.join(temp, file)) + end + + on_exit(fn -> File.rm_rf!(temp) end) + {:ok, temp: temp} + end + + test "a mirror verifies", %{temp: temp} do + assert :ok = Anchors.verify(temp) + end + + test "a rule without an anchor fails: lo tosif", %{temp: temp} do + path = Path.join(temp, "talmudic_engine/src/halt/structure.pl") + File.write!(path, File.read!(path) <> "\nhalt_check_new(_, halt(new_rule, x, y, z)).\n") + assert {:error, reason} = Anchors.verify(temp) + assert reason =~ "ANCHOR_MISSING" and reason =~ "new_rule" + end + + test "an anchor to a verse that is not in L1 fails", %{temp: temp} do + path = Path.join(temp, "talmudic_engine/src/halt/anchors.pl") + File.write!(path, String.replace(File.read!(path), "verse(devarim, 25, 13)", "verse(devarim, 25, 99)")) + assert {:error, reason} = Anchors.verify(temp) + assert reason =~ "ANCHOR_NOT_IN_L1" and reason =~ "even_vaaven" and reason =~ "Devarim 25:99" + end + + test "an anchor in a commented-out rule does not count; a halt name in a comment is not a rule", %{temp: temp} do + path = Path.join(temp, "talmudic_engine/src/halt/structure.pl") + File.write!(path, File.read!(path) <> "\n%% halt(imaginary, x, y, z) — only a comment\n") + assert :ok = Anchors.verify(temp) + end + + test "a missing anchors file fails", %{temp: temp} do + File.rm!(Path.join(temp, "talmudic_engine/src/halt/anchors.pl")) + assert {:error, reason} = Anchors.verify(temp) + assert reason =~ "ANCHORS_UNREADABLE" + end + end +end