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>
292 lines
11 KiB
Haskell
292 lines
11 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
-- | Te'amim — the cantillation mark reader.
|
|
--
|
|
-- Architecture §3.1:
|
|
-- "The marks are RECEIVED from L1 — they are on the text as transmitted.
|
|
-- The parse tree is CONSTRUCTED by the reader in the act of reading.
|
|
-- This is not pre-computation. This is literacy."
|
|
--
|
|
-- The te'amim system is hierarchical:
|
|
-- - Melakhim (kings/disjunctors) SEPARATE
|
|
-- - Meshartim (servants/conjunctors) CONNECT
|
|
--
|
|
-- The strongest disjunctor (sof-pasuk) ends the verse.
|
|
-- The next strongest (etnachta) creates the primary division.
|
|
-- Within each half, subordinate disjunctors create further divisions.
|
|
-- The result is a binary tree — the verse's own syntactic structure.
|
|
--
|
|
-- L1 stores the mark names as strings in each word's "teamim" array.
|
|
-- This module reads those strings and builds the tree.
|
|
module Shoresh.Teamim
|
|
( -- * Types
|
|
Taam(..)
|
|
, TaamRole(..)
|
|
, AnnotatedWord(..)
|
|
, ParsedVerse(..)
|
|
-- * Classification
|
|
, taamRole
|
|
, taamRank
|
|
, taamFromName
|
|
-- * Parsing
|
|
, readTeamim
|
|
, primaryDivision
|
|
, boundGroups
|
|
) where
|
|
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.List (maximumBy)
|
|
import Data.Ord (comparing)
|
|
|
|
-- | Cantillation marks.
|
|
-- The full system has ~30 marks. We enumerate the ones that appear
|
|
-- in L1 data (parsed from WLC Unicode codepoints).
|
|
data Taam
|
|
= SofPasuk -- ^ Verse end (terminal) — strongest disjunctor
|
|
| Etnachta -- ^ Primary division — second strongest
|
|
| ZakefGadol -- ^ Secondary division (strong)
|
|
| ZakefKatan -- ^ Secondary division
|
|
| Segol -- ^ Tertiary
|
|
| Shalshelet -- ^ Rare, strong disjunctor
|
|
| Tipcha -- ^ Tertiary division (near-subordinate to etnachta/zakef)
|
|
| Revia -- ^ Mid-level disjunctor
|
|
| Zarka -- ^ Pre-segol disjunctor
|
|
| Pashta -- ^ Subordinate disjunctor
|
|
| Yetiv -- ^ Subordinate disjunctor
|
|
| Tevir -- ^ Subordinate disjunctor
|
|
| Geresh -- ^ Subordinate disjunctor
|
|
| GershayimMark -- ^ Double geresh — subordinate disjunctor
|
|
| Pazer -- ^ Subordinate disjunctor
|
|
| PazerGadol -- ^ Rare, subordinate disjunctor
|
|
| Telisha_Gedola -- ^ Subordinate disjunctor
|
|
| Telisha_Qetana -- ^ Conjunctor
|
|
| Munach -- ^ Servant — connects to following disjunctor
|
|
| Mercha -- ^ Servant — connects to following disjunctor
|
|
| MerchaKefula -- ^ Double mercha — servant
|
|
| Darga -- ^ Servant
|
|
| Kadma -- ^ Servant (azla)
|
|
| Mahapakh -- ^ Servant
|
|
| Yerach_Ben_Yomo -- ^ Rare servant
|
|
| MetegSiluq -- ^ Dual identity: siluq (disjunctor on final word) / meteg
|
|
deriving (Eq, Ord, Show)
|
|
|
|
-- | Role classification: kings separate, servants connect.
|
|
data TaamRole
|
|
= Melekh -- ^ Disjunctor (separator)
|
|
| Mesharet -- ^ Conjunctor (connector)
|
|
deriving (Eq, Show)
|
|
|
|
-- | Classify a taam as melekh (disjunctor) or mesharet (conjunctor).
|
|
taamRole :: Taam -> TaamRole
|
|
taamRole t = case t of
|
|
SofPasuk -> Melekh
|
|
Etnachta -> Melekh
|
|
ZakefGadol -> Melekh
|
|
ZakefKatan -> Melekh
|
|
Segol -> Melekh
|
|
Shalshelet -> Melekh
|
|
Tipcha -> Melekh
|
|
Revia -> Melekh
|
|
Zarka -> Melekh
|
|
Pashta -> Melekh
|
|
Yetiv -> Melekh
|
|
Tevir -> Melekh
|
|
Geresh -> Melekh
|
|
GershayimMark -> Melekh
|
|
Pazer -> Melekh
|
|
PazerGadol -> Melekh
|
|
Telisha_Gedola -> Melekh
|
|
MetegSiluq -> Melekh -- siluq on final word is a disjunctor
|
|
-- Servants
|
|
Telisha_Qetana -> Mesharet
|
|
Munach -> Mesharet
|
|
Mercha -> Mesharet
|
|
MerchaKefula -> Mesharet
|
|
Darga -> Mesharet
|
|
Kadma -> Mesharet
|
|
Mahapakh -> Mesharet
|
|
Yerach_Ben_Yomo -> Mesharet
|
|
|
|
-- | Disjunctive rank — higher means stronger separator.
|
|
-- This determines where the tree splits.
|
|
-- SofPasuk is strongest (it ends the verse).
|
|
-- Etnachta is next (primary division).
|
|
-- Then the hierarchy descends.
|
|
taamRank :: Taam -> Int
|
|
taamRank t = case t of
|
|
SofPasuk -> 100
|
|
Etnachta -> 90
|
|
ZakefGadol -> 70
|
|
ZakefKatan -> 65
|
|
Segol -> 60
|
|
Shalshelet -> 58
|
|
Revia -> 50
|
|
Tipcha -> 45
|
|
Zarka -> 40
|
|
Pashta -> 35
|
|
Yetiv -> 33
|
|
Tevir -> 30
|
|
Geresh -> 25
|
|
GershayimMark -> 25
|
|
Pazer -> 20
|
|
PazerGadol -> 20
|
|
Telisha_Gedola -> 15
|
|
MetegSiluq -> 80 -- siluq ranks just below etnachta
|
|
-- Servants get rank 0 — they do not create divisions
|
|
_ -> 0
|
|
|
|
-- | Parse a taam name from L1 JSON data.
|
|
-- L1 stores these as strings like "tipcha", "etnachta", "munach".
|
|
-- Build log correction #1: "meteg_siluq" is in TEAMIM_MAP due to
|
|
-- U+05BD dual identity.
|
|
taamFromName :: Text -> Maybe Taam
|
|
taamFromName name = case T.toLower name of
|
|
"sof_pasuk" -> Just SofPasuk
|
|
"sof-pasuk" -> Just SofPasuk
|
|
"sofpasuk" -> Just SofPasuk
|
|
"etnachta" -> Just Etnachta
|
|
"etnahta" -> Just Etnachta
|
|
"zakef_gadol" -> Just ZakefGadol
|
|
"zakef-gadol" -> Just ZakefGadol
|
|
"zaqef_gadol" -> Just ZakefGadol -- L1 spelling
|
|
"zakef_katan" -> Just ZakefKatan
|
|
"zakef-katan" -> Just ZakefKatan
|
|
"zaqef_qatan" -> Just ZakefKatan -- L1 spelling
|
|
"segol" -> Just Segol
|
|
"segol_accent" -> Just Segol -- L1 spelling
|
|
"shalshelet" -> Just Shalshelet
|
|
"tipcha" -> Just Tipcha
|
|
"tipeha" -> Just Tipcha
|
|
"revia" -> Just Revia
|
|
"revi'i" -> Just Revia
|
|
"zarka" -> Just Zarka
|
|
"pashta" -> Just Pashta
|
|
"yetiv" -> Just Yetiv
|
|
"tevir" -> Just Tevir
|
|
"geresh" -> Just Geresh
|
|
"gershayim" -> Just GershayimMark
|
|
"pazer" -> Just Pazer
|
|
"pazer_gadol" -> Just PazerGadol
|
|
"telisha_gedola" -> Just Telisha_Gedola
|
|
"telisha_qetana" -> Just Telisha_Qetana
|
|
"munach" -> Just Munach
|
|
"mercha" -> Just Mercha
|
|
"mercha_kefula" -> Just MerchaKefula
|
|
"darga" -> Just Darga
|
|
"kadma" -> Just Kadma
|
|
"qadma" -> Just Kadma -- L1 spelling
|
|
"azla" -> Just Kadma
|
|
"mahapakh" -> Just Mahapakh
|
|
"yerach_ben_yomo" -> Just Yerach_Ben_Yomo
|
|
"meteg_siluq" -> Just MetegSiluq
|
|
"siluq" -> Just MetegSiluq
|
|
"meteg" -> Just MetegSiluq
|
|
"zinor" -> Just Zarka -- L1: zinor is variant name for zarka
|
|
"geresh_muqdam" -> Just Geresh -- L1: pre-positioned geresh variant
|
|
_ -> Nothing
|
|
|
|
|
|
-- | A word annotated with its cantillation mark and role.
|
|
-- This is what we receive from L1 after parsing the raw mark names.
|
|
data AnnotatedWord = AnnotatedWord
|
|
{ awWord :: !Text -- ^ The Hebrew word text
|
|
, awTaam :: !Taam -- ^ The primary cantillation mark
|
|
, awRole :: !TaamRole -- ^ King or servant
|
|
} deriving (Show)
|
|
|
|
-- | A parsed verse: binary tree where branches are te'amim divisions.
|
|
-- Leaf: a single annotated word.
|
|
-- Branch: a disjunctor splitting the verse into two halves.
|
|
--
|
|
-- This tree is CONSTRUCTED by readTeamim from the raw marks.
|
|
-- It is not stored in L1. This is the act of reading.
|
|
data ParsedVerse
|
|
= Leaf !AnnotatedWord
|
|
| Branch !Taam !ParsedVerse !ParsedVerse
|
|
deriving (Show)
|
|
|
|
|
|
-- | Read the te'amim — construct the parse tree from annotated words.
|
|
--
|
|
-- Algorithm: find the strongest disjunctor, split there,
|
|
-- recurse on each half. This mirrors how a human reader
|
|
-- parses the verse structure from the cantillation marks.
|
|
--
|
|
-- The marks are received. The tree is built in the act of reading.
|
|
readTeamim :: [AnnotatedWord] -> ParsedVerse
|
|
readTeamim [] = error "readTeamim: empty word list — a verse cannot be empty"
|
|
readTeamim [w] = Leaf w
|
|
readTeamim ws =
|
|
-- Find the word with the strongest disjunctive mark
|
|
let indexed = zip [0..] ws
|
|
disjunctors = [ (i, w) | (i, w) <- indexed
|
|
, awRole w == Melekh
|
|
, i < length ws - 1 -- don't split after the last word
|
|
]
|
|
in case disjunctors of
|
|
[] ->
|
|
-- No disjunctors found (all servants) — treat as a chain
|
|
-- This shouldn't happen in well-formed verses but we handle it
|
|
buildChain ws
|
|
_ ->
|
|
-- Split at the strongest disjunctor
|
|
let (splitIdx, splitWord) = maximumBy (comparing (taamRank . awTaam . snd)) disjunctors
|
|
(left, right) = splitAt (splitIdx + 1) ws
|
|
splitTaam = awTaam splitWord
|
|
in Branch splitTaam (readTeamim left) (readTeamim right)
|
|
|
|
-- | Build a linear chain when no disjunctors are present.
|
|
buildChain :: [AnnotatedWord] -> ParsedVerse
|
|
buildChain [] = error "buildChain: empty"
|
|
buildChain [w] = Leaf w
|
|
buildChain (w:ws) = Branch (awTaam w) (Leaf w) (buildChain ws)
|
|
|
|
|
|
-- | Find the primary division (etnachta) of a parsed verse.
|
|
-- Returns the two halves of the verse.
|
|
-- The etnachta divides the verse into its two main parts.
|
|
primaryDivision :: ParsedVerse -> (ParsedVerse, ParsedVerse)
|
|
primaryDivision (Leaf w) = (Leaf w, Leaf w)
|
|
primaryDivision (Branch t left right)
|
|
| t == Etnachta = (left, right)
|
|
| otherwise =
|
|
-- The etnachta might be deeper in the tree.
|
|
-- In a well-formed parse, it should be at the top level.
|
|
-- If not, search left then right.
|
|
let (ll, lr) = primaryDivision left
|
|
(rl, rr) = primaryDivision right
|
|
in if hasEtnachta left
|
|
then (ll, Branch t lr right)
|
|
else if hasEtnachta right
|
|
then (Branch t left rl, rr)
|
|
else (left, right) -- no etnachta found — use top-level split
|
|
|
|
-- | Check if a parsed tree contains an etnachta division.
|
|
hasEtnachta :: ParsedVerse -> Bool
|
|
hasEtnachta (Leaf _) = False
|
|
hasEtnachta (Branch t l r) = t == Etnachta || hasEtnachta l || hasEtnachta r
|
|
|
|
|
|
-- | Identify which words are bound together by meshartim (servant chains).
|
|
-- Words connected by servants form semantic units.
|
|
-- Returns groups of words that are bound together.
|
|
boundGroups :: ParsedVerse -> [[Text]]
|
|
boundGroups = go []
|
|
where
|
|
go acc (Leaf w) = reverse (addToCurrentGroup (awWord w) acc)
|
|
go acc (Branch t left right)
|
|
| taamRole t == Mesharet =
|
|
-- Servants connect — merge groups
|
|
let leftGroups = go acc left
|
|
in go leftGroups right
|
|
| otherwise =
|
|
-- Kings separate — start new group for right
|
|
let leftGroups = go acc left
|
|
-- Close current group, start fresh for right
|
|
rightGroups = go ([] : leftGroups) right
|
|
in rightGroups
|
|
|
|
addToCurrentGroup w [] = [[w]]
|
|
addToCurrentGroup w (g:gs) = (w:g) : gs
|