Step 14: Connect components — chamishim karsay zahav

Three connections built and tested:
- Haskell Shoresh Engine via Erlang Port (JSON lines over stdin/stdout)
- Prolog Talmudic Engine via Unix domain socket (JSON over /tmp/shorashim/talmudic.sock)
- Erlang Sanhedrin via native BEAM calls

New: Server.hs (JSON-line server loop), json_bridge.pl + socket_server.pl
(Prolog socket interface), ShoreshEnginePort + TalmudicEngineSocket +
SanhedrinClient (Elixir GenServer wrappers). BereishitCycle now routes
unresolved cases through Talmudic Engine and Sanhedrin. All connections
gracefully degrade — pipeline falls back to L1 data when engines unavailable.

46 Elixir tests, 33 Haskell tests, 57 Prolog tests — 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Opus 4.6 committed 2026-03-12 17:39:21 -04:00
1 parent f8f70c77e5
commit c67672777b
18 files changed
+1435 -121

No files matched your search

+3
View File
@@ -27,6 +27,7 @@ import Shoresh.Teamim
import Shoresh.KetivQere
import Shoresh.Lexicon
import Shoresh.Graph
import Shoresh.Server (runServer)
import Luchot.Firmware
main :: IO ()
@@ -34,6 +35,7 @@ main = do
args <- getArgs
case args of
["verify"] -> runVerification
["server"] -> runServer
["verse", book, ch, vs] -> runVerse book ch vs
["root", rootText] -> runRoot rootText
["gematria", word] -> runGematria word
@@ -45,6 +47,7 @@ printUsage = do
putStrLn ""
putStrLn "Usage:"
putStrLn " shoresh-engine verify Run Step 7 verification"
putStrLn " shoresh-engine server JSON-line server (Erlang Port mode)"
putStrLn " shoresh-engine verse <book> <ch> <vs> Read and parse a verse from L1"
putStrLn " shoresh-engine root <root> Activate a root through all 7 binyanim"
putStrLn " shoresh-engine gematria <word> Compute gematria of a Hebrew word"
+2
View File
@@ -79,6 +79,7 @@ data L1Word = L1Word
, l1wShoresh :: !(Maybe Text)
} deriving (Show, Generic)
instance ToJSON L1Word
instance FromJSON L1Word where
parseJSON = withObject "L1Word" $ \v -> L1Word
<$> v .: "position"
@@ -100,6 +101,7 @@ data L1Verse = L1Verse
, l1vWords :: ![L1Word]
} deriving (Show, Generic)
instance ToJSON L1Verse
instance FromJSON L1Verse where
parseJSON = withObject "L1Verse" $ \v -> L1Verse
<$> v .: "ref"
+301
View File
@@ -0,0 +1,301 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Server — JSON-line port interface for pipeline integration.
--
-- Step 14 (Architecture §4): "Connect components: Elixir pipeline
-- calls Haskell engine."
--
-- Construction spec §3.1: Protocol is Erlang Port (JSON).
-- The engine reads one JSON line from stdin, processes it,
-- writes one JSON line to stdout, and repeats.
--
-- Buffering is set to LineBuffering — critical for Erlang Port
-- communication. Haskell defaults to block-buffering on non-TTY
-- stdout, which would deadlock the port.
module Shoresh.Server
( runServer
, handleRequest
) where
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Aeson
import Data.Maybe (mapMaybe)
import System.IO (hSetBuffering, stdin, stdout, stderr,
BufferMode(..), hFlush, hIsEOF)
import System.Directory (doesFileExist)
import Control.Exception (catch, SomeException)
import Shoresh.Types
import Shoresh.Gematria
import Shoresh.Binyanim
import Shoresh.Teamim
import Shoresh.Lexicon
-- | Run the JSON-line server loop.
-- Called by Main.hs when invoked with "server" argument.
runServer :: IO ()
runServer = do
-- Critical: set line buffering on stdin/stdout.
-- Haskell defaults to block-buffering on non-TTY, which would
-- deadlock the Erlang Port.
hSetBuffering stdin LineBuffering
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
serverLoop
-- | Main server loop: read line, process, write line, repeat.
serverLoop :: IO ()
serverLoop = do
eof <- hIsEOF stdin
if eof
then return ()
else do
line <- BL.fromStrict <$> BS8.getLine
response <- processLine line
BLC.putStrLn (encode response)
hFlush stdout
serverLoop
-- | Process a single JSON line and return a response.
processLine :: BL.ByteString -> IO Value
processLine line = case eitherDecode line of
Left err -> return $ errorResponse "PARSE_ERROR" (T.pack err)
Right req -> handleRequest req
`catch` \e -> return $ errorResponse "INTERNAL_ERROR"
(T.pack $ show (e :: SomeException))
-- | Handle a parsed request and produce a response.
handleRequest :: Value -> IO Value
handleRequest val = case parseOperation val of
Nothing -> return $ errorResponse "INVALID_OPERATION" "Missing or invalid 'operation' field"
Just "process" -> handleProcess val
Just "separate" -> handleSeparate val
Just "name" -> handleName val
Just "evaluate" -> handleEvaluate val
Just op -> return $ errorResponse "UNKNOWN_OPERATION" ("Unknown operation: " <> op)
-- | Extract the operation string from a request.
parseOperation :: Value -> Maybe Text
parseOperation (Object obj) = case obj .: "operation" of
Success t -> Just t
_ -> Nothing
where
(.:) o k = case Map.lookup k (toMap o) of
Just (String s) -> Success s
_ -> Error "not found"
toMap = \o -> case fromJSON (Object o) :: Result (Map.Map Text Value) of
Success m -> m
Error _ -> Map.empty
parseOperation _ = Nothing
-- | Handle "process" operation — root activation, te'amim parse, gematria.
-- Construction spec §3.1: Takes text + scope, returns roots_activated,
-- parse_tree, dual_readings.
handleProcess :: Value -> IO Value
handleProcess val = do
let reqText = extractText val
mscope = extractScope val
-- Parse roots from input text
let roots = extractRoots reqText
-- Build activated roots with gematria and binyanim
let activatedRoots = map activateRoot roots
-- If scope is provided, load verse from L1 and parse te'amim
(parseTree, dualReadings) <- case mscope of
Nothing -> return (Null, toJSON ([] :: [Value]))
Just (book, ch, vs) -> do
let path = "l1/torah/" ++ T.unpack (bookToText book)
++ "/" ++ show ch ++ "/" ++ show vs ++ ".json"
exists <- doesFileExist path
if not exists
then return (Null, toJSON ([] :: [Value]))
else do
mverse <- loadL1Verse path
case mverse of
Nothing -> return (Null, toJSON ([] :: [Value]))
Just verse -> do
let annotated = mapMaybe annotateL1Word (l1vWords verse)
tree = readTeamim annotated
ptJson = treeToJson tree
drJson = toJSON ([] :: [Value]) -- Phase 3: extract ketiv/qere
return (ptJson, drJson)
return $ object
[ "status" .= ("ok" :: Text)
, "result" .= object
[ "roots_activated" .= activatedRoots
, "parse_tree" .= parseTree
, "dual_readings" .= dualReadings
]
]
-- | Handle "separate" operation — te'amim-based separation.
-- Construction spec §3.1 operation "separate".
handleSeparate :: Value -> IO Value
handleSeparate _val = do
-- Phase 2: returns stub separation
-- Full implementation requires the annotated words from context
return $ object
[ "status" .= ("ok" :: Text)
, "result" .= object
[ "groups" .= ([] :: [Value])
]
]
-- | Handle "name" operation — assign names to categories.
handleName :: Value -> IO Value
handleName _val = do
return $ object
[ "status" .= ("ok" :: Text)
, "result" .= object
[ "name" .= ("unnamed" :: Text)
]
]
-- | Handle "evaluate" operation — ki tov check.
handleEvaluate :: Value -> IO Value
handleEvaluate _val = do
return $ object
[ "status" .= ("ok" :: Text)
, "result" .= object
[ "evaluation" .= ("teiku" :: Text)
]
]
-- ============================================================
-- Helpers
-- ============================================================
-- | Extract text from the input field of a request.
extractText :: Value -> Text
extractText (Object obj) = case Map.lookup "input" (toMap obj) of
Just (Object inp) -> case Map.lookup "text" (toMap inp) of
Just (String t) -> t
_ -> ""
_ -> ""
where
toMap o = case fromJSON (Object o) :: Result (Map.Map Text Value) of
Success m -> m
Error _ -> Map.empty
extractText _ = ""
-- | Extract scope (book, chapter, verse) from the input field.
extractScope :: Value -> Maybe (Book, Int, Int)
extractScope (Object obj) = do
inp <- case Map.lookup "input" (toMap obj) of
Just (Object i) -> Just (toMap i)
_ -> Nothing
scopeObj <- case Map.lookup "scope" inp of
Just (Object s) -> Just (toMap s)
_ -> Nothing
bookVal <- Map.lookup "book" scopeObj
bookText <- case bookVal of
String s -> Just s
_ -> Nothing
book <- textToBook bookText
ch <- case Map.lookup "chapter" scopeObj of
Just (Number n) -> Just (floor n)
_ -> Nothing
vs <- case Map.lookup "verse" scopeObj of
Just (Number n) -> Just (floor n)
_ -> Nothing
return (book, ch, vs)
where
toMap o = case fromJSON (Object o) :: Result (Map.Map Text Value) of
Success m -> m
Error _ -> Map.empty
extractScope _ = Nothing
-- | Extract Hebrew roots from input text.
extractRoots :: Text -> [Shoresh]
extractRoots t =
let words' = T.words t
-- Try each word as a potential root
candidates = mapMaybe textToShoresh words'
-- Also try the whole text as a single root
wholeRoot = textToShoresh (T.filter (\c -> c >= '\x05D0' && c <= '\x05EA') t)
in case candidates of
[] -> case wholeRoot of
Just r -> [r]
Nothing -> []
rs -> rs
-- | Activate a root: expand through binyanim, compute gematria, find neighbors.
activateRoot :: Shoresh -> Value
activateRoot sh =
let expanded = expand sh
gv = gematriaRoot sh
consonantNames = map consonantName [c1 sh, c2 sh, c3 sh]
defaultBinyan = case Map.lookup Qal expanded of
Just p -> T.pack (show (Shoresh.Types.binyan p))
Nothing -> "Qal"
in object
[ "shoresh" .= consonantNames
, "shoresh_hebrew" .= shoreshToText sh
, "binyan" .= defaultBinyan
, "domain" .= ("unknown" :: Text) -- Phase 3: domain lookup
, "torah_refs" .= ([] :: [Value]) -- Phase 3: lexicon lookup
, "gematria_value" .= gv
, "gematria_neighbors" .= ([] :: [Value]) -- Phase 3: lexicon needed
, "neighbors" .= ([] :: [Value]) -- Phase 3: lexicon needed
]
-- | Convert an L1 word to an AnnotatedWord for te'amim parsing.
annotateL1Word :: L1Word -> Maybe AnnotatedWord
annotateL1Word w = case l1wTeamim w of
[] -> Nothing
(t:_) -> case taamFromName t of
Nothing -> Nothing
Just taam -> Just AnnotatedWord
{ awWord = l1wText w
, awTaam = taam
, awRole = taamRole taam
}
-- | Convert a ParsedVerse tree to JSON.
treeToJson :: ParsedVerse -> Value
treeToJson (Leaf w) = object
[ "type" .= ("leaf" :: Text)
, "word" .= awWord w
, "taam" .= T.pack (show (awTaam w))
]
treeToJson (Branch t l r) = object
[ "type" .= ("branch" :: Text)
, "taam" .= T.pack (show t)
, "left" .= treeToJson l
, "right" .= treeToJson r
]
-- | Build an error response.
errorResponse :: Text -> Text -> Value
errorResponse code msg = object
[ "status" .= ("error" :: Text)
, "error" .= object
[ "code" .= code
, "message" .= msg
]
]
+18 -1
View File
@@ -49,7 +49,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Map.Strict (Map)
import GHC.Generics (Generic)
import Data.Aeson (FromJSON(..), ToJSON(..), withText)
import Data.Aeson (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), withText)
-- | The 22 Hebrew consonants.
-- These are the building blocks. No more, no less.
@@ -145,6 +145,8 @@ data Binyan
instance ToJSON Binyan
instance FromJSON Binyan
instance ToJSONKey Binyan
instance FromJSONKey Binyan
-- | All seven binyanim as a list.
allBinyanim :: [Binyan]
@@ -298,6 +300,9 @@ data ShoreshEntry = ShoreshEntry
, seGlosses :: !(Map Binyan Text)
} deriving (Eq, Show, Generic)
instance ToJSON ShoreshEntry
instance FromJSON ShoreshEntry
-- | Raw input to the processing pipeline.
data Input = Input
@@ -305,18 +310,27 @@ data Input = Input
, inputSource :: !Text
} deriving (Show, Generic)
instance ToJSON Input
instance FromJSON Input
-- | A distinguished category after havdalah.
data Category = Category
{ catName :: !Text
, catContent :: !Text
} deriving (Show, Generic)
instance ToJSON Category
instance FromJSON Category
-- | A processed result ready for evaluation.
data ProcessedResult = ProcessedResult
{ prCategories :: ![Category]
, prRoots :: ![ShoreshEntry]
} deriving (Show, Generic)
instance ToJSON ProcessedResult
instance FromJSON ProcessedResult
-- | The evaluation outcome.
-- Tov: fits the specification.
-- LoTov: does not fit, with reason.
@@ -326,3 +340,6 @@ data Evaluation
| LoTov !Text
| Teiku
deriving (Eq, Show, Generic)
instance ToJSON Evaluation
instance FromJSON Evaluation