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>
264 lines
7.5 KiB
Elixir
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
|