L1: the inverted nuns are engraved; the small letters cannot be (tablet 40)

Transmitter's word: "add the inverted nuns and small letters to L1."

THE NUNS. The WLC carries the two nun hafukha signs (׆) that mark off
Vayehi binsoa ha'aron — one after Bamidbar 10:34, one after 10:36 —
and scripts/build_l1.py read past them, as it had the פ and ס.
- build_l1.py: `"inverted_nuns": [{ "after_word": 7 }]` on those two
  verses, recorded like a paragraph break by the word the sign follows;
  not part of text_hebrew. Count in build_report.json.
- L1Reader: `inverted_nuns` on every verse (a list; empty elsewhere).
- test/tablet40_inverted_nuns_test.exs — 5 tests: the two verses, the
  passage between them carrying none, each nun standing with its
  setumah, exactly two in the scroll.

WHAT CHANGED IN THE EDUT: one field added to two verse files. With it
removed all 5,853 verse files are byte-identical to the previous L1.

  L1 SHA-256  f743b34b…3f1494  →  9e1abe4e…3c3086

Moved together: L1Reader @l1_expected_hash · Luchot.Firmware + Spec ·
azazel goral._L1_INTEGRITY_HASH · the kohen's gate in KOHEN.md and
KOHEN_PROMPT.md. The goral's seed moves with it.

THE SMALL LETTERS. Not added, because there are none to read. The
builder was not dropping them: it reads `x-small` and records what it
finds. The source — the WLC as the Open Scriptures Hebrew Bible carries
it — has no small-letter mark anywhere in the five books. Not the aleph
of ויקרא, not the he of בהבראם. It marks four large letters (the vav of
גחון, the nun of משפטן, the ayin and dalet of the Shema), and L1 has
those four. The fuller Masoretic lists of enlarged and reduced letters
are a tradition of the scroll that this edition does not encode. To
engrave them would be to add to L1 something its source does not say;
that needs a second source and the transmitter's word, not a builder's
list from memory. l1/README.md, vayakhel §1.2 and terumah §E1 now state
this plainly.

(The puncta extraordinaria ARE in L1 — the upper dot U+05C4 is kept on
the text of 11 words.)

Doorpost @expected_hash b62311e7… → 3bc5dd6a…, in this commit, with the
witnessed file that moved it (vayakhel §1.2).

Read before committing — all five suites on the new L1: Elixir 496/0
(seeds 0 7 42) · Haskell 59/0 · korban 222 passed 1 skipped · azazel
125 passed · Prolog 6 suites exit 0. verify_l1.py OK. Four images
built; full local stack: the witness reads 9e1abe4e… and matches,
health 7/7, no errors; each image carries the new scroll read-only.

A trap met on the way: the first Elixir run after the Doorpost constant
was edited failed two Doorpost tests against the OLD constant. mix had
not recompiled doorpost.ex — the edit changed neither its size nor
(within the same second) its mtime. `touch` and re-run: 496/0. Images
compile from scratch and are not exposed to this.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-04 22:54:21 +00:00
1 parent 7d46fdc7c9
commit 3a9ebdc7d4
17 files changed
+141 -16

No files matched your search

+2 -2
View File
@@ -55,7 +55,7 @@ You inherit infrastructure that has been provisioned and verified:
| cert-manager + nginx-ingress | Running. ClusterIssuer `letsencrypt-prod` working. |
| Gitea (persistent storage) | Running. OAuth app + PAT exist for the operator's account. |
| Gitea MCP server | Running at `https://mcp.shorashim.ch`. Connected to claude.ai. |
| L1 Torah filesystem | In the `shorashim` repo at `l1/torah/`. SHA-256 `f743b34b…3f1494` (full hash in `l1/integrity.sha256`; verified at Day 1). |
| L1 Torah filesystem | In the `shorashim` repo at `l1/torah/`. SHA-256 `9e1abe4e…3c3086` (full hash in `l1/integrity.sha256`; verified at Day 1). |
| Image pull secret `shorashim-registry` | **Verify before assuming.** A prior partial setup may have created it. `kubectl get secrets -n shorashim` is the source of truth. |
| `shorashim-secrets` (DATABASE_URL) | **Verify before assuming.** Same as above. If present, also verify the role it points at (the architecture requires `shorashim_app`, not `avnadmin`). |
| `shorashim-config` ConfigMap | **Verify before assuming.** Runtime config (Shabbat lat/lng, paths, timeouts) — Construction §4.4. |
@@ -105,7 +105,7 @@ cd ~/shorashim # or wherever you cloned
# 1. The scroll itself hashes to the constant baked into the binaries.
# (Until 2026-10-04 this step compared integrity.sha256 with the
# constant — two copies of one string. This reads every verse.)
expected="f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494"
expected="9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086"
python3 scripts/verify_l1.py l1 "$expected"
# Expect: computed = recorded = expected, "OK: the scroll matches"
+1 -1
View File
@@ -77,7 +77,7 @@ test -d ~/shorashim/specs && test -d ~/shorashim/l1/torah && echo "repo OK"
# 0d. L1 integrity matches the firmware constant
cd ~/shorashim
expected="f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494"
expected="9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086"
python3 scripts/verify_l1.py l1 "$expected" || echo "L1 FAIL — STOP"
# 0e. Specification identity (Doorpost)
@@ -77,7 +77,7 @@ from enum import Enum
# the hash changes, this constant updates too. The shared value is
# how the goral's seed inherits L1's tamper-resistance.
_L1_INTEGRITY_HASH = (
"f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494"
"9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086"
)
+34 -1
View File
@@ -90,7 +90,40 @@ structure, not text.
Until 2026-10-04 the builder read past these marks and L1 had no
paragraphs at all (broken tablet 39). Adding them added this one field
to 686 verse files and changed nothing else; the hash moved from
`156b1d27…` to `f743b34b…`.
`156b1d27…` to `f743b34b…` (and since, with the inverted nuns, to
`9e1abe4e…`).
## Inverted Nuns
Two verses carry `inverted_nuns`:
```json
"inverted_nuns": [ { "after_word": 7 } ]
```
The *nun hafukha* (׆) stands after Bamidbar 10:34 and after 10:36,
marking off *Vayehi binsoa ha'aron* (10:35–36) above and below — "because
it is a book of its own" (Shabbat 115b–116a). The WLC writes the sign
after the verse's sof pasuq, before the setumah; like a paragraph break
it is recorded by the word it follows, and it is not part of
`text_hebrew`. Added 2026-10-04 (broken tablet 40); the hash moved from
`f743b34b…` to `9e1abe4e…`.
## Large and Small Letters
A word with an enlarged letter carries `large_letters`. The WLC marks
four in Torah, and L1 has those four: the vav of גחון (Vayikra 11:42),
the nun of משפטן (Bamidbar 27:5), and the ayin and dalet of the Shema
(Devarim 6:4).
**L1 records no small letters, because its source marks none.** The WLC
as the Open Scriptures Hebrew Bible carries it has no small-letter mark
anywhere in the five books — not the aleph of ויקרא (Vayikra 1:1), not
the he of בהבראם (Bereishit 2:4). The builder reads `x-small` and would
record one if the source had it. The enlarged and reduced letters of the
later Masoretic lists (the bet of בראשית among them) are a tradition of
the scroll that this edition of the Leningrad Codex does not encode;
adding them would mean engraving something the source does not say.
## Word Fields
+2 -1
View File
@@ -9,6 +9,7 @@
"petuchah": 293,
"setumah": 394
},
"inverted_nuns": 2,
"words_with_shoresh": 57487,
"shoresh_provenance": {
"main": 23750,
@@ -18,7 +19,7 @@
"none": 3002,
"unlisted": 0
},
"sha256": "f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494",
"sha256": "9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086",
"missing_teamim_refs": [],
"words_without_lexicon_root": [
{
+1 -1
View File
@@ -1 +1 @@
f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494
9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086
+5
View File
@@ -97,5 +97,10 @@
"after_word": 7,
"type": "setumah"
}
],
"inverted_nuns": [
{
"after_word": 7
}
]
}
+5
View File
@@ -106,5 +106,10 @@
"after_word": 7,
"type": "setumah"
}
],
"inverted_nuns": [
{
"after_word": 7
}
]
}
+16
View File
@@ -421,6 +421,7 @@ def parse_verse(verse_elem, book_osis: str, book_hebrew: str) -> dict:
word_position = 0
ketiv_qere_entries = []
parashah_breaks = []
inverted_nuns = []
children = list(verse_elem)
i = 0
@@ -477,6 +478,16 @@ def parse_verse(verse_elem, book_osis: str, book_hebrew: str) -> dict:
"after_word": word_position,
"type": PARASHAH_BREAKS[seg_type],
})
elif seg_type == "x-reversednun":
# Nun hafukha (׆). Two in Torah, bracketing "Vayehi
# binsoa ha'aron" (Bamidbar 10:35–36): one after 10:34
# and one after 10:36. Shabbat 115b–116a: the passage is
# marked off above and below because it is a book of its
# own. The WLC writes the sign after the verse's sof
# pasuq; it is recorded, like a paragraph break, by the
# word it follows. Until tablet 40 the builder read past
# it.
inverted_nuns.append({"after_word": word_position})
i += 1
@@ -507,6 +518,8 @@ def parse_verse(verse_elem, book_osis: str, book_hebrew: str) -> dict:
verse_out["smikhut"] = smikhut
if parashah_breaks:
verse_out["parashah_breaks"] = parashah_breaks
if inverted_nuns:
verse_out["inverted_nuns"] = inverted_nuns
return verse_out
@@ -730,6 +743,7 @@ def build_l1(morphhb_dir: str, output_dir: str, lexicon_dir: str):
total_verses += 1
for brk in verse_data.get("parashah_breaks", []):
parashah_counts[brk["type"]] += 1
parashah_counts["inverted_nun"] += len(verse_data.get("inverted_nuns", []))
# Count words and collect stats
for word in verse_data["words"]:
@@ -802,6 +816,7 @@ def build_l1(morphhb_dir: str, output_dir: str, lexicon_dir: str):
print(f"Verses missing te'amim: {len(missing_teamim)}")
print(f"Parashah breaks: {parashah_counts['petuchah']} petuchot, "
f"{parashah_counts['setumah']} setumot")
print(f"Inverted nuns: {parashah_counts['inverted_nun']}")
words_with_shoresh = sum(_SHORESH_STATS[k] for k in ("main", "sub", "single"))
rootless = sorted(_ROOTLESS.items(), key=lambda kv: (-kv[1], kv[0]))
print(f"Words with a shoresh: {words_with_shoresh}")
@@ -840,6 +855,7 @@ def build_l1(morphhb_dir: str, output_dir: str, lexicon_dir: str):
"petuchah": parashah_counts["petuchah"],
"setumah": parashah_counts["setumah"],
},
"inverted_nuns": parashah_counts["inverted_nun"],
"words_with_shoresh": words_with_shoresh,
"shoresh_provenance": {k: _SHORESH_STATS[k] for k in
("main", "sub", "single", "particle", "none", "unlisted")},
+1 -1
View File
@@ -30,7 +30,7 @@ import Data.Text (Text)
-- From build_report.json and verified during L1 build.
-- If the hash does not match, the specification is compromised.
l1ExpectedHash :: Text
l1ExpectedHash = "f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494"
l1ExpectedHash = "9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086"
-- | Torah book count. Five books. Not four, not six.
torahBookCount :: Int
+1 -1
View File
@@ -314,7 +314,7 @@ firmwareSpec :: Spec
firmwareSpec = describe "Firmware" $ do
it "has correct L1 hash" $
l1ExpectedHash `shouldBe`
"f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494"
"9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086"
it "has correct counts" $ do
torahBookCount `shouldBe` 5
+2
View File
@@ -121,6 +121,8 @@ Source: Westminster Leningrad Codex (WLC) 4.20, as carried by the Open Scripture
| Full Torah text — voweled (nikkud) | ✅ | WLC via OSHB morphhb. |
| Cantillation marks (raw Unicode codepoints U+0591–U+05AF) | ✅ | Present in WLC data. Stored as raw marks on the text, NOT pre-parsed into trees. The system reads them in the act of reading. |
| Petuchah/Stumah paragraph markers | ✅ | In L1: 293 petuchot and 394 setumot, read from the WLC's `פ` / `ס` marks (OSIS `<seg type="x-pe">`, `x-samekh`) and stored per verse as `parashah_breaks`. |
| Inverted nuns (nun hafukha) | ✅ | In L1: the two signs after Bamidbar 10:34 and 10:36, from the WLC's own marks, stored per verse as `inverted_nuns`. |
| Large and small letters | ⚠️ | The WLC source marks four large letters in Torah (in L1 as `large_letters`) and NO small letters. The fuller Masoretic lists of enlarged and reduced letters are not in the source and not in L1. |
| Ketiv/qere annotations | ✅ | WLC annotations: 67 pairs in 64 verses. Stored per verse as `{word_position, ketiv, ketiv_consonants, qere, qere_consonants}` — both readings preserved, never collapsed. |
| Gematria (root → numerical value) | ✅ | Not stored in L1: computed from the consonants when read. Letter values fixed. No equivalence index is built. |
| Smikhut compound index (attested construct-state pairs) | ✅ | Built: 9,203 pairs in 4,106 verses, extracted by `scripts/build_l1.py` from the morphology where a word is marked construct. |
+3 -2
View File
@@ -45,14 +45,15 @@ The specification that sits inside the aron must be specified with the same prec
| **Source — roots** | `openscriptures/HebrewLexicon` at commit `21c9add` (`LexicalIndex.xml`, `AugIndex.xml`): each lemma filed under its Brown-Driver-Briggs root. BDB and Strong's public domain; the index CC BY 4.0 |
| **Builder** | `scripts/build_l1.py`. Deterministic: the same two checkouts give the same bytes and the same hash. Both commits are recorded in `l1/build_report.json` |
| **Extent** | 5 books, 187 chapters, 5,853 verses (WLC/BHS versification; the traditional count is 5,845), 79,982 words |
| **Format** | One JSON file per verse, `l1/torah/{book}/{chapter}/{verse}.json`: `{ "ref": "Bereishit 1:1", "book": "bereishit", "chapter": 1, "verse": 1, "text_hebrew": "...", "words": [...] }`, with `"ketiv_qere": [...]`, `"smikhut": [...]` and `"parashah_breaks": [...]` where the verse has them |
| **Word** | `{ "position": 1, "text": "בְּרֵאשִׁ֖ית", "consonants": "בראשית", "lemma": "7225", "morph": "HR/Ncfsa", "teamim": ["tipcha"], "prefix": "ב", "shoresh": "ראש" }`. `prefix` and `shoresh` are present only where the word has them; `large_letters` marks an enlarged letter (4 words) |
| **Format** | One JSON file per verse, `l1/torah/{book}/{chapter}/{verse}.json`: `{ "ref": "Bereishit 1:1", "book": "bereishit", "chapter": 1, "verse": 1, "text_hebrew": "...", "words": [...] }`, with `"ketiv_qere": [...]`, `"smikhut": [...]`, `"parashah_breaks": [...]` and `"inverted_nuns": [...]` where the verse has them |
| **Word** | `{ "position": 1, "text": "בְּרֵאשִׁ֖ית", "consonants": "בראשית", "lemma": "7225", "morph": "HR/Ncfsa", "teamim": ["tipcha"], "prefix": "ב", "shoresh": "ראש" }`. `prefix` and `shoresh` are present only where the word has them; `large_letters` marks an enlarged letter (4 words — all the source marks). No small letters are recorded: the source marks none in the five books |
| **Integrity** | SHA-256 over every file of the L1 tree (path, then bytes), recorded at build time in `integrity.sha256`. The pipeline re-reads and re-hashes the tree at startup and every ten minutes |
| **Cantillation marks** | Kept on the text as the Unicode codepoints the WLC carries (U+0591–U+05AF), and named per word in `teamim`. The parse tree is NOT pre-computed: the reader builds it from the marks |
| **Ketiv/qere** | From the WLC's ketiv/qere annotations: 67 pairs in 64 verses, stored as `{ "word_position": 6, "ketiv": "...", "ketiv_consonants": "...", "qere": "...", "qere_consonants": "..." }` |
| **Shoresh** | Read from the lexicon by the word's lemma — never derived from the consonants. 57,487 words carry one; 1,378 distinct roots. Particles and pronouns carry none, by rule; names the lexicon gives no etymology carry none |
| **Gematria** | Not stored. Computed from the consonants when read (engine and pipeline) |
| **Petuchah / setumah** | From the WLC's own `פ` / `ס` marks: 293 petuchot and 394 setumot in 686 verses, stored as `"parashah_breaks": [{ "after_word": 13, "type": "petuchah" }]`. The break is recorded by the word it follows; four fall inside a verse. These are the Leningrad Codex's divisions |
| **Inverted nuns** | The two *nun hafukha* signs, after Bamidbar 10:34 and after 10:36, from the WLC's own marks: `"inverted_nuns": [{ "after_word": 7 }]` |
| **Smikhut compounds** | From the morphology, where a word is marked construct: 9,203 attested pairs in 4,106 verses, stored as `{ "position": 5, "construct": "...", "construct_consonants": "...", "absolute": "...", "absolute_consonants": "..." }` |
**Verification procedure:**
+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 "b62311e7541277f3881f074e2e884b742f6f85bd2de8b4890f44e978d06a3f21"
@expected_hash "3bc5dd6aa4d76eeb8c0bc2ee4d7d33b7f35bc1dc8e03dc76697acc33eec8e245"
@doc "Return the list of files that constitute the category-grade specification."
@spec files() :: [String.t()]
+10 -3
View File
@@ -17,7 +17,7 @@ defmodule VavPipeline.L1Reader do
# Updated after build_l1.py fix: enlarged letters (Shema's ayin/dalet, gachon's
# vav, mishpatan's nun), ketiv/qere pairs, and smikhut construct pairs are now
# preserved in L1 rather than silently dropped.
@l1_expected_hash "f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494"
@l1_expected_hash "9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086"
@type letter_mark :: %{letter: String.t(), offset: non_neg_integer()}
@@ -64,7 +64,8 @@ defmodule VavPipeline.L1Reader do
words: [word()],
ketiv_qere: [ketiv_qere_entry()],
smikhut: [smikhut_pair()],
parashah_breaks: [parashah_break()]
parashah_breaks: [parashah_break()],
inverted_nuns: [%{after_word: pos_integer()}]
}
@doc """
@@ -386,7 +387,13 @@ defmodule VavPipeline.L1Reader do
words: Enum.map(data["words"] || [], &normalize_word/1),
ketiv_qere: Enum.map(data["ketiv_qere"] || [], &normalize_ketiv_qere/1),
smikhut: Enum.map(data["smikhut"] || [], &normalize_smikhut/1),
parashah_breaks: Enum.flat_map(data["parashah_breaks"] || [], &normalize_parashah_break/1)
parashah_breaks: Enum.flat_map(data["parashah_breaks"] || [], &normalize_parashah_break/1),
# Nun hafukha (׆) — recorded, like a paragraph break, by the word it
# follows. Two in Torah: after Bamidbar 10:34 and after 10:36.
inverted_nuns:
for %{"after_word" => n} when is_integer(n) <- data["inverted_nuns"] || [] do
%{after_word: n}
end
}
end
@@ -20,7 +20,7 @@ defmodule VavPipeline.ShemaVoiceTrajectoryTest do
stabilizing at one pole — which is the algorithmic shape of "you have
forgotten the One who took you out of Egypt".
Anchoring verses (text from L1, hash f743b34b…):
Anchoring verses (text from L1, hash 9e1abe4e…):
Devarim 6:5 וְאָהַבְתָּ … אֱלֹהֶיךָ … לְבָבְךָ — uniformly 2ms
Devarim 11:13 וְהָיָה אִם־שָׁמֹעַ תִּשְׁמְעוּ … אֱלֹהֵיכֶם — 2mp
Devarim 11:14 אַרְצְכֶם (2mp) → וְאָסַפְתָּ דְגָנֶךָ (2ms)
@@ -0,0 +1,55 @@
defmodule VavPipeline.Tablet40InvertedNunsTest do
@moduledoc """
Tablet 40 (2026-10-04) — the inverted nuns.
*Vayehi binsoa ha'aron* (Bamidbar 10:35–36) is marked off in the scroll
by a nun hafukha before it and after it. The WLC carries both signs; the
builder read past them. They are now engraved on the verse each one
follows — 10:34 and 10:36 — and the reader carries them.
"""
use ExUnit.Case, async: true
alias VavPipeline.L1Reader
test "an inverted nun follows Bamidbar 10:34 and another follows 10:36" do
for verse <- [34, 36] do
{:ok, v} = L1Reader.read_verse("bamidbar", 10, verse)
assert v.inverted_nuns == [%{after_word: length(v.words)}], "Bamidbar 10:#{verse}"
end
end
test "the two verses between them carry none — the signs bracket the passage" do
{:ok, v35} = L1Reader.read_verse("bamidbar", 10, 35)
assert v35.inverted_nuns == []
assert hd(v35.words).consonants == "ויהי"
assert Enum.at(v35.words, 1).consonants == "בנסע"
end
test "each nun stands with a setumah, and neither is in the verse's text" do
for verse <- [34, 36] do
{:ok, v} = L1Reader.read_verse("bamidbar", 10, verse)
assert v.parashah_breaks == [%{after_word: length(v.words), type: :setumah}]
refute v.text_hebrew =~ "׆"
end
end
test "there are exactly two in the whole scroll" do
index = L1Reader.l1_root() |> Path.join("torah/index.json") |> File.read!() |> Jason.decode!()
refs =
for book <- index["books"],
{chapter, verses} <- book["chapter_verse_counts"],
verse <- 1..verses,
{:ok, v} = L1Reader.read_verse(book["book"], String.to_integer(chapter), verse),
v.inverted_nuns != [],
do: v.ref
assert Enum.sort(refs) == ["Bamidbar 10:34", "Bamidbar 10:36"]
end
test "every other verse carries an empty list, not a missing field" do
{:ok, v} = L1Reader.read_verse("bereishit", 1, 1)
assert v.inverted_nuns == []
end
end