L3 Oral Tradition: PostgreSQL persistence layer for interpretations, minority opinions, teiku
Devarim 17:9 — v'darashta v'higidu lekha et d'var hamishpat. The inquiry requires that what is found is RECORDED. - Ecto Repo with graceful degradation (maybe_add_repo, like other engines) - Migration: interpretations, minority_opinions, teiku_registry tables - Append-only enforcement via REVOKE UPDATE/DELETE - Includes eish_zarah fields: coupling_trace, grounding_type, challenge_sequence - Ecto schemas: Interpretation, MinorityOpinion, TeikuRecord - Changeset validations: status inclusion, panel_size > 0, grounding_type enum - TeikuRecord.resolve_changeset restricts updates to resolved_at/resolution only - L3.Store bridge module: record, query, count operations - L3 tests tagged @moduletag :l3, excluded by default (no local PostgreSQL) - Enable with: SHORASHIM_L3=1 mix test --include l3 - All 76 existing Elixir tests pass, 195 Prolog tests pass, 33 Haskell tests pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
adc3e41679
commit
d34e2799ac
14 files changed
+722
-3
No files matched your search
@@ -3,6 +3,7 @@ defmodule VavPipeline.Application do
|
||||
OTP Application for the Vav Pipeline.
|
||||
|
||||
Starts the supervision tree with:
|
||||
- Repo (L3 PostgreSQL — oral tradition persistence)
|
||||
- ShabbatTimer (mandatory cessation monitor)
|
||||
- ShoreshEnginePort (Erlang Port to Haskell engine, if binary found)
|
||||
- TalmudicEngineSocket (Unix socket to Prolog engine)
|
||||
@@ -26,6 +27,7 @@ defmodule VavPipeline.Application do
|
||||
[
|
||||
{VavPipeline.ShabbatTimer, []}
|
||||
]
|
||||
|> maybe_add_repo()
|
||||
|> maybe_add_shoresh_port(shoresh_binary, shoresh_cwd)
|
||||
|> maybe_add_talmudic_socket(talmudic_socket)
|
||||
|
||||
@@ -33,6 +35,23 @@ defmodule VavPipeline.Application do
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
defp maybe_add_repo(children) do
|
||||
repo_config = Application.get_env(:vav_pipeline, VavPipeline.Repo)
|
||||
|
||||
cond do
|
||||
is_nil(repo_config) ->
|
||||
Logger.warning("VavPipeline.Repo: no config found — running without L3 persistence")
|
||||
children
|
||||
|
||||
repo_config[:start] == false ->
|
||||
Logger.info("VavPipeline.Repo: start disabled — running without L3 persistence")
|
||||
children
|
||||
|
||||
true ->
|
||||
[VavPipeline.Repo | children]
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_add_shoresh_port(children, binary, cwd) do
|
||||
case System.find_executable(binary) do
|
||||
nil ->
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
defmodule VavPipeline.L3.Interpretation do
|
||||
@moduledoc """
|
||||
L3 schema for recorded interpretations.
|
||||
|
||||
Architecture §3.7: Each interpretation records the case, the reasoning,
|
||||
the Torah references that grounded it, the panel vote, and the status.
|
||||
|
||||
Append-only: once inserted, never updated or deleted.
|
||||
The oral tradition is written once.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "interpretations" do
|
||||
field :case_ref, :string
|
||||
field :interpretation, :string
|
||||
field :status, :string
|
||||
field :reasoning, :string
|
||||
field :torah_references, {:array, :string}, default: []
|
||||
field :roots_involved, {:array, :string}, default: []
|
||||
field :panel_size, :integer
|
||||
field :vote_count_for, :integer
|
||||
field :vote_count_against, :integer
|
||||
field :shema_context_hash, :string
|
||||
field :processing_component, :string
|
||||
field :coupling_trace, {:array, :string}, default: []
|
||||
|
||||
has_many :minority_opinions, VavPipeline.L3.MinorityOpinion
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
end
|
||||
|
||||
@required_fields [
|
||||
:case_ref, :interpretation, :status, :reasoning,
|
||||
:torah_references, :roots_involved,
|
||||
:panel_size, :vote_count_for, :vote_count_against
|
||||
]
|
||||
@optional_fields [:shema_context_hash, :processing_component, :coupling_trace]
|
||||
@valid_statuses ~w(accepted rejected teiku)
|
||||
|
||||
def changeset(interpretation, attrs) do
|
||||
interpretation
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> validate_inclusion(:status, @valid_statuses)
|
||||
|> validate_number(:panel_size, greater_than: 0)
|
||||
|> validate_number(:vote_count_for, greater_than_or_equal_to: 0)
|
||||
|> validate_number(:vote_count_against, greater_than_or_equal_to: 0)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
defmodule VavPipeline.L3.MinorityOpinion do
|
||||
@moduledoc """
|
||||
L3 schema for preserved dissent.
|
||||
|
||||
Architecture §3.7: Minority opinions are recorded alongside majority
|
||||
decisions. "Rabbi X says... but Rabbi Y says..." The dissent is never
|
||||
discarded. It may be needed when conditions change.
|
||||
|
||||
Append-only: once inserted, never updated or deleted.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "minority_opinions" do
|
||||
field :dissenting_reasoning, :string
|
||||
field :torah_references, {:array, :string}, default: []
|
||||
field :grounding_type, :string
|
||||
|
||||
belongs_to :interpretation, VavPipeline.L3.Interpretation
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
end
|
||||
|
||||
@required_fields [:dissenting_reasoning, :torah_references, :interpretation_id]
|
||||
@optional_fields [:grounding_type]
|
||||
@valid_grounding_types ~w(achorai panim)
|
||||
|
||||
def changeset(opinion, attrs) do
|
||||
opinion
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> validate_inclusion(:grounding_type, @valid_grounding_types ++ [nil])
|
||||
|> foreign_key_constraint(:interpretation_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
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.{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}.
|
||||
"""
|
||||
def record_interpretation(attrs) when is_map(attrs) do
|
||||
%Interpretation{}
|
||||
|> Interpretation.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
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 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 """
|
||||
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
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
defmodule VavPipeline.L3.TeikuRecord do
|
||||
@moduledoc """
|
||||
L3 schema for unresolvable tensions.
|
||||
|
||||
Architecture §3.7: When resolution fails and teiku is declared,
|
||||
both sides are preserved. The tension is recorded. It may be
|
||||
resolved later — or never.
|
||||
|
||||
Tishbi Yetaretz Kushyot V'Abayot — Elijah will resolve
|
||||
difficulties and problems. Until then, the tension stands.
|
||||
|
||||
Unlike interpretations and minority_opinions, teiku records
|
||||
allow UPDATE on resolved_at and resolution fields only.
|
||||
All other fields are append-only.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "teiku_registry" do
|
||||
field :case_ref, :string
|
||||
field :tension_description, :string
|
||||
field :interpretation_a, :string
|
||||
field :interpretation_b, :string
|
||||
field :torah_refs_a, {:array, :string}, default: []
|
||||
field :torah_refs_b, {:array, :string}, default: []
|
||||
field :challenge_sequence, {:array, :string}, default: []
|
||||
field :resolved_at, :utc_datetime
|
||||
field :resolution, :string
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
end
|
||||
|
||||
@required_fields [
|
||||
:case_ref, :tension_description,
|
||||
:interpretation_a, :interpretation_b,
|
||||
:torah_refs_a, :torah_refs_b
|
||||
]
|
||||
@optional_fields [:challenge_sequence, :resolved_at, :resolution]
|
||||
|
||||
def changeset(record, attrs) do
|
||||
record
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resolution changeset — only allows updating resolved_at and resolution.
|
||||
All other fields are immutable once written.
|
||||
"""
|
||||
def resolve_changeset(record, attrs) do
|
||||
record
|
||||
|> cast(attrs, [:resolved_at, :resolution])
|
||||
|> validate_required([:resolved_at, :resolution])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
defmodule VavPipeline.Repo do
|
||||
@moduledoc """
|
||||
L3 — Oral Tradition. PostgreSQL repository.
|
||||
|
||||
Architecture §3.7: Append-only policy enforced at database level.
|
||||
INSERT allowed. UPDATE and DELETE prohibited (via REVOKE on shorashim_app role).
|
||||
|
||||
The oral tradition is written once and never erased.
|
||||
Luchot v'shivrei luchot munachim ba'aron — both the whole tablets
|
||||
and the broken tablets are in the aron. What is recorded stays recorded.
|
||||
"""
|
||||
|
||||
use Ecto.Repo,
|
||||
otp_app: :vav_pipeline,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
||||
Reference in new issue
Block a user