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:
Scott KarpandClaude Opus 4.6 committed 2026-03-17 09:43:44 -04:00
1 parent adc3e41679
commit d34e2799ac
14 files changed
+722 -3

No files matched your search

@@ -0,0 +1,105 @@
defmodule VavPipeline.Repo.Migrations.CreateL3Tables do
@moduledoc """
L3 — Oral Tradition. Three tables, append-only.
Architecture §3.7:
interpretations — case decisions with reasoning and Torah references
minority_opinions — preserved dissent (foreign key to interpretations)
teiku_registry — unresolvable tensions with both sides preserved
Construction §3.8 (L3 Write Protocol):
Every write is an INSERT. Never UPDATE. Never DELETE.
The application user has INSERT-only permissions.
Every INSERT includes shema_context_hash and processing_component.
Append-only enforcement via REVOKE is applied after table creation.
The shorashim_app role must be created separately (by the kohen/DBA).
"""
use Ecto.Migration
def up do
# === interpretations ===
create table(:interpretations) do
add :case_ref, :text, null: false
add :interpretation, :text, null: false
add :status, :text, null: false
add :reasoning, :text, null: false
add :torah_references, {:array, :text}, null: false, default: []
add :roots_involved, {:array, :text}, null: false, default: []
add :panel_size, :integer, null: false
add :vote_count_for, :integer, null: false
add :vote_count_against, :integer, null: false
# Construction §3.8 additions
add :shema_context_hash, :text
add :processing_component, :text
# Coupling trace from eish_zarah_reading.md
add :coupling_trace, {:array, :text}, default: []
timestamps(type: :utc_datetime, updated_at: false)
end
# Status constraint
create constraint(:interpretations, :valid_status,
check: "status IN ('accepted', 'rejected', 'teiku')")
create index(:interpretations, [:case_ref])
create index(:interpretations, [:status])
# === minority_opinions ===
create table(:minority_opinions) do
add :interpretation_id, references(:interpretations, on_delete: :restrict), null: false
add :dissenting_reasoning, :text, null: false
add :torah_references, {:array, :text}, null: false, default: []
# Grounding type from eish_zarah_reading.md (achorai/panim)
add :grounding_type, :text
timestamps(type: :utc_datetime, updated_at: false)
end
create index(:minority_opinions, [:interpretation_id])
# === teiku_registry ===
create table(:teiku_registry) do
add :case_ref, :text, null: false
add :tension_description, :text, null: false
add :interpretation_a, :text, null: false
add :interpretation_b, :text, null: false
add :torah_refs_a, {:array, :text}, null: false, default: []
add :torah_refs_b, {:array, :text}, null: false, default: []
# Challenge sequence from eish_zarah_reading.md
add :challenge_sequence, {:array, :text}, default: []
# Resolution fields — teiku may be resolved later (or never)
add :resolved_at, :utc_datetime
add :resolution, :text
timestamps(type: :utc_datetime, updated_at: false)
end
create index(:teiku_registry, [:case_ref])
# === APPEND-ONLY ENFORCEMENT ===
# These execute only if the shorashim_app role exists.
# In dev/test, the migration runs as the superuser and these are skipped.
# In production, the DBA creates the role first.
execute """
DO $$
BEGIN
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'shorashim_app') THEN
REVOKE UPDATE, DELETE ON interpretations FROM shorashim_app;
REVOKE UPDATE, DELETE ON minority_opinions FROM shorashim_app;
REVOKE DELETE ON teiku_registry FROM shorashim_app;
-- teiku_registry allows UPDATE only on resolved_at and resolution
-- (Enforced at application level — Ecto schema validates)
END IF;
END
$$;
"""
end
def down do
drop table(:teiku_registry)
drop table(:minority_opinions)
drop table(:interpretations)
end
end