Files
bikoret/scripts/build_engine_verses.py
T
Scott KarpandClaude Fable 5.1 f6651425ff engine: the verses indexed by root — the pairwise middot read, not scan
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>
2026-10-25 02:16:08 +00:00

120 lines
4.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()