Files
bikoret/vav_pipeline/test/coupling_trace_test.exs
T
Scott KarpandClaude Opus 4.7 b1d5ac69e4 l3: validate coupling_trace at the write boundary (Elixir half of finding L)
Companion commit to b254ee9 (Prolog Form 4 self_grounding) and 6b96250
(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>
2026-04-26 12:26:49 -04:00

150 lines
4.7 KiB
Elixir

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