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>
134 lines
4.2 KiB
Elixir
134 lines
4.2 KiB
Elixir
defmodule VavPipeline.Mesorah do
|
|
@moduledoc """
|
|
The second scroll — the priests' copy of the mesorah (bishe'arecha §IV;
|
|
Construction §3.11). Built by `scripts/build_mesorah.py` from the
|
|
Sefaria export, pinned by one SHA-256 over the whole tree (sorted
|
|
relative path, then bytes — the same reading as L1's), mounted
|
|
read-only at `mesorah_mount_point` (`/mnt/mesorah` in the images).
|
|
|
|
The Korban Processor reads the passages; this module keeps the pin:
|
|
what is on disk is hashed and compared with what was engraved here,
|
|
at startup and then at most every ten minutes, and the readiness probe
|
|
carries the answer. A verdict never fetches a text over the network —
|
|
the scroll is the one that was built, or the vessel is not ready.
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
|
|
@expected_hash "7d5b3a6caa61322239d2daf97f8bff1cc6c9aeb81ff58efad033a8e8a3aa90b5"
|
|
@interval_ms 10 * 60 * 1000
|
|
|
|
def expected_hash, do: @expected_hash
|
|
|
|
@spec root() :: String.t()
|
|
def root, do: Application.get_env(:vav_pipeline, :mesorah_mount_point, "mesorah")
|
|
|
|
def start_link(opts \\ []), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
|
|
@doc """
|
|
`:ok` when the tree on disk hashes to the pin; `{:error, reason}`
|
|
otherwise. Answered from the witness's last reading when it runs,
|
|
else read here and now.
|
|
"""
|
|
@spec verify_integrity() :: :ok | {:error, String.t()}
|
|
def verify_integrity do
|
|
if Process.whereis(__MODULE__) do
|
|
GenServer.call(__MODULE__, :status, 30_000)
|
|
else
|
|
compare(compute_tree_hash(root()))
|
|
end
|
|
end
|
|
|
|
@doc "The index the builder wrote, or nil."
|
|
def index do
|
|
case File.read(Path.join(root(), "index.json")) do
|
|
{:ok, text} -> Jason.decode!(text)
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
# The two files the builder writes after hashing are not part of the scroll.
|
|
@not_hashed ~w(integrity.sha256 build_report.json)
|
|
|
|
@doc "SHA-256 over every regular file under `root` but the two the builder writes afterwards: relative path, then bytes, sorted — `build_mesorah.tree_hash`."
|
|
@spec compute_tree_hash(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
|
def compute_tree_hash(root) do
|
|
if File.dir?(root) do
|
|
digest =
|
|
[root | subdirectories(root)]
|
|
|> Enum.sort()
|
|
|> Enum.reduce(:crypto.hash_init(:sha256), fn dir, ctx ->
|
|
dir
|
|
|> File.ls!()
|
|
|> Enum.sort()
|
|
|> Enum.reduce(ctx, fn name, ctx ->
|
|
path = Path.join(dir, name)
|
|
|
|
if File.regular?(path) and not (dir == root and name in @not_hashed) do
|
|
ctx |> :crypto.hash_update(Path.relative_to(path, root)) |> :crypto.hash_update(File.read!(path))
|
|
else
|
|
ctx
|
|
end
|
|
end)
|
|
end)
|
|
|> :crypto.hash_final()
|
|
|> Base.encode16(case: :lower)
|
|
|
|
{:ok, digest}
|
|
else
|
|
{:error, "#{root} is not a directory"}
|
|
end
|
|
rescue
|
|
e -> {:error, Exception.message(e)}
|
|
end
|
|
|
|
defp subdirectories(dir) do
|
|
dir
|
|
|> File.ls!()
|
|
|> Enum.map(&Path.join(dir, &1))
|
|
|> Enum.filter(&File.dir?/1)
|
|
|> Enum.flat_map(fn d -> [d | subdirectories(d)] end)
|
|
end
|
|
|
|
defp compare({:ok, hash}) when hash == @expected_hash, do: :ok
|
|
|
|
defp compare({:ok, hash}),
|
|
do: {:error, "MESORAH_INTEGRITY_FAILED: tree hashes to #{hash}, pinned #{@expected_hash} — not the scroll that was built"}
|
|
|
|
defp compare({:error, reason}), do: {:error, "MESORAH_INTEGRITY_FAILED: #{reason}"}
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
{:ok, %{status: read_now()}, {:continue, :schedule}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_continue(:schedule, state) do
|
|
Process.send_after(self(), :reread, @interval_ms)
|
|
{:noreply, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(:reread, state) do
|
|
Process.send_after(self(), :reread, @interval_ms)
|
|
{:noreply, %{state | status: read_now()}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:status, _from, state), do: {:reply, state.status, state}
|
|
|
|
defp read_now do
|
|
status = compare(compute_tree_hash(root()))
|
|
|
|
case status do
|
|
:ok -> Logger.info("Mesorah: the second scroll hashes to its pin")
|
|
{:error, reason} -> Logger.error(reason)
|
|
end
|
|
|
|
status
|
|
end
|
|
end
|