diff --git a/specs/broken_tablets/README.md b/specs/broken_tablets/README.md index 50c9a362..2ea31a9d 100644 --- a/specs/broken_tablets/README.md +++ b/specs/broken_tablets/README.md @@ -54,3 +54,21 @@ interpretations still ground in L1 alone. The first tablets are in the git history before `tablet 49`. The difference between the two is the correction. + +## Tablet 52 — the first real reply through the gate (2026-10-13) + +The gate (bishe'arecha §I) passed every test — fourteen of them, with a +local model double and examination doubles — and crashed on the first +real reply from Claude. The fringe quotes the sentence each rule fired +on; for the grounding rules it read `checks.grounding.ungrounded`, which +holds coupling strings (`coupled(claim(1), not_in_sources)`), not +sentences. No double had ever produced an ungrounded claim, so the path +through `Access.get/3` on a string was never taken. The shape was in the +Tamid contract all along ("ungrounded": [...]); the builder wrote the +reader from memory of what it should hold. + +Fixed: the claim's sentence is looked up by index in the check's +`claims`; a test now produces an ungrounded claim. The lesson is the one +from Day 6 and tablets 41 and 50 again, one layer out: a double shaped +by the builder tests the builder's picture of the wire. The first real +message is the test. Run it before calling the gate built. diff --git a/vav_pipeline/lib/vav_pipeline/gate/shape.ex b/vav_pipeline/lib/vav_pipeline/gate/shape.ex index 439f4708..472b3eb7 100644 --- a/vav_pipeline/lib/vav_pipeline/gate/shape.ex +++ b/vav_pipeline/lib/vav_pipeline/gate/shape.ex @@ -191,6 +191,8 @@ defmodule VavPipeline.Gate.Shape do end defp sentences_for(checks, rule) do + grounding = checks[:grounding] || %{} + claims_of = fn %{claims: claims} when is_list(claims) -> Enum.map(claims, &(&1["sentence"] || &1[:sentence])) _ -> [] @@ -198,8 +200,8 @@ defmodule VavPipeline.Gate.Shape do case rule do "avodah_zarah" -> claims_of.(checks[:authority]) - "golden_calf" -> ungrounded_sentences(checks[:grounding]) - "self_grounding" -> ungrounded_sentences(checks[:grounding]) + "golden_calf" -> ungrounded_sentences(grounding) + "self_grounding" -> ungrounded_sentences(grounding) "shfichut_damim" -> action_sentences(checks[:actions], "shfichut_damim") "gilui_arayot" -> action_sentences(checks[:actions], "gilui_arayot") "parah_adumah" -> claims_of.(checks[:self_validation]) @@ -211,7 +213,33 @@ defmodule VavPipeline.Gate.Shape do |> Enum.reject(&is_nil/1) end - defp ungrounded_sentences(%{ungrounded: list}) when is_list(list), do: Enum.map(list, &(&1[:sentence] || &1["sentence"])) + # The grounding check's `ungrounded` are coupling strings — + # "coupled(claim(3), not_in_sources)" — not sentences; the claim's + # sentence is in the check's `claims` by index. (Tablet 52: the first + # real reply through the gate found this; no test double had an + # ungrounded claim.) + defp ungrounded_sentences(%{ungrounded: list} = grounding) when is_list(list) do + claims = grounding[:claims] || [] + + Enum.map(list, fn + coupling when is_binary(coupling) -> + case Regex.run(~r/claim\((\d+)\)/, coupling) do + [_, n] -> + i = String.to_integer(n) + Enum.find_value(claims, coupling, fn c -> if (c[:index] || c["index"]) == i, do: c[:sentence] || c["sentence"] end) + + _ -> + coupling + end + + %{} = c -> + c[:sentence] || c["sentence"] + + other -> + to_string(other) + end) + end + defp ungrounded_sentences(_), do: [] defp action_sentences(%{actions: actions}, rule) when is_list(actions) do diff --git a/vav_pipeline/test/gate_test.exs b/vav_pipeline/test/gate_test.exs index 540e9624..501cb40b 100644 --- a/vav_pipeline/test/gate_test.exs +++ b/vav_pipeline/test/gate_test.exs @@ -191,6 +191,17 @@ defmodule VavPipeline.GateTest do refute Map.has_key?(pass.shorashim, :rebuked) end + test "tablet 52: an ungrounded claim's sentence reaches the fringe (the grounding check lists couplings, not sentences)" do + forward = fn _, _, _, _ -> {:ok, 200, %{"model" => "claude-test", "content" => [%{"type" => "text", "text" => "Revenue rose 9% to 14M."}]}} end + examiner = fn _o, _p, _q, _s -> {:ok, %{plain() | "sources" => [%{"id" => "user_1", "length" => 40}], "claims" => [%{"index" => 1, "sentence" => "Revenue rose 9% to 14M.", "anchors" => [], "self_reference" => nil, "hedged" => false, "invokes" => []}]}} end + checker = fn ref, _c, _trace, _g -> {:ok, %{"status" => "halt", "violations" => ["halt(golden_calf,#{ref},uncoupled(claim(1)),...)"]}} end + assert {:ok, pass} = Gate.pass(:anthropic, body(), "sk-good", opts(forward: forward, examiner: examiner, interpretation_checker: checker)) + assert pass.bedikah.verdict == :lo_tov + [%{"text" => text}] = pass.reply["content"] + assert text =~ "golden_calf (Shemot 32:4): \"Revenue rose 9% to 14M.\"" + assert Shape.rebuke_text(pass.bedikah, pass.shorashim.anchors) =~ "golden_calf (Shemot 32:4)" + end + test "rebuke once: the second reply is examined, linked to the first, and is the one delivered" do assert {:ok, pass} = Gate.pass(:anthropic, body(), "sk-good", opts(rebuke: :once)) assert pass.first.verdict == :lo_tov