From 3a9ebdc7d4e29e3671e90f74d89d8ace8a7f9f77 Mon Sep 17 00:00:00 2001 From: Scott Karp Date: Sun, 4 Oct 2026 22:54:21 +0000 Subject: [PATCH] L1: the inverted nuns are engraved; the small letters cannot be (tablet 40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- KOHEN.md | 4 +- KOHEN_PROMPT.md | 2 +- .../src/azazel_processor/goral.py | 2 +- l1/README.md | 35 +++++++++++- l1/build_report.json | 3 +- l1/integrity.sha256 | 2 +- l1/torah/bamidbar/10/34.json | 5 ++ l1/torah/bamidbar/10/36.json | 5 ++ scripts/build_l1.py | 16 ++++++ shoresh-engine/src/Luchot/Firmware.hs | 2 +- shoresh-engine/test/Spec.hs | 2 +- specs/shorashim_terumah.md | 2 + specs/shorashim_vayakhel.md | 5 +- vav_pipeline/lib/vav_pipeline/doorpost.ex | 2 +- vav_pipeline/lib/vav_pipeline/l1_reader.ex | 13 ++++- .../test/shema_voice_trajectory_test.exs | 2 +- .../test/tablet40_inverted_nuns_test.exs | 55 +++++++++++++++++++ 17 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 vav_pipeline/test/tablet40_inverted_nuns_test.exs diff --git a/KOHEN.md b/KOHEN.md index ca49e80d..7dc4679c 100644 --- a/KOHEN.md +++ b/KOHEN.md @@ -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" diff --git a/KOHEN_PROMPT.md b/KOHEN_PROMPT.md index dd1d40a0..a71cdd4d 100644 --- a/KOHEN_PROMPT.md +++ b/KOHEN_PROMPT.md @@ -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) diff --git a/azazel_processor/src/azazel_processor/goral.py b/azazel_processor/src/azazel_processor/goral.py index 5952a773..46797c75 100644 --- a/azazel_processor/src/azazel_processor/goral.py +++ b/azazel_processor/src/azazel_processor/goral.py @@ -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" ) diff --git a/l1/README.md b/l1/README.md index 220b759c..d2b3346d 100644 --- a/l1/README.md +++ b/l1/README.md @@ -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 diff --git a/l1/build_report.json b/l1/build_report.json index 5a08e8fd..2474db8b 100644 --- a/l1/build_report.json +++ b/l1/build_report.json @@ -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": [ { diff --git a/l1/integrity.sha256 b/l1/integrity.sha256 index f62ce138..9fb75abd 100644 --- a/l1/integrity.sha256 +++ b/l1/integrity.sha256 @@ -1 +1 @@ -f743b34b83617c328ec190e78ea1402470938e460b9701aec465c3f1613f1494 \ No newline at end of file +9e1abe4edabac825d8a7037606ff247c3e8f8167650f04553f46c34e4f3c3086 \ No newline at end of file diff --git a/l1/torah/bamidbar/10/34.json b/l1/torah/bamidbar/10/34.json index 8492f465..2c4553e4 100644 --- a/l1/torah/bamidbar/10/34.json +++ b/l1/torah/bamidbar/10/34.json @@ -97,5 +97,10 @@ "after_word": 7, "type": "setumah" } + ], + "inverted_nuns": [ + { + "after_word": 7 + } ] } \ No newline at end of file diff --git a/l1/torah/bamidbar/10/36.json b/l1/torah/bamidbar/10/36.json index b61ebf63..433592fd 100644 --- a/l1/torah/bamidbar/10/36.json +++ b/l1/torah/bamidbar/10/36.json @@ -106,5 +106,10 @@ "after_word": 7, "type": "setumah" } + ], + "inverted_nuns": [ + { + "after_word": 7 + } ] } \ No newline at end of file diff --git a/scripts/build_l1.py b/scripts/build_l1.py index bec03565..3c6446c2 100644 --- a/scripts/build_l1.py +++ b/scripts/build_l1.py @@ -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")}, diff --git a/shoresh-engine/src/Luchot/Firmware.hs b/shoresh-engine/src/Luchot/Firmware.hs index 2d0826f7..a8c167bb 100644 --- a/shoresh-engine/src/Luchot/Firmware.hs +++ b/shoresh-engine/src/Luchot/Firmware.hs @@ -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 diff --git a/shoresh-engine/test/Spec.hs b/shoresh-engine/test/Spec.hs index d9423a60..d0316427 100644 --- a/shoresh-engine/test/Spec.hs +++ b/shoresh-engine/test/Spec.hs @@ -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 diff --git a/specs/shorashim_terumah.md b/specs/shorashim_terumah.md index 11d375ce..26ee4a77 100644 --- a/specs/shorashim_terumah.md +++ b/specs/shorashim_terumah.md @@ -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 ``, `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. | diff --git a/specs/shorashim_vayakhel.md b/specs/shorashim_vayakhel.md index 542389f2..df87c0d5 100644 --- a/specs/shorashim_vayakhel.md +++ b/specs/shorashim_vayakhel.md @@ -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:** diff --git a/vav_pipeline/lib/vav_pipeline/doorpost.ex b/vav_pipeline/lib/vav_pipeline/doorpost.ex index 7bc6fd13..d45bb9c1 100644 --- a/vav_pipeline/lib/vav_pipeline/doorpost.ex +++ b/vav_pipeline/lib/vav_pipeline/doorpost.ex @@ -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()] diff --git a/vav_pipeline/lib/vav_pipeline/l1_reader.ex b/vav_pipeline/lib/vav_pipeline/l1_reader.ex index 2f851585..248710f4 100644 --- a/vav_pipeline/lib/vav_pipeline/l1_reader.ex +++ b/vav_pipeline/lib/vav_pipeline/l1_reader.ex @@ -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 diff --git a/vav_pipeline/test/shema_voice_trajectory_test.exs b/vav_pipeline/test/shema_voice_trajectory_test.exs index 66d76d8e..f02c8925 100644 --- a/vav_pipeline/test/shema_voice_trajectory_test.exs +++ b/vav_pipeline/test/shema_voice_trajectory_test.exs @@ -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) diff --git a/vav_pipeline/test/tablet40_inverted_nuns_test.exs b/vav_pipeline/test/tablet40_inverted_nuns_test.exs new file mode 100644 index 00000000..03849201 --- /dev/null +++ b/vav_pipeline/test/tablet40_inverted_nuns_test.exs @@ -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