Files
bikoret/scripts/build_mesorah.py
Scott KarpandClaude Fable 5.1 5e325d36cc the second scroll: the mesorah snapshot, pinned and read (bishe'arecha step 6)
Devarim 17:18 — the king writes his copy from before the priests, not from
memory. scripts/build_mesorah.py takes one open-licence version per work
per language from the Sefaria export (bucket sefaria-export, catalogue of
2026-10-02; Public Domain / CC0 / CC-BY / CC-BY-SA only, checked in every
file; no CC-BY-NC — the Bavli is Wikisource's) and lays it down beside
L1: mesorah/works/<slug>/{he,en}.json, index.json, integrity.sha256,
build_report.json. 128 works (Rashi, Onkelos, Ramban, Ibn Ezra, Sforno
on the Torah; all of Mishnah; 37 Bavli tractates; Bereshit and Shemot
Rabbah, Sifrei, Tanchuma), 256 files, 44.9 MiB, tree hash 7d5b3a6c…
computed as L1's (sorted relative path, then bytes; the two post-hash
files excluded). What is out, and why (licence), is in Terumah §E5.

Read, never fetched: korban_processor/mesorah.py finds citations as
written (Rashi on Devarim 6:5, Berakhot 2a, Mishnah Berakhot 1:1,
Bereshit Rabbah 1:1, Sifrei Devarim 32, Tanchuma Bereshit 1 …), resolves
each against the snapshot (complex works through their unnamed node,
Tanchuma by parashah), lists them as examination.mesorah_references, and
makes every passage found a given source for that examination — so a
claim about Rashi anchors in Rashi or does not. The Vav Pipeline couples
coupled(reference(R), mesorah(Work, [Address])) or not_in_mesorah; the
engine accepts mesorah/2 as a terminal and not_in_mesorah is Form 4.
VavPipeline.Mesorah pins the hash, re-reads the tree at startup and every
ten minutes, and /kodesh/health carries mesorah_hash. Mounted read-only at
/mnt/mesorah in the vav-pipeline and korban-processor images. L3
bedikot.mesorah (migration 20261013000002).

Also: the 1-in-12 Day6 serving failure of 2026-10-04 reproduced with seed
446116 — HaltMonitorTest left the system halted; it now resolves on exit
and the Day6 setup is defensive. Construction §3.11, Terumah §E5, Tamid
§1.1, bishearecha §IV, KOHEN, AGENTS. Doorpost 0638d4eb…. Elixir 604/0
(seeds 835592 and 446116), korban 294/1 skipped, Prolog bedikah 44/44,
L3 10/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 19:13:36 +00:00

176 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""
The second scroll — the priests' copy of the mesorah (bishe'arecha §IV).
python3 scripts/build_mesorah.py scripts/mesorah_sources.json [--out mesorah] [--cache DIR]
Devarim 17:18 — the king writes his copy "mil'fnei hakohanim halevi'im",
from before the custodians, not from memory. This builder takes the
chosen version of each work from the Sefaria export (storage bucket
`sefaria-export`, one JSON per version) and lays it down read-only
beside L1: one directory per work, one file per language, an index, a
build report, and one SHA-256 over the whole tree computed exactly as
L1's is (sorted relative path, then bytes). Deterministic: the same
sources file and the same export bytes give the same tree and the same
hash. Nothing is edited, translated or normalised: the text is the
custodians' text, HTML tags and all; readers strip what they must.
Only open versions are taken (Public Domain, CC0, CC-BY, CC-BY-SA);
the licence found in each file must match the sources file, or the
build stops. Sefaria is credited per work in index.json.
Ka'asher tzivah.
"""
import hashlib
import json
import os
import re
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
OPEN = {"Public Domain", "PD", "CC0", "CC-BY", "CC-BY-SA"}
BUCKET = "https://storage.googleapis.com/sefaria-export"
TORAH_BOOKS = {"Genesis": "bereishit", "Exodus": "shemot", "Leviticus": "vayikra", "Numbers": "bamidbar", "Deuteronomy": "devarim"}
def slug(title, categories=None):
"""'Rashi on Genesis' → 'rashi_on_bereishit'; 'Mishnah Berakhot' → 'mishnah_berakhot'; Bavli 'Berakhot' → 'bavli_berakhot'."""
t = title
for en, he in TORAH_BOOKS.items():
t = re.sub(rf"\b{en}\b", he, t)
s = re.sub(r"[^a-z0-9]+", "_", t.lower()).strip("_")
if (categories or [])[:2] == ["Talmud", "Bavli"]:
s = "bavli_" + s
return s
def fetch(url, cache):
key = hashlib.sha256(url.encode()).hexdigest()[:24]
path = Path(cache) / key
if path.exists():
return path.read_bytes()
for attempt in range(4):
try:
with urllib.request.urlopen(url, timeout=120) as r:
data = r.read()
path.write_bytes(data)
return data
except Exception as e: # noqa: BLE001
if attempt == 3:
raise
time.sleep(2 * (attempt + 1))
def schema_for(title, cache):
url = f"{BUCKET}/schemas/{urllib.parse.quote(title.replace(' ', '_'))}.json"
try:
d = json.loads(fetch(url, cache))
except Exception: # noqa: BLE001
return None
s = d.get("schema", {})
if s.get("nodeType") == "JaggedArrayNode":
return {"depth": s.get("depth"), "addressTypes": s.get("addressTypes"), "sectionNames": s.get("sectionNames"), "lengths": s.get("lengths")}
return {"nodeType": s.get("nodeType"), "complex": True, "nodes": [n.get("key") or n.get("title") for n in s.get("nodes", [])]}
# The two files the builder writes AFTER hashing are not part of the scroll.
NOT_HASHED = {"integrity.sha256", "build_report.json"}
def tree_hash(root):
"""SHA-256 over every regular file under root except NOT_HASHED: relative path, then bytes, in sorted order — VavPipeline.Mesorah.compute_tree_hash."""
h = hashlib.sha256()
dirs = [str(root)] + sorted(str(p) for p in Path(root).rglob("*") if p.is_dir())
for d in sorted(dirs):
for name in sorted(os.listdir(d)):
p = Path(d) / name
if p.is_file() and not (Path(d) == Path(root) and name in NOT_HASHED):
h.update(os.path.relpath(p, root).encode())
h.update(p.read_bytes())
return h.hexdigest()
def main(sources_path, out="mesorah", cache=None):
sources = json.load(open(sources_path))
cache = cache or os.path.expanduser("~/shorashim-source-data/sefaria-export-cache")
Path(cache).mkdir(parents=True, exist_ok=True)
out = Path(out)
works_dir = out / "works"
if works_dir.exists():
for p in sorted(works_dir.rglob("*"), reverse=True):
p.unlink() if p.is_file() else p.rmdir()
works_dir.mkdir(parents=True, exist_ok=True)
index = {"export_generated_at": sources["export_generated_at"], "source": "Sefaria export (storage bucket sefaria-export), one version per work per language", "works": {}}
report = {"works": 0, "files": 0, "bytes": 0, "licences": {}, "complex": [], "errors": []}
for w in sources["works"]:
title = w["title"]
# The slug needs the categories (Bavli tractates share titles with Mishnah ones); read them from the Hebrew file.
he_doc = json.loads(fetch(w["versions"]["he"]["url"], cache))
s = slug(title, he_doc.get("categories"))
entry = {"title": title, "slug": s, "versions": {}}
schema = schema_for(title, cache)
if schema:
entry["schema"] = schema
if schema.get("complex"):
report["complex"].append(title)
for lang, v in w["versions"].items():
data = fetch(v["url"], cache)
doc = json.loads(data)
lic = doc.get("license")
if lic not in OPEN:
raise SystemExit(f"{title} [{lang}]: licence in file is {lic!r}, not open — refusing")
if lic != v["license"]:
raise SystemExit(f"{title} [{lang}]: licence in file {lic!r} != sources {v['license']!r}")
keep = {
"title": doc.get("title", title),
"heTitle": doc.get("heTitle"),
"language": doc.get("language", lang),
"versionTitle": doc.get("versionTitle"),
"versionSource": doc.get("versionSource"),
"license": lic,
"categories": doc.get("categories"),
"sectionNames": doc.get("sectionNames") or (schema or {}).get("sectionNames"),
"text": doc.get("text"),
}
d = works_dir / s
d.mkdir(exist_ok=True)
path = d / f"{lang}.json"
payload = json.dumps(keep, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode()
path.write_bytes(payload)
sha = hashlib.sha256(payload).hexdigest()
entry["versions"][lang] = {"versionTitle": keep["versionTitle"], "versionSource": keep["versionSource"], "license": lic, "sha256": sha, "bytes": len(payload), "export_url": v["url"]}
if keep["categories"]:
entry["categories"] = keep["categories"]
if keep["heTitle"]:
entry["heTitle"] = keep["heTitle"]
report["files"] += 1
report["bytes"] += len(payload)
report["licences"][lic] = report["licences"].get(lic, 0) + 1
print(f" {title:34} {lang} {lic:14} {len(payload)/1024:7.0f} KiB {keep['versionTitle'][:50]}")
index["works"][s] = entry
report["works"] += 1
(out / "index.json").write_bytes(json.dumps(index, ensure_ascii=False, indent=1, sort_keys=True).encode())
digest = tree_hash(out)
(out / "integrity.sha256").write_text(digest + "\n")
report["sha256"] = digest
(out / "build_report.json").write_text(json.dumps(report, indent=1, ensure_ascii=False))
print(f"\n{report['works']} works, {report['files']} files, {report['bytes']/1048576:.1f} MiB, licences {report['licences']}")
print(f"complex (named nodes, not numerically addressed): {len(report['complex'])}")
print(f"sha256 {digest}")
return digest
if __name__ == "__main__":
args = sys.argv[1:]
if not args:
sys.exit(__doc__)
out = args[args.index("--out") + 1] if "--out" in args else "mesorah"
cache = args[args.index("--cache") + 1] if "--cache" in args else None
main(args[0], out, cache)