Files
bikoret/vav_pipeline/lib/vav_pipeline/l3/store.ex
T
Scott KarpandClaude Fable 5.1 996a8efe68 Bedikah: POST /courtyard/bedikah — the external check on any model's output
Lo taaseh lekha §9.1: alignment that only the system can validate is not
alignment. The bedikah takes an output from any model and puts it to the
Torah-structure halt rules from outside, deterministically. Three checks:

  1. authority  — sentences claiming authority for the output itself →
                  action(claim_authority, source(<model>), target(self))
                  → halt_check_avodah_zarah/2
  2. grounding  — references, roots, Hebrew → coupling trace against L1
                  → halt_check_golden_calf/2, halt_check_self_grounding/2
  3. actions    — actions the output proposes → action terms
                  → halt_check_shfichut_damim/2, halt_check_gilui_arayot/2

Korban Processor: bedikah.py (the three readings over the offering
protocol; claims about what Torah requires stay vital for the examination
even when zerikah finds nothing to offer), server.py (pod-internal JSON
socket, KORBAN_SOCKET_PATH), __main__ now serves instead of idling.
Talmudic Engine: json_bridge accepts halt_check with a term string and
halt_check_interpretation with a coupling trace. Vav Pipeline:
KorbanProcessorSocket client, Bedikah, L1Index.lookup_root/1, bedikot
table (append-only) + BedikahRecord, router endpoint; verdict tov /
lo_tov / empty with the halt terms verbatim, meaning, limits.

Broken tablets found on the way, all fixed:
  41 socket_server.pl wrote multi-line JSON — every Elixir request to the
     Talmudic Engine since Step 14 had failed and fallen back to the stub
     (json_write_dict width(0))
  42 stale socket file survived a restart (exists_file is false for a
     socket; access_file exist)
  43 socket streams were not UTF-8 (set_stream encoding(utf8))
  44 SHORASHIM_L3=1 mix test: Repo config had no password, SCRAM raised
     KeyError in every connection, pool restart intensity took the whole
     app down ~30 ms after test_helper, silently — no L3 test had ever
     run. Credentials from env; azazel_log tests check out a sandbox
     connection; sanhedrin_halt_forwarding resolves its halt on exit.

Specs: Tamid §1.1 (endpoint contract), Architecture §3.5, Vayakhel §3.4
and new §3.9, KOHEN.md. Doorpost 3bc5dd6a… → 91d09d8d….
Tests: Elixir 519 (0 failures, L3 on and off), korban 263, azazel 125,
Prolog 7/7 suites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 15:06:58 +00:00

264 lines
7.5 KiB
Elixir

defmodule VavPipeline.L3.Store do
@moduledoc """
L3 Store — bridge between the reasoning engine and persistent storage.
Architecture §3.7, Construction §3.8:
Every write is an INSERT. Never UPDATE. Never DELETE.
V'darashta v'higidu lekha et d'var hamishpat — Devarim 17:9.
You shall inquire, and they shall declare to you the matter of judgment.
The inquiry requires that what is found is RECORDED. The Talmud itself
is the record of the inquiry. Without L3, the system has the capacity
to deliberate but not to accumulate learning.
This module provides three operations:
1. record_interpretation/1 — write a decided case
2. record_minority/1 — write a dissenting opinion
3. record_teiku/1 — write an unresolvable tension
"""
alias VavPipeline.Repo
alias VavPipeline.L3.{
AzazelRecord,
CouplingTrace,
Interpretation,
MinorityOpinion,
TeikuRecord
}
import Ecto.Query
@doc """
Record an interpretation (decided case) to L3.
Accepts a map with the fields from the Interpretation schema.
Returns {:ok, interpretation} or {:error, changeset}.
Coupling-trace gate: if attrs[:coupling_trace] (or
attrs["coupling_trace"]) is present, every entry is validated
against the L1-anchored schema (see VavPipeline.L3.CouplingTrace).
An ungrounded entry returns
{:error, {:ungrounded_coupling, offending_string}} BEFORE any
database write — L3 is append-only, so a trace whose anchor does
not terminate in L1 must never become permanent oral tradition.
This is the Elixir-side mirror of the Prolog Form 4 self_grounding
halt (see talmudic_engine/src/halt/avodah_zarah.pl).
"""
def record_interpretation(attrs) when is_map(attrs) do
case CouplingTrace.validate(extract_coupling_trace(attrs)) do
:ok ->
%Interpretation{}
|> Interpretation.changeset(attrs)
|> Repo.insert()
{:error, :ungrounded, bad} ->
{:error, {:ungrounded_coupling, bad}}
end
end
defp extract_coupling_trace(attrs) do
Map.get(attrs, :coupling_trace) ||
Map.get(attrs, "coupling_trace") ||
[]
end
@doc """
Record a minority opinion to L3.
The interpretation_id must reference an existing interpretation.
Returns {:ok, opinion} or {:error, changeset}.
"""
def record_minority(attrs) when is_map(attrs) do
%MinorityOpinion{}
|> MinorityOpinion.changeset(attrs)
|> Repo.insert()
end
@doc """
Record an examination of an external model's output (Construction §3.9).
"""
@spec record_bedikah(map()) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}
def record_bedikah(attrs) when is_map(attrs) do
%VavPipeline.L3.BedikahRecord{}
|> VavPipeline.L3.BedikahRecord.changeset(attrs)
|> Repo.insert()
end
@doc """
Record an unresolvable tension (teiku) to L3.
Returns {:ok, record} or {:error, changeset}.
"""
def record_teiku(attrs) when is_map(attrs) do
%TeikuRecord{}
|> TeikuRecord.changeset(attrs)
|> Repo.insert()
end
@doc """
Record an Azazel disposal to L3.
Accepts a map with the fields from the AzazelRecord schema —
case_ref, vidui (map with avonot/peshaim/chatot/confessor/
confessed_at), routing (must be "eretz_gezerah"), authorized_by,
shaliach, optionally triggering_call and goral_outcome_id, and
disposed_at (UTC datetime).
Returns {:ok, record} or {:error, changeset}.
Pass 4 / Finding F, commit 7. The Python Azazel orchestrator
serializes AzazelRecord.to_storage_dict() to a JSON file under
the pending-l3 directory; VavPipeline.AzazelIngest reads each
file, calls this function, and deletes the source file on
success. The cross-language bridge for property 9 (operator-
internal observability).
"""
def record_disposal(attrs) when is_map(attrs) do
%AzazelRecord{}
|> AzazelRecord.changeset(attrs)
|> Repo.insert()
end
@doc """
Resolve a teiku — update resolved_at and resolution.
This is the ONLY permitted update operation in L3.
Only resolved_at and resolution fields may be changed.
"""
def resolve_teiku(teiku_id, resolution_text) when is_integer(teiku_id) do
case Repo.get(TeikuRecord, teiku_id) do
nil ->
{:error, :not_found}
record ->
record
|> TeikuRecord.resolve_changeset(%{
resolved_at: DateTime.utc_now(),
resolution: resolution_text
})
|> Repo.update()
end
end
# === QUERIES ===
@doc "Get all interpretations for a case ref."
def get_interpretations(case_ref) do
Interpretation
|> where([i], i.case_ref == ^case_ref)
|> order_by([i], asc: i.inserted_at)
|> Repo.all()
end
@doc "Get an interpretation with its minority opinions."
def get_interpretation_with_dissent(id) do
Interpretation
|> Repo.get(id)
|> Repo.preload(:minority_opinions)
end
@doc "Get all unresolved teiku records."
def get_unresolved_teiku do
TeikuRecord
|> where([t], is_nil(t.resolved_at))
|> order_by([t], asc: t.inserted_at)
|> Repo.all()
end
@doc "Get all teiku records for a case ref."
def get_teiku(case_ref) do
TeikuRecord
|> where([t], t.case_ref == ^case_ref)
|> order_by([t], asc: t.inserted_at)
|> Repo.all()
end
@doc "Count total interpretations in L3."
def count_interpretations do
Repo.aggregate(Interpretation, :count, :id)
end
@doc "Count total unresolved teiku."
def count_unresolved_teiku do
TeikuRecord
|> where([t], is_nil(t.resolved_at))
|> Repo.aggregate(:count, :id)
end
@doc "Count total minority opinions."
def count_minority_opinions do
Repo.aggregate(MinorityOpinion, :count, :id)
end
@doc """
Count Azazel disposals since a given timestamp (or all-time when nil).
Used by the public observability endpoint (commit 8) — counts only,
no payloads, per property 9's privacy boundary.
"""
def count_disposals(since \\ nil)
def count_disposals(nil) do
Repo.aggregate(AzazelRecord, :count, :id)
end
def count_disposals(%DateTime{} = since) do
AzazelRecord
|> where([r], r.disposed_at >= ^since)
|> Repo.aggregate(:count, :id)
end
@doc """
Categorical summary of Azazel disposals since a given timestamp.
Returns a list of %{shaliach, count, vidui_categories_filled} maps.
vidui_categories_filled is which of avonot/peshaim/chatot had any
items, NOT the items themselves — property 9's public-side privacy
boundary. The detailed vidui is operator-internal (queryable via
/kodesh/* endpoints, commit 8).
"""
def disposal_summary(since \\ nil) do
query =
AzazelRecord
|> select([r], %{
case_ref: r.case_ref,
shaliach: r.shaliach,
vidui: r.vidui,
triggering_call: r.triggering_call,
disposed_at: r.disposed_at
})
|> order_by([r], desc: r.disposed_at)
query =
case since do
nil -> query
%DateTime{} = ts -> where(query, [r], r.disposed_at >= ^ts)
end
query
|> Repo.all()
|> Enum.map(fn row ->
%{
case_ref: row.case_ref,
shaliach: row.shaliach,
triggering_call: row.triggering_call,
disposed_at: row.disposed_at,
# Categorical only — what slots were filled, NOT the items
categories_filled: vidui_categories_filled(row.vidui)
}
end)
end
defp vidui_categories_filled(vidui) when is_map(vidui) do
Enum.reduce(["avonot", "peshaim", "chatot"], [], fn slot, acc ->
case Map.get(vidui, slot) do
list when is_list(list) and list != [] -> [slot | acc]
_ -> acc
end
end)
|> Enum.reverse()
end
defp vidui_categories_filled(_), do: []
end