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.
+65 -35
View File
@@ -64,32 +64,39 @@ consonantSpec = describe "Consonant" $ do
shoreshSpec :: Spec
shoreshSpec = describe "Shoresh" $ do
it "constructs a triliteral root" $ do
let sh = mkShoresh Bet Resh Aleph
c1 sh `shouldBe` Bet
c2 sh `shouldBe` Resh
c3 sh `shouldBe` Aleph
let sh = triliteral Bet Resh Aleph
radicals sh `shouldBe` [Bet, Resh, Aleph]
it "renders as Hebrew text" $ do
shoreshToText (mkShoresh Bet Resh Aleph) `shouldBe` "ברא"
shoreshToText (mkShoresh Qof Dalet Shin) `shouldBe` "קדש"
shoreshToText (triliteral Bet Resh Aleph) `shouldBe` "ברא"
shoreshToText (triliteral Qof Dalet Shin) `shouldBe` "קדש"
it "parses from Hebrew text (3 consonants)" $ do
textToShoresh "ברא" `shouldBe` Just (mkShoresh Bet Resh Aleph)
textToShoresh "קדש" `shouldBe` Just (mkShoresh Qof Dalet Shin)
textToShoresh "ברא" `shouldBe` Just (triliteral Bet Resh Aleph)
textToShoresh "קדש" `shouldBe` Just (triliteral Qof Dalet Shin)
it "parses from Hebrew text (2 consonants — biconsonantal)" $ do
-- Build log: some roots in L1 are 2-consonant (e.g. "שמ")
let result = textToShoresh "שמ"
result `shouldSatisfy` isJust
-- Bicons pad with He
case result of
Just sh -> c3 sh `shouldBe` He
Nothing -> expectationFailure "Should have parsed"
it "a two-letter root stays two letters — nothing is added" $ do
-- L1 has roots of two consonants (בן, יד). This type once held
-- exactly three and padded these with He: בן became בנה.
fmap radicals (textToShoresh "בן") `shouldBe` Just [Bet, Nun]
fmap shoreshToText (textToShoresh "בן") `shouldBe` Just "בנ"
fmap gematriaRoot (textToShoresh "בן") `shouldBe` Just 52
it "a root of four or more is held whole" $ do
-- שמאל — once rejected as "too long".
fmap radicals (textToShoresh "שמאל") `shouldBe` Just [Shin, Mem, Aleph, Lamed]
fmap gematriaRoot (textToShoresh "שמאל") `shouldBe` Just 371
it "mkShoresh needs at least two consonants" $ do
mkShoresh [] `shouldBe` Nothing
mkShoresh [Aleph] `shouldBe` Nothing
mkShoresh [Bet, Nun] `shouldSatisfy` isJust
it "rejects invalid input" $ do
textToShoresh "" `shouldBe` Nothing
textToShoresh "א" `shouldBe` Nothing -- too short
textToShoresh "אבגד" `shouldBe` Nothing -- too long
textToShoresh "א" `shouldBe` Nothing -- one letter is not a root
textToShoresh "abc" `shouldBe` Nothing
textToShoresh "בר1" `shouldBe` Nothing
binyanSpec :: Spec
@@ -98,14 +105,14 @@ binyanSpec = describe "Binyanim" $ do
length allBinyanim `shouldBe` 7
it "activates a root through all 7 binyanim" $ do
let sh = mkShoresh Bet Resh Aleph
let sh = triliteral Bet Resh Aleph
let expanded = expand sh
Map.size expanded `shouldBe` 7
-- Each binyan should produce a peulah with the same root
mapM_ (\(b, p) -> root p `shouldBe` sh) (Map.toList expanded)
it "activate produces correct grammatical features" $ do
let sh = mkShoresh Shin Mem Resh -- שמר = guard
let sh = triliteral Shin Mem Resh -- שמר = guard
let p = activate Qal Third Singular Masculine Perfect sh
root p `shouldBe` sh
binyan p `shouldBe` Qal
@@ -115,7 +122,7 @@ binyanSpec = describe "Binyanim" $ do
aspect p `shouldBe` Perfect
it "activateDefault uses 3ms perfect citation form" $ do
let sh = mkShoresh Kaf Tav Bet -- כתב = write
let sh = triliteral Kaf Tav Bet -- כתב = write
let p = activateDefault Qal sh
person p `shouldBe` Third
number p `shouldBe` Singular
@@ -133,11 +140,11 @@ gematriaSpec = describe "Gematria" $ do
it "computes root gematria correctly" $ do
-- ברא = Bet(2) + Resh(200) + Aleph(1) = 203
gematriaRoot (mkShoresh Bet Resh Aleph) `shouldBe` 203
gematriaRoot (triliteral Bet Resh Aleph) `shouldBe` 203
-- קדש = Qof(100) + Dalet(4) + Shin(300) = 404
gematriaRoot (mkShoresh Qof Dalet Shin) `shouldBe` 404
gematriaRoot (triliteral Qof Dalet Shin) `shouldBe` 404
-- שלם = Shin(300) + Lamed(30) + Mem(40) = 370
gematriaRoot (mkShoresh Shin Lamed Mem) `shouldBe` 370
gematriaRoot (triliteral Shin Lamed Mem) `shouldBe` 370
it "computes word gematria correctly" $ do
-- תורה = Tav(400) + Vav(6) + Resh(200) + He(5) = 611
@@ -151,8 +158,8 @@ gematriaSpec = describe "Gematria" $ do
gematriaWord "בְּרֵאשִׁ֖ית" `shouldBe` gematriaWord "בראשית"
it "finds gematria neighbors" $ do
let bara = mkShoresh Bet Resh Aleph -- 203
let other = mkShoresh Resh Gimel Gimel -- 200+3+3 = 206, not 203
let bara = triliteral Bet Resh Aleph -- 203
let other = triliteral Resh Gimel Gimel -- 200+3+3 = 206, not 203
let lex = Map.fromList
[ (bara, ShoreshEntry bara Creation [] [] Map.empty)
, (other, ShoreshEntry other Creation [] [] Map.empty)
@@ -240,10 +247,10 @@ ketivQereSpec = describe "KetivQere" $ do
graphSpec :: Spec
graphSpec = describe "Graph" $ do
it "finds roots sharing 2 consonants" $ do
let bara = mkShoresh Bet Resh Aleph -- ברא
let barak = mkShoresh Bet Resh Kaf -- ברך (shares ב,ר)
let garaEnt = mkShoresh Gimel Resh Aleph -- גרא (shares ר,א)
let unrelated = mkShoresh Shin Lamed Mem -- שלם (shares nothing)
let bara = triliteral Bet Resh Aleph -- ברא
let barak = triliteral Bet Resh Kaf -- ברך (shares ב,ר)
let garaEnt = triliteral Gimel Resh Aleph -- גרא (shares ר,א)
let unrelated = triliteral Shin Lamed Mem -- שלם (shares nothing)
let lex = Map.fromList
[ (bara, ShoreshEntry bara Creation [] [] Map.empty)
, (barak, ShoreshEntry barak Blessing [] [] Map.empty)
@@ -253,25 +260,48 @@ graphSpec = describe "Graph" $ do
let ns = neighbors bara lex
length ns `shouldBe` 2 -- barak and gara, not unrelated
it "compares only roots of the same length" $ do
let Just ben = textToShoresh "בן"
let Just bat = textToShoresh "בת"
let bana = triliteral Bet Nun He
sharedConsonants ben bat `shouldBe` 1
sharedConsonants ben bana `shouldBe` 0
let lex = Map.fromList
[ (ben, ShoreshEntry ben Creation [] [] Map.empty)
, (bat, ShoreshEntry bat Creation [] [] Map.empty)
, (bana, ShoreshEntry bana Creation [] [] Map.empty)
]
map seShoresh (neighbors ben lex) `shouldBe` [bat]
map fst (neighborsByPosition ben lex) `shouldBe` ["c1"]
it "labels a triliteral neighbor by the positions it shares" $ do
let bara = triliteral Bet Resh Aleph
let barak = triliteral Bet Resh Kaf
let lex = Map.fromList
[ (bara, ShoreshEntry bara Creation [] [] Map.empty)
, (barak, ShoreshEntry barak Blessing [] [] Map.empty)
]
map fst (neighborsByPosition bara lex) `shouldBe` ["c1c2"]
it "counts shared consonants correctly" $ do
let sh1 = mkShoresh Bet Resh Aleph
let sh2 = mkShoresh Bet Resh Kaf
let sh1 = triliteral Bet Resh Aleph
let sh2 = triliteral Bet Resh Kaf
sharedConsonants sh1 sh2 `shouldBe` 2
let sh3 = mkShoresh Shin Lamed Mem
let sh3 = triliteral Shin Lamed Mem
sharedConsonants sh1 sh3 `shouldBe` 0
kiTovSpec :: Spec
kiTovSpec = describe "KiTov" $ do
it "returns Tov when all roots exist in lexicon" $ do
let sh = mkShoresh Bet Resh Aleph
let sh = triliteral Bet Resh Aleph
let entry = ShoreshEntry sh Creation [] [Reference Bereishit 1 1] Map.empty
let lex = Map.fromList [(sh, entry)]
let result = ProcessedResult [] [entry]
kiTov result lex `shouldBe` Tov
it "returns LoTov when roots are missing" $ do
let sh = mkShoresh Bet Resh Aleph
let sh = triliteral Bet Resh Aleph
let entry = ShoreshEntry sh Creation [] [Reference Bereishit 1 1] Map.empty
let emptyLex = Map.empty
let result = ProcessedResult [] [entry]
+21 -12
View File
@@ -119,13 +119,17 @@ data Consonant = Aleph | Bet | Gimel | Dalet | He | Vav | Zayin
| Samekh | Ayin | Pe | Tsade | Qof | Resh | Shin | Tav
deriving (Eq, Ord, Show, Enum, Bounded)
-- A triliteral root — the atomic semantic unit
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 have
-- three consonants; Torah also has roots of two (בן, יד) and of four or
-- more (שמאל). A root is never padded or truncated to make it three.
newtype Shoresh = Shoresh
{ radicals :: [Consonant] -- two or more, in order
} deriving (Eq, Ord, Show)
mkShoresh :: [Consonant] -> Maybe Shoresh -- needs at least two
triliteral :: Consonant -> Consonant -> Consonant -> Shoresh -- the common case
-- The seven binyanim — transformation operators
data Binyan = Qal -- Simple active
| Nifal -- Passive / reflexive
@@ -157,7 +161,7 @@ data SemanticDomain = Creation | Separation | Speech | Movement
data ShoreshEntry = ShoreshEntry
{ shoresh :: Shoresh
, primaryDomain :: SemanticDomain
, relatedRoots :: [Shoresh] -- roots sharing 2 consonants
, relatedRoots :: [Shoresh] -- same length, differing in one consonant
, torahOccurrences :: [Reference] -- where this root appears in Torah
, glosses :: Map Binyan Text -- basic meanings per binyan
} deriving (Show)
@@ -176,7 +180,8 @@ data Reference = Reference
-- Apply a binyan to a root (the fundamental operation)
activate :: Binyan -> Shoresh -> Peulah
-- Find all roots sharing 2 consonants (semantic neighborhood)
-- Find all roots of the same length differing in exactly one consonant
-- (for a root of three: sharing 2 consonants) — the semantic neighborhood
neighbors :: Shoresh -> ShoreshLexicon -> [ShoreshEntry]
-- Trace a root through all 7 binyanim (full semantic expansion)
@@ -316,6 +321,7 @@ shoresh-engine/
│ ├── Shoresh/
│ │ ├── Types.hs -- All data types above
│ │ ├── Lexicon.hs -- The root lexicon, read from L1 (1,378 roots)
│ │ ├── Roots.hs -- A word's root, heard from L1 (verse, lemma, Torah-wide)
│ │ ├── Binyanim.hs -- Transformation operators
│ │ ├── Havdalah.hs -- Separation/distinction engine
│ │ ├── Kriah.hs -- Naming/identification
@@ -324,23 +330,26 @@ shoresh-engine/
│ │ ├── Teamim.hs -- Te'amim reader (reads raw marks, constructs parse tree)
│ │ ├── Gematria.hs -- Numerical value lookup and equivalences
│ │ ├── Smikhut.hs -- Construct-state composition (reads attested compounds)
│ │ └── KetivQere.hs -- Dual reading type support
│ │ ├── KetivQere.hs -- Dual reading type support
│ │ └── Server.hs -- JSON-line port for the Vav Pipeline (Construction §3.1)
│ ├── Luchot/
│ │ └── Firmware.hs -- L0 immutable constants (compiled in)
│ └── Main.hs
├── data/
│ ├── roots.json -- Root lexicon data
│ └── torah_refs.json -- Torah reference index
├── test/
│ └── Spec.hs
├── cabal.project.freeze
└── shoresh-engine.cabal
```
The engine carries no data files of its own. The roots and the Torah
references are in L1 (`l1/torah/`, found through `L1_MOUNT_POINT`) and
are read from there when the engine starts.
**Dependencies (Haskell packages):**
- `base`
- `containers` (Map, Set)
- `text`
- `aeson` (JSON parsing for lexicon data)
- `aeson` (JSON: L1 verses and the port protocol)
- `vector`
---
+1 -1
View File
@@ -175,7 +175,7 @@ defmodule VavPipeline.Doorpost do
# changes; the update is a deliberate operator commit. A bare
# hash-update commit (without a corresponding spec change in the
# same commit) is itself a drift signal.
@expected_hash "094b8164dc1e5b280dc797d0e4c351ed67898a2046133e5f00a1972ffbde5661"
@expected_hash "f653ef0520c86bb378727fa877421796e18a424acc3fb396c62082b47c53cd50"
@doc "Return the list of files that constitute the category-grade specification."
@spec files() :: [String.t()]