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>
This commit is contained in:
Scott KarpandClaude Opus 4.7 committed 2026-04-26 12:26:49 -04:00
1 parent 5c76a6f42c
commit b1d5ac69e4
4 files changed
+388 -4

No files matched your search

+51
View File
@@ -72,6 +72,57 @@ defmodule VavPipeline.L3.StoreTest do
assert {:ok, %Interpretation{status: "teiku"}} = Store.record_interpretation(attrs)
end
# === COUPLING_TRACE GATE (Pass 2 finding L, Elixir half) ===
# Mirrors the Prolog Form 4 self_grounding halt at the L3 write
# boundary. L3 is append-only — once a trace is written it is
# permanent. Validation MUST fire before Repo.insert so a trace whose
# anchor is not L1-grounded never becomes oral tradition.
test "record_interpretation refuses ungrounded coupling trace (prior_decision)" do
attrs =
Map.put(@valid_interpretation, :coupling_trace, [
"coupled(self, prior_decision(d1))"
])
assert {:error, {:ungrounded_coupling, "coupled(self, prior_decision(d1))"}} =
Store.record_interpretation(attrs)
end
test "record_interpretation refuses mixed trace (one valid + one ungrounded)" do
attrs =
Map.put(@valid_interpretation, :coupling_trace, [
"coupled(root(brk), verse(bereishit, 1, 1))",
"coupled(consensus, system_view(s1))"
])
assert {:error, {:ungrounded_coupling, "coupled(consensus, system_view(s1))"}} =
Store.record_interpretation(attrs)
end
test "record_interpretation accepts empty coupling trace (golden_calf's domain, not Form 4)" do
attrs = Map.put(@valid_interpretation, :coupling_trace, [])
assert {:ok, %Interpretation{coupling_trace: []}} = Store.record_interpretation(attrs)
end
test "record_interpretation accepts mitzvah-anchored trace (Rambam-derived)" do
attrs =
Map.put(@valid_interpretation, :coupling_trace, [
"coupled(domain(creation), mitzvah(5))"
])
assert {:ok, %Interpretation{coupling_trace: ["coupled(domain(creation), mitzvah(5))"]}} =
Store.record_interpretation(attrs)
end
test "record_interpretation accepts via_verse 3-arg trace" do
attrs =
Map.put(@valid_interpretation, :coupling_trace, [
"coupled(root(shm), mitzvah(7), via_verse(devarim, 6, 4))"
])
assert {:ok, %Interpretation{}} = Store.record_interpretation(attrs)
end
# === MINORITY OPINION RECORDING ===
test "record_minority inserts a dissenting opinion" do