specs §3.1 + engine + Doorpost: a root is the consonants L1 records — two, three, or more

Architecture §3.1 typed a root as exactly three consonants
(`Shoresh {c1,c2,c3}`) and listed data/roots.json and
data/torah_refs.json in the engine's file tree. L1 has 115 roots that
are not three letters (32 of two, 83 of four or more), and the engine
has no data directory: the roots are in L1.

The code had followed the spec's type and paid for it. textToShoresh
padded a two-letter root with He — בן became בנה, כל became כלה — and
returned Nothing for anything longer, so Lexicon.buildLexiconFromVerses
either falsified those roots or dropped them. Shoresh.Roots (tablet 33)
had stepped around the type by carrying roots as Text.

Spec:
- §3.1 data structures: `newtype Shoresh = Shoresh { radicals ::
  [Consonant] }` — two or more, never padded or truncated; `mkShoresh`,
  `triliteral`.
- relatedRoots / neighbors: same length, differing in exactly one
  consonant (for a root of three, the old "sharing 2 consonants").
- File tree: Roots.hs and Server.hs added; data/ removed, with a line
  saying where the roots are; cabal.project.freeze listed; aeson's
  purpose corrected.

Engine:
- Types: the newtype, mkShoresh :: [Consonant] -> Maybe Shoresh,
  triliteral, textToShoresh without padding.
- Gematria: sum over the radicals. Graph: neighbors among roots of the
  same length; neighborsByPosition labels the shared positions
  ("c1c2" for a triliteral, as before).
- Server: consonant names through the type.
- Spec.hs: בן stays two letters (gematria 52, not 57); שמאל is held
  whole; the graph compares only roots of equal length. 59 examples.

Doorpost @expected_hash 094b8164… → f653ef05…, in this commit, with the
witnessed file that moved it (architecture).

Read before committing: Haskell 59/0; Elixir 474/0 on seeds 0 7 42;
both images built and run together with the real binary — Shemot 3:14
answers ב-ן (two letters), Bereishit 48:13 answers ש-מ-א-ל (four),
Devarim 6:5 unchanged; local specification_identity verified
f653ef05…; no engine errors or fallbacks.

Not touched: §3.1's dependency list omits bytestring, filepath and
directory, which the cabal file has.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-04 18:42:52 +00:00
1 parent 4f1879d7ce
commit 5ff1b73d00
8 files changed
+153 -96

No files matched your search

+1 -1
View File
@@ -62,7 +62,7 @@ runVerification = do
-- 1. Root activation through all 7 binyanim
putStrLn "--- Test 1: Root activation (ב-ר-א through 7 binyanim) ---"
let bara = mkShoresh Bet Resh Aleph -- ברא = create
let bara = triliteral Bet Resh Aleph -- ברא = create
let expanded = expand bara
mapM_ (\(b, p) -> do
TIO.putStr $ " " <> T.pack (show b) <> ": "
+2 -2
View File
@@ -49,9 +49,9 @@ letterValue c = case c of
Shin -> 300
Tav -> 400
-- | Gematria of a triliteral root.
-- | Gematria of a root — the sum of its consonants, however many.
gematriaRoot :: Shoresh -> Int
gematriaRoot (Shoresh a b d) = letterValue a + letterValue b + letterValue d
gematriaRoot = sum . map letterValue . radicals
-- | Gematria of a single Hebrew character.
-- Returns 0 for non-consonant characters (vowels, cantillation marks).
+31 -17
View File
@@ -6,9 +6,11 @@
-- neighbors :: Shoresh -> ShoreshLexicon -> [ShoreshEntry]
-- "Find all roots sharing 2 consonants (semantic neighborhood)"
--
-- Two roots are neighbors if they share 2 of 3 consonants
-- in the same positions. This captures the Hebrew linguistic
-- phenomenon where related roots share consonants.
-- Two roots are neighbors if they have the same number of consonants
-- and differ in exactly one position — for a root of three, the spec's
-- "sharing 2 consonants". This captures the Hebrew linguistic phenomenon
-- where related roots share consonants. Roots of different lengths are
-- not compared.
module Shoresh.Graph
( neighbors
, sharedConsonants
@@ -20,30 +22,42 @@ import Shoresh.Lexicon (ShoreshLexicon, allRoots, lookupRoot)
import Data.Maybe (mapMaybe)
-- | Count shared consonants between two roots (position-sensitive).
-- Roots of different lengths share none: there is no position to align.
sharedConsonants :: Shoresh -> Shoresh -> Int
sharedConsonants (Shoresh a1 b1 c1') (Shoresh a2 b2 c2') =
(if a1 == a2 then 1 else 0) +
(if b1 == b2 then 1 else 0) +
(if c1' == c2' then 1 else 0)
sharedConsonants x y
| length rx /= length ry = 0
| otherwise = length (filter id (zipWith (==) rx ry))
where
rx = radicals x
ry = radicals y
-- | Find all roots sharing exactly 2 consonants in the same positions.
-- | Find all roots of the same length differing in exactly one consonant.
neighbors :: Shoresh -> ShoreshLexicon -> [ShoreshEntry]
neighbors sh lexicon =
mapMaybe (\sh' ->
if sh' /= sh && sharedConsonants sh sh' >= 2
if sh' /= sh && isNeighbor sh sh'
then lookupRoot sh' lexicon
else Nothing
) (allRoots lexicon)
-- | Find neighbors that share consonants at specific positions.
-- Returns (position_shared, neighbor_entry) pairs.
isNeighbor :: Shoresh -> Shoresh -> Bool
isNeighbor x y =
length (radicals x) == length (radicals y)
&& sharedConsonants x y == length (radicals x) - 1
-- | Find neighbors, labelled by the positions they share.
-- Returns (positions_shared, neighbor_entry) pairs — for a root of
-- three, "c1c2", "c1c3" or "c2c3".
neighborsByPosition :: Shoresh -> ShoreshLexicon -> [(String, ShoreshEntry)]
neighborsByPosition sh@(Shoresh a b d) lexicon =
neighborsByPosition sh lexicon =
concatMap checkNeighbor (allRoots lexicon)
where
checkNeighbor sh'@(Shoresh a' b' d')
| sh' == sh = []
| a == a' && b == b' = maybe [] (\e -> [("c1c2", e)]) (lookupRoot sh' lexicon)
| a == a' && d == d' = maybe [] (\e -> [("c1c3", e)]) (lookupRoot sh' lexicon)
| b == b' && d == d' = maybe [] (\e -> [("c2c3", e)]) (lookupRoot sh' lexicon)
checkNeighbor sh'
| sh' /= sh && isNeighbor sh sh' =
maybe [] (\e -> [(sharedLabel sh', e)]) (lookupRoot sh' lexicon)
| otherwise = []
sharedLabel sh' =
concat [ 'c' : show i
| (i, same) <- zip [1 :: Int ..] (zipWith (==) (radicals sh) (radicals sh'))
, same ]
+8 -5
View File
@@ -248,10 +248,10 @@ extractScope _ = Nothing
-- | Render a root heard from L1 in the port's contract shape
-- (Construction spec §3.1), plus where it was heard from.
--
-- The shoresh is rendered exactly as L1 records it. A two-letter root
-- (כל, שמ) stays two letters: the old path padded it with He to fit the
-- triliteral type and reported a root — and a gematria — the text does
-- not contain.
-- The shoresh is rendered exactly as L1 records it: a two-letter root
-- (בן, יד) stays two letters and a longer one stays whole. The old path
-- padded two letters with He to fit a triliteral type and reported a
-- root — and a gematria — the text does not contain.
--
-- @binyan@ is the stem the verb actually stands in, read from the
-- verse's morphology. It is null for a noun and for a word that is not
@@ -259,8 +259,11 @@ extractScope _ = Nothing
activateRoot :: ResolvedRoot -> Value
activateRoot r =
let sh = rrShoresh r
-- L1's roots are consonants, so this parse cannot fail; if one
-- ever did, the names are simply absent — never invented.
names = maybe [] (map consonantName . radicals) (textToShoresh sh)
in object
[ "shoresh" .= mapMaybe (fmap consonantName . charToConsonant) (T.unpack sh)
[ "shoresh" .= names
, "shoresh_hebrew" .= sh
, "binyan" .= fmap (T.pack . show) (rrBinyan r)
, "domain" .= ("unknown" :: Text) -- Phase 3: domain lookup
+24 -23
View File
@@ -18,6 +18,7 @@ module Shoresh.Types
-- * Triliteral Roots
, Shoresh(..)
, mkShoresh
, triliteral
, shoreshToText
, textToShoresh
-- * Binyanim (Transformation Operators)
@@ -96,39 +97,39 @@ consonantName c = case c of
Shin -> "shin"; Tav -> "tav"
-- | A triliteral root — the atomic semantic unit.
-- Most Hebrew roots are three consonants. Some are two (biconsonantal).
-- The spec says triliteral; L1 shows some bicons (e.g. "שמ").
-- We model the common case. Bicons get a silent third.
data Shoresh = Shoresh
{ c1 :: !Consonant
, c2 :: !Consonant
, c3 :: !Consonant
-- | A root — the atomic semantic unit: the consonants under which the
-- lexicon files a word, exactly as L1 records them.
--
-- Most roots are three consonants. Torah also has roots of two (בן, יד)
-- and of four or more (שמאל). A root is never padded and never truncated
-- to make it three: until 2026-10-04 this type held exactly three
-- consonants, a two-letter root was given a He the text never wrote
-- (כל became כלה), and a longer one could not be held at all.
newtype Shoresh = Shoresh
{ radicals :: [Consonant] -- ^ two or more, in order
} deriving (Eq, Ord, Show, Generic)
instance ToJSON Shoresh
instance FromJSON Shoresh
-- | Construct a Shoresh from three consonants.
mkShoresh :: Consonant -> Consonant -> Consonant -> Shoresh
mkShoresh = Shoresh
-- | Construct a root from its consonants. A root has at least two.
mkShoresh :: [Consonant] -> Maybe Shoresh
mkShoresh cs
| length cs >= 2 = Just (Shoresh cs)
| otherwise = Nothing
-- | A root of three consonants — the common case, and total.
triliteral :: Consonant -> Consonant -> Consonant -> Shoresh
triliteral a b d = Shoresh [a, b, d]
-- | Render a Shoresh as Hebrew text.
shoreshToText :: Shoresh -> Text
shoreshToText (Shoresh a b d) =
T.pack [consonantToChar a, consonantToChar b, consonantToChar d]
shoreshToText = T.pack . map consonantToChar . radicals
-- | Parse a Hebrew text string into a Shoresh.
-- Accepts 2 or 3 consonant strings.
-- For 2-consonant roots, pads with He (ה) as placeholder.
-- | Parse Hebrew consonants into a Shoresh. Every character must be a
-- consonant and there must be at least two. Nothing is added.
textToShoresh :: Text -> Maybe Shoresh
textToShoresh t =
let chars = T.unpack t
consonants = mapM charToConsonant chars
in case consonants of
Just [a, b, d] -> Just (Shoresh a b d)
Just [a, b] -> Just (Shoresh a b He) -- bicons: pad with He
_ -> Nothing
textToShoresh t = mapM charToConsonant (T.unpack t) >>= mkShoresh
-- | The seven binyanim — transformation operators on roots.