Shoresh Engine (Haskell, Step 5) - 12 modules: Types, Lexicon, Binyanim, Teamim, Gematria, Graph, KetivQere, Smikhut, Havdalah, Kriah, KiTov, Firmware - 33 EUnit tests, 0 failures - Firmware module verifies L1 SHA-256 hash at startup Talmudic Engine (SWI-Prolog, Step 6) - 15 modules: halt, reasoning, specification, interface - 57 PlUnit tests, 0 failures - Three cardinal halt conditions: avodah_zarah, shfichut_damim, gilui_arayot Vav Pipeline (Elixir, Steps 8-11) - 14 modules: BereishitCycle, Shema, Cloud, Manna, L1Reader, Specification, HaltConditions, ShabbatTimer, CycleLog, Chatzotzrot, ShoreshEngineClient, TalmudicEngineClient, Application - 46 ExUnit tests, 0 failures - Nishma-first architecture: hears before it speaks Sanhedrin Framework (Erlang/OTP, Step 12) - 8 modules: sanhedrin_app, sanhedrin_sup, escalation, panel_sup, panel, judge, clerk, halt_monitor - 46 EUnit tests, 0 failures (includes fix: rejected outcome now reachable) - Panel sizes: 1/3/23/71 — unanimous capital guilty → acquit (Sanhedrin 17a) - Minority opinions append-only, never deleted Korban Processor (Python, Step 13) - 6 modules: korban, shechitah, semichah, zerikah, haktarah, router Builder tooling: AGENTS.md, opencode.json, memory/, src/tools/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
234 lines
6.1 KiB
Elixir
234 lines
6.1 KiB
Elixir
defmodule VavPipeline.L1Reader do
|
||
@moduledoc """
|
||
Reads from the L1 Torah filesystem.
|
||
|
||
L1 is read-only. This module reads, it never writes.
|
||
The aron is sealed. The scroll is read, not rewritten.
|
||
|
||
File structure:
|
||
l1/torah/{book}/{chapter}/{verse}.json
|
||
l1/torah/index.json
|
||
l1/integrity.sha256
|
||
"""
|
||
|
||
@l1_root Application.compile_env(:vav_pipeline, :l1_mount_point, "l1")
|
||
|
||
# L0 firmware constant — from Luchot.Firmware (Haskell)
|
||
@l1_expected_hash "0ee0020aac6c6beb0e83c0638a861c73d37d9dbd39c30d3cb08c1878b481c844"
|
||
|
||
@type word :: %{
|
||
position: pos_integer(),
|
||
text: String.t(),
|
||
consonants: String.t(),
|
||
lemma: String.t(),
|
||
morph: String.t(),
|
||
teamim: [String.t()],
|
||
prefix: String.t() | nil,
|
||
shoresh: String.t() | nil
|
||
}
|
||
|
||
@type verse :: %{
|
||
ref: String.t(),
|
||
book: String.t(),
|
||
chapter: pos_integer(),
|
||
verse: pos_integer(),
|
||
text_hebrew: String.t(),
|
||
words: [word()]
|
||
}
|
||
|
||
@doc """
|
||
Read a verse from L1.
|
||
|
||
Returns the parsed verse data or an error.
|
||
"""
|
||
@spec read_verse(String.t(), pos_integer(), pos_integer()) ::
|
||
{:ok, verse()} | {:error, String.t()}
|
||
def read_verse(book, chapter, verse) do
|
||
path = verse_path(book, chapter, verse)
|
||
|
||
case File.read(path) do
|
||
{:ok, contents} ->
|
||
case Jason.decode(contents) do
|
||
{:ok, data} -> {:ok, normalize_verse(data)}
|
||
{:error, _} -> {:error, "L1_READ_FAILED: corrupt JSON at #{path}"}
|
||
end
|
||
|
||
{:error, :enoent} ->
|
||
{:error, "INVALID_SCOPE: #{book} #{chapter}:#{verse} not found in L1"}
|
||
|
||
{:error, reason} ->
|
||
{:error, "L1_READ_FAILED: #{inspect(reason)} at #{path}"}
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Extract teamim marks from a verse, with word indices.
|
||
|
||
Returns a list of %{word_index, taam, role} maps matching
|
||
the ShemaContext schema from Construction spec §3.1.
|
||
"""
|
||
@spec extract_teamim(verse()) :: [map()]
|
||
def extract_teamim(verse) do
|
||
verse.words
|
||
|> Enum.flat_map(fn word ->
|
||
Enum.map(word.teamim, fn taam ->
|
||
%{
|
||
word_index: word.position - 1,
|
||
taam: taam,
|
||
role: taam_role(taam)
|
||
}
|
||
end)
|
||
end)
|
||
end
|
||
|
||
@doc """
|
||
Extract ketiv/qere dual readings from a verse.
|
||
|
||
Currently L1 does not encode ketiv/qere directly —
|
||
this is a placeholder for when L1 is enriched.
|
||
Returns empty list for now.
|
||
"""
|
||
@spec extract_ketiv_qere(verse()) :: [map()]
|
||
def extract_ketiv_qere(_verse) do
|
||
# L1 does not yet encode ketiv/qere as separate fields.
|
||
# When it does, this will parse them.
|
||
[]
|
||
end
|
||
|
||
@doc """
|
||
Extract roots from a verse with gematria values.
|
||
"""
|
||
@spec extract_roots(verse()) :: map()
|
||
def extract_roots(verse) do
|
||
verse.words
|
||
|> Enum.filter(fn w -> w.shoresh != nil end)
|
||
|> Enum.map(fn w -> {w.shoresh, gematria(w.shoresh)} end)
|
||
|> Enum.uniq_by(fn {root, _} -> root end)
|
||
|> Map.new()
|
||
end
|
||
|
||
@doc """
|
||
Extract smikhut (construct-state) pairs from a verse.
|
||
|
||
Currently a placeholder — smikhut detection requires
|
||
morphological analysis beyond what L1 currently marks.
|
||
"""
|
||
@spec extract_smikhut(verse()) :: [map()]
|
||
def extract_smikhut(_verse) do
|
||
[]
|
||
end
|
||
|
||
@doc """
|
||
Verify L1 integrity by checking the SHA-256 hash.
|
||
|
||
This is a critical check — if L1 is corrupted, the pipeline
|
||
must halt. The specification store is inviolable.
|
||
"""
|
||
@spec verify_integrity() :: :ok | {:error, String.t()}
|
||
def verify_integrity do
|
||
hash_file = Path.join(l1_root(), "integrity.sha256")
|
||
|
||
case File.read(hash_file) do
|
||
{:ok, contents} ->
|
||
stored_hash = contents |> String.trim()
|
||
|
||
if stored_hash == @l1_expected_hash do
|
||
:ok
|
||
else
|
||
{:error, "L1_INTEGRITY_FAILED: hash mismatch — stored #{stored_hash}, expected #{@l1_expected_hash}"}
|
||
end
|
||
|
||
{:error, _} ->
|
||
{:error, "L1_INTEGRITY_FAILED: cannot read integrity.sha256"}
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Get the root path of L1.
|
||
"""
|
||
@spec l1_root() :: String.t()
|
||
def l1_root, do: @l1_root
|
||
|
||
# --- Private ---
|
||
|
||
defp verse_path(book, chapter, verse) do
|
||
Path.join([l1_root(), "torah", book, to_string(chapter), "#{verse}.json"])
|
||
end
|
||
|
||
defp normalize_verse(data) do
|
||
%{
|
||
ref: data["ref"],
|
||
book: data["book"],
|
||
chapter: data["chapter"],
|
||
verse: data["verse"],
|
||
text_hebrew: data["text_hebrew"],
|
||
words:
|
||
Enum.map(data["words"] || [], fn w ->
|
||
%{
|
||
position: w["position"],
|
||
text: w["text"],
|
||
consonants: w["consonants"],
|
||
lemma: w["lemma"],
|
||
morph: w["morph"],
|
||
teamim: w["teamim"] || [],
|
||
prefix: w["prefix"],
|
||
shoresh: w["shoresh"]
|
||
}
|
||
end)
|
||
}
|
||
end
|
||
|
||
# Te'amim classification — mirrors Shoresh Engine Teamim.hs
|
||
# Melekh = disjunctor (emperor/king), Mesharet = conjunctor (servant)
|
||
# Spellings must match what build_l1.py writes to L1 JSON.
|
||
# Verified against actual L1 data (bereishit-devarim ch1 sample).
|
||
@melekh_marks ~w(sof_pasuk etnachta segol_accent shalshelet zaqef_qatan
|
||
zaqef_gadol tipcha revia zarqa pashta yetiv tevir geresh
|
||
gershayim pazer telisha_gedola meteg_siluq zinor geresh_muqdam)
|
||
@mesharet_marks ~w(munach mercha mahapakh darga qadma
|
||
telisha_qetana yerah_ben_yomo)
|
||
|
||
defp taam_role(taam) when taam in @melekh_marks, do: "melekh"
|
||
defp taam_role(taam) when taam in @mesharet_marks, do: "mesharet"
|
||
defp taam_role(_), do: "unknown"
|
||
|
||
# Simple gematria — consonant values
|
||
@gematria_values %{
|
||
"א" => 1,
|
||
"ב" => 2,
|
||
"ג" => 3,
|
||
"ד" => 4,
|
||
"ה" => 5,
|
||
"ו" => 6,
|
||
"ז" => 7,
|
||
"ח" => 8,
|
||
"ט" => 9,
|
||
"י" => 10,
|
||
"כ" => 20,
|
||
"ך" => 20,
|
||
"ל" => 30,
|
||
"מ" => 40,
|
||
"ם" => 40,
|
||
"נ" => 50,
|
||
"ן" => 50,
|
||
"ס" => 60,
|
||
"ע" => 70,
|
||
"פ" => 80,
|
||
"ף" => 80,
|
||
"צ" => 90,
|
||
"ץ" => 90,
|
||
"ק" => 100,
|
||
"ר" => 200,
|
||
"ש" => 300,
|
||
"ת" => 400
|
||
}
|
||
|
||
defp gematria(text) do
|
||
text
|
||
|> String.graphemes()
|
||
|> Enum.reduce(0, fn char, acc ->
|
||
acc + Map.get(@gematria_values, char, 0)
|
||
end)
|
||
end
|
||
end
|