Over the whole scroll (861cd4e6) gezerah shavah, binyan av and shnei
ketuvim scanned verse pairs — quadratic over 5,853 verses, 2.9 s in the
middot suite and waiting to be a live cost the day a middah is
consulted. The generator now also writes root_in(Root, Book, Ch, Vs),
one fact per root occurrence (49,542), exported by torah_text; the
pairwise rules read the first verse and find the second (and third) by
the root's index: shared_keyword, binyan_av_two, verses_contradict,
resolving_verse; verses_mentioning_root is the index itself. Same
answers (middot 47/47, phase3 28/28 unchanged), middot suite 2.865 s →
0.112 s. l1_sha256 unchanged (the facts are derived from the same L1);
--check passes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
120 lines
4.9 KiB
Python
Executable File
120 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""build_engine_verses.py — the Talmudic Engine's verse facts, derived from L1.
|
||
|
||
Architecture §3.4: verse(Book, Chapter, Verse, HebrewRoots, Gloss) is "the
|
||
reasoning layer's view of the specification — not a replacement for L1".
|
||
Until 2026-10-19 that view was 32 hand-written verses, so any root the
|
||
engine knew lit the same handful and every reading cited the same list.
|
||
This script writes talmudic_engine/src/specification/l1_verses.pl from
|
||
l1/torah — every verse, its roots as the engine names them
|
||
(Hebrew shoresh → letter names, finals folded: ברא → bet_resh_aleph), the
|
||
consonantal text as the gloss — and records the SHA-256 of the L1 files it
|
||
read in the header, so the facts can be checked against the scroll.
|
||
|
||
scripts/build_engine_verses.py # write the facts
|
||
scripts/build_engine_verses.py --check # exit 1 if the facts do not match L1 as it stands
|
||
|
||
Devarim 4:2 — nothing added, nothing taken: the engine reads the scroll's
|
||
roots, not the builder's memory of them.
|
||
"""
|
||
import argparse, hashlib, json, os, sys
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
L1 = os.path.join(ROOT, "l1", "torah")
|
||
OUT = os.path.join(ROOT, "talmudic_engine", "src", "specification", "l1_verses.pl")
|
||
BOOKS = ["bereishit", "shemot", "vayikra", "bamidbar", "devarim"]
|
||
|
||
NAMES = {
|
||
"א": "aleph", "ב": "bet", "ג": "gimel", "ד": "dalet", "ה": "he", "ו": "vav", "ז": "zayin",
|
||
"ח": "chet", "ט": "tet", "י": "yod", "כ": "kaf", "ך": "kaf", "ל": "lamed", "מ": "mem", "ם": "mem",
|
||
"נ": "nun", "ן": "nun", "ס": "samekh", "ע": "ayin", "פ": "pe", "ף": "pe", "צ": "tsadi", "ץ": "tsadi",
|
||
"ק": "qof", "ר": "resh", "ש": "shin", "ת": "tav",
|
||
}
|
||
|
||
|
||
def root_atom(shoresh):
|
||
if not shoresh:
|
||
return None
|
||
names = [NAMES.get(c) for c in shoresh]
|
||
if not names or any(n is None for n in names):
|
||
return None
|
||
return "_".join(names)
|
||
|
||
|
||
def quote(s):
|
||
return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
||
|
||
|
||
def verse_files():
|
||
out = []
|
||
for book in BOOKS:
|
||
bdir = os.path.join(L1, book)
|
||
for ch in sorted((d for d in os.listdir(bdir) if d.isdigit()), key=int):
|
||
cdir = os.path.join(bdir, ch)
|
||
for f in sorted((f for f in os.listdir(cdir) if f.endswith(".json") and f[:-5].isdigit()), key=lambda x: int(x[:-5])):
|
||
out.append((book, int(ch), int(f.split(".")[0]), os.path.join(cdir, f)))
|
||
return out
|
||
|
||
|
||
def build():
|
||
h = hashlib.sha256()
|
||
facts = []
|
||
index = [] # root_in/4: the verses by root — what the pairwise middot read instead of scanning (2026-10-25)
|
||
nverses = nroots = 0
|
||
for book, ch, vs, path in verse_files():
|
||
data = open(path, "rb").read()
|
||
h.update(f"{book}/{ch}/{vs}\n".encode())
|
||
h.update(data)
|
||
d = json.loads(data)
|
||
roots = []
|
||
for w in d.get("words", []):
|
||
a = root_atom(w.get("shoresh"))
|
||
if a and a not in roots:
|
||
roots.append(a)
|
||
gloss = " ".join(w.get("consonants", "") for w in d.get("words", []) if w.get("consonants"))
|
||
facts.append(f"verse({book}, {ch}, {vs}, [{', '.join(roots)}], {quote(gloss)}).")
|
||
for r in roots:
|
||
index.append(f"root_in({r}, {book}, {ch}, {vs}).")
|
||
nverses += 1
|
||
nroots += len(roots)
|
||
header = f"""%% l1_verses.pl — GENERATED by scripts/build_engine_verses.py from l1/torah.
|
||
%% Do not edit: regenerate. verse(Book, Chapter, Verse, Roots, Gloss) for
|
||
%% every verse of L1; Roots are the engine's letter-name atoms derived from
|
||
%% each word's shoresh (finals folded), once per verse in word order; Gloss
|
||
%% is the consonantal text. Included by torah_text.pl.
|
||
%%
|
||
%% l1_sha256: {h.hexdigest()}
|
||
%% verses: {nverses} root_occurrences: {nroots}
|
||
%%
|
||
%% root_in(Root, Book, Ch, Vs) — the same facts inverted, one per root
|
||
%% occurrence, so a rule that asks "which verses carry this root" reads
|
||
%% an indexed table instead of scanning 5,853 verses (the pairwise middot:
|
||
%% gezerah shavah, binyan av, shnei ketuvim).
|
||
"""
|
||
return header + "\n".join(facts) + "\n\n" + "\n".join(index) + "\n", h.hexdigest(), nverses
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--check", action="store_true")
|
||
a = ap.parse_args()
|
||
text, digest, n = build()
|
||
if a.check:
|
||
try:
|
||
current = open(OUT, encoding="utf-8").read()
|
||
except FileNotFoundError:
|
||
print(f"engine verses: {OUT} missing — run scripts/build_engine_verses.py", file=sys.stderr)
|
||
sys.exit(1)
|
||
if current != text:
|
||
print(f"engine verses: {OUT} does not match L1 (l1_sha256 now {digest[:12]}…) — regenerate", file=sys.stderr)
|
||
sys.exit(1)
|
||
print(f"engine verses: match L1 ({n} verses, l1_sha256 {digest[:12]}…)")
|
||
return
|
||
with open(OUT, "w", encoding="utf-8") as f:
|
||
f.write(text)
|
||
print(f"wrote {OUT}: {n} verses, l1_sha256 {digest[:12]}…")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|