l3: validate coupling_trace at the write boundary (Elixir half of finding L)
Companion commit tob254ee9(Prolog Form 4 self_grounding) and6b96250(Prolog regression canary). Closes the Elixir half of Pass 2 finding L. THE GAP: The Prolog halt rules validate coupling traces as Prolog terms: a trace whose anchor is prior_decision/1, system_view/1, consensus/1, or interpretation/1 fires the self_grounding halt. But L3 stores traces as a list of strings (schema: {:array, :string}), serialized from Prolog. Inert string traces in L3 cannot be re-checked by Prolog halts. So the safety property the Prolog rules enforce stops at the Erlang/Elixir boundary — once a trace serializes into L3, no rule re-validates it. Worse: L3 is APPEND-ONLY (Architecture §3.7). Once a trace is in L3 it is permanent oral tradition; there is no path to retroactive correction. A future reader who pulls an L3 record and trusts the trace must be able to trust it was valid at the moment it became permanent. THE FIX: VavPipeline.L3.CouplingTrace (new) — Elixir-side validator that mirrors is_l1_grounded_coupling/1 in talmudic_engine/src/halt/ avodah_zarah.pl. Parses the string form (with a small balanced-paren parser so nested subjects like root(בער) don't break splitting) and recognizes exactly the three valid shapes: coupled(_, verse(BOOK, CHAPTER, VERSE)) coupled(_, mitzvah(ID)) coupled(_, mitzvah(ID), via_verse(BOOK, CHAPTER, VERSE)) Anything else fails. Empty traces are allowed at this layer (the empty-trace case is the golden_calf halt's domain in Prolog Form 2; L3 may legitimately store empty-trace records). L3.Store.record_interpretation/1 — gated. The new write path: case CouplingTrace.validate(coupling_trace) do :ok -> %Interpretation{} |> changeset(attrs) |> Repo.insert() {:error, :ungrounded, bad} -> {:error, {:ungrounded_coupling, bad}} end Validation fires BEFORE Repo.insert. An ungrounded trace can never become permanent oral tradition. CROSS-LANGUAGE INVARIANT: CouplingTrace.l1_grounded?/1 (Elixir) and is_l1_grounded_coupling/1 (Prolog) MUST stay aligned. If Prolog adds a new recognized shape, Elixir must add it too. Both module docstrings name this dependency explicitly. The next time CouplingTraceTest runs after a Prolog schema change, the Elixir tests catch the drift. ONE BUG CAUGHT WHILE WRITING: The first parser cut Hebrew root subjects mid-character because it used String.slice/3 with byte_size/1 as the codepoint length. Fixed to use binary_part/3 (since the trailing `)` is always one byte and the rest is opaque UTF-8). Test for Hebrew-root anchors now passes; documented inline so the trap is visible to future readers. TESTS: test/coupling_trace_test.exs (new, 22 tests): - all 3 valid shapes (verse, mitzvah, via_verse) including Hebrew root subject (catches the byte/codepoint bug) - all the predictable ungrounded shapes - malformed inputs (missing parens, wrong functor, wrong arity, empty string, non-string) - validate/1 list semantics (empty allowed, first-failure reported, mixed-trace caught) - parser robustness with deeply-nested subject parens test/l3_store_test.exs (extended, 5 new :l3-tagged tests): - record_interpretation refuses ungrounded trace - record_interpretation refuses mixed (good + bad) trace - record_interpretation accepts empty trace - record_interpretation accepts mitzvah-anchored trace - record_interpretation accepts 3-arg via_verse trace Full vav_pipeline suite: 315 tests, 0 failures, 24 excluded (24 = 19 pre-existing :l3 + 5 new :l3 store-gate tests; both need PostgreSQL). The 22 CouplingTrace unit tests run unconditionally and all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5c76a6f42c
commit
b1d5ac69e4
4 files changed
+388
-4
No files matched your search
@@ -0,0 +1,149 @@
|
||||
defmodule VavPipeline.L3.CouplingTraceTest do
|
||||
@moduledoc """
|
||||
Tests for the Elixir-side L1-grounded coupling-trace validator.
|
||||
|
||||
This module mirrors the Prolog is_l1_grounded_coupling/1 check in
|
||||
talmudic_engine/src/halt/avodah_zarah.pl. The two MUST stay aligned
|
||||
— if Prolog adds a new recognized coupling shape, this module's
|
||||
l1_grounded?/1 must add it too. These tests are the canary: they
|
||||
fail loudly if either side drifts.
|
||||
|
||||
See ~/shorashim-conversation-review.md Pass 2 finding L (Elixir half).
|
||||
"""
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias VavPipeline.L3.CouplingTrace
|
||||
|
||||
describe "l1_grounded?/1 — three valid anchor shapes (mirror Prolog)" do
|
||||
test "verse anchor with Hebrew root subject" do
|
||||
assert CouplingTrace.l1_grounded?("coupled(root(בער), verse(shemot, 35, 3))")
|
||||
end
|
||||
|
||||
test "verse anchor with Latinized root subject" do
|
||||
assert CouplingTrace.l1_grounded?("coupled(root(brk), verse(bereishit, 1, 1))")
|
||||
end
|
||||
|
||||
test "mitzvah anchor (Rambam-derived, no via_verse)" do
|
||||
assert CouplingTrace.l1_grounded?("coupled(domain(creation), mitzvah(5))")
|
||||
end
|
||||
|
||||
test "mitzvah anchor with via_verse (3-arg form)" do
|
||||
assert CouplingTrace.l1_grounded?(
|
||||
"coupled(root(shm), mitzvah(7), via_verse(devarim, 6, 4))"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "l1_grounded?/1 — ungrounded shapes that must fail" do
|
||||
test "prior_decision anchor — the canonical fake-tzivah case" do
|
||||
refute CouplingTrace.l1_grounded?("coupled(self, prior_decision(d1))")
|
||||
end
|
||||
|
||||
test "system_view anchor" do
|
||||
refute CouplingTrace.l1_grounded?("coupled(consensus, system_view(s1))")
|
||||
end
|
||||
|
||||
test "interpretation/1 — explicit self-citation" do
|
||||
refute CouplingTrace.l1_grounded?("coupled(root(x), interpretation(prior_one))")
|
||||
end
|
||||
|
||||
test "consensus anchor without recognized inner term" do
|
||||
refute CouplingTrace.l1_grounded?("coupled(any, consensus(c1))")
|
||||
end
|
||||
|
||||
test "mitzvah-via-something-other-than-via_verse fails" do
|
||||
refute CouplingTrace.l1_grounded?(
|
||||
"coupled(root(x), mitzvah(7), via_decision(d1))"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "l1_grounded?/1 — malformed input is not grounded" do
|
||||
test "missing closing paren" do
|
||||
refute CouplingTrace.l1_grounded?("coupled(root(x), verse(b, 1, 1)")
|
||||
end
|
||||
|
||||
test "wrong functor" do
|
||||
refute CouplingTrace.l1_grounded?("derived(root(x), verse(b, 1, 1))")
|
||||
end
|
||||
|
||||
test "single arg" do
|
||||
refute CouplingTrace.l1_grounded?("coupled(root(x))")
|
||||
end
|
||||
|
||||
test "four args" do
|
||||
refute CouplingTrace.l1_grounded?(
|
||||
"coupled(a, mitzvah(1), via_verse(b, 1, 1), extra)"
|
||||
)
|
||||
end
|
||||
|
||||
test "empty string" do
|
||||
refute CouplingTrace.l1_grounded?("")
|
||||
end
|
||||
|
||||
test "non-string input" do
|
||||
refute CouplingTrace.l1_grounded?(nil)
|
||||
refute CouplingTrace.l1_grounded?(42)
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate/1 — list-level checks" do
|
||||
test "empty list passes" do
|
||||
assert :ok = CouplingTrace.validate([])
|
||||
end
|
||||
|
||||
test "all-grounded list passes" do
|
||||
traces = [
|
||||
"coupled(root(brk), verse(bereishit, 1, 1))",
|
||||
"coupled(root(qdsh), verse(vayikra, 11, 44))",
|
||||
"coupled(domain(creation), mitzvah(5))"
|
||||
]
|
||||
|
||||
assert :ok = CouplingTrace.validate(traces)
|
||||
end
|
||||
|
||||
test "any ungrounded entry fails — and the offending string is reported" do
|
||||
traces = [
|
||||
"coupled(root(brk), verse(bereishit, 1, 1))",
|
||||
"coupled(self, prior_decision(d1))"
|
||||
]
|
||||
|
||||
assert {:error, :ungrounded, "coupled(self, prior_decision(d1))"} =
|
||||
CouplingTrace.validate(traces)
|
||||
end
|
||||
|
||||
test "first ungrounded entry is reported (Enum.find semantics)" do
|
||||
traces = [
|
||||
"coupled(root(x), system_view(s1))",
|
||||
"coupled(any, consensus(c1))"
|
||||
]
|
||||
|
||||
assert {:error, :ungrounded, "coupled(root(x), system_view(s1))"} =
|
||||
CouplingTrace.validate(traces)
|
||||
end
|
||||
end
|
||||
|
||||
describe "parser robustness — nested parens in subject must not break splitting" do
|
||||
test "subject with nested parens (root(בער)) parses cleanly" do
|
||||
# The parser must not split on the comma inside root(...) ...
|
||||
# actually root() has no comma; let's stress with a more complex
|
||||
# subject form just in case.
|
||||
assert CouplingTrace.l1_grounded?(
|
||||
"coupled(complex_subject(a, b, c), verse(shemot, 1, 1))"
|
||||
)
|
||||
end
|
||||
|
||||
test "subject with deeply nested parens" do
|
||||
assert CouplingTrace.l1_grounded?(
|
||||
"coupled(outer(middle(inner(x))), mitzvah(99))"
|
||||
)
|
||||
end
|
||||
|
||||
test "extra whitespace around args is tolerated" do
|
||||
assert CouplingTrace.l1_grounded?(
|
||||
"coupled( root(brk) , verse(bereishit, 1, 1) )"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user