l1_reader: document te'amim classification principle and shoresh derivation provenance

Two related Pass 2 findings — both about UNSTATED reasoning behind
hard-coded lists in L1Reader.

Finding G — te'amim classification principle:

@melekh_marks and @mesharet_marks were lists of strings with no
documented rule for which marks belong to which class. A future
reader saw the lists but not the deliberation. Added classification
principle to the comment block:

  - The diagnostic test: can the ta'am occupy a sof-pasuk-equivalent
    position? → melekh. Does it only serve a following melekh? →
    mesharet.
  - The hierarchy of melakhim corresponds to syntactic depth
    (sof_pasuk → etnachta → zaqef → tipcha → revia → …).
  - Mesharetim precede their melakhim — the servant goes before
    the king.
  - The classification is BINDING for havdalah: treating a mesharet
    as a phrase-end is a small gilui_arayot.
  - Source authority: Yeivin's Tiberian Masorah §174-§268.
  - Maintenance rule: if OSHB ever encodes a ta'am with a different
    name, BOTH lists must be updated.

Finding H — shoresh derivation provenance:

extract_roots/1 returned a flat root → gematria map without recording
which derivational opinion produced each shoresh. For triliterals
this rarely matters; for weak roots and biliterals it matters
constantly (e.g. וְנָתַתִּי in Devarim 11:14 — OSHB records נת,
classical analysis often gives נתן with assimilated nun). Added
provenance note to the function docstring:

  - The shoresh comes from OSHB lemma database via build_l1.py
  - OSHB pins one root per word at build time; L1 JSON does not
    record which derivational opinion produced it
  - Downstream consumers MUST NOT treat the returned shoresh as
    unique-truth
  - The Talmudic Engine should record the derivational source
    in coupling_trace when a shoresh-based ruling enters L3 —
    otherwise a future re-derivation cannot be reconciled with
    prior opinions stored on top of the OSHB choice
  - Preserving derivational alternatives belongs in the build_l1.py
    enrichment pass, not in this read function

Both docstrings make the WHY visible; the WHAT was already there.
No code change. Tests pass unchanged (288 in vav_pipeline,
8 in shema_voice_trajectory_test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Opus 4.7 committed 2026-04-26 12:02:05 -04:00
1 parent af2a3949cf
commit d3869c5b75
1 file changed
+52
@@ -120,6 +120,27 @@ defmodule VavPipeline.L1Reader do
@doc """
Extract roots from a verse with gematria values.
PROVENANCE NOTE — single-opinion derivation:
The shoresh value on each word comes from the OSHB (Open Scriptures
Hebrew Bible) lemma database via build_l1.py. OSHB pins one root
per word at build time; the L1 JSON does not record which
derivational opinion produced it. For triliteral roots the choice
is rarely contested, but for weak roots and biliterals it matters
constantly — e.g. for וְנָתַתִּי (Devarim 11:14) OSHB records the
biliteral נת, while classical analysis often gives נתן with
assimilated nun. Both are defensible; OSHB picked one.
Downstream consumers MUST NOT treat the returned shoresh as
unique-truth. The Talmudic Engine should record the derivational
source as part of its coupling_trace when a shoresh-based ruling
enters L3 — otherwise a future re-derivation cannot be reconciled
with prior opinions stored on top of the OSHB choice.
This function returns a flat root → gematria map. To preserve
derivational alternatives, that work belongs in the build_l1.py
enrichment pass, not here.
"""
@spec extract_roots(verse()) :: map()
def extract_roots(verse) do
@@ -315,6 +336,37 @@ defmodule VavPipeline.L1Reader do
# Melekh = disjunctor (emperor/king), Mesharet = conjunctor (servant)
# Spellings must match what build_l1.py writes to L1 JSON.
# Verified against actual L1 data (bereishit-devarim ch1 sample).
#
# Classification principle (the WHY behind the two lists):
#
# Each ta'am is either MELEKH or MESHARET. The classification is
# not a stylistic preference — it is structural. A melekh stands
# alone and ends a phrase; a mesharet only joins to the following
# word and cannot stand alone. The diagnostic test:
#
# "Can this ta'am occupy the position of sof-pasuk-equivalent
# in some valid context?" → if yes, melekh.
# "Does this ta'am only occur in service of a following melekh,
# binding two words into one syntactic unit?" → if yes, mesharet.
#
# The hierarchy of melakhim (sof_pasuk → etnachta → zaqef → tipcha
# → revia → …) corresponds to syntactic depth: each level marks
# the cut for one level of clausal subdivision. The mesharetim
# (munach, mercha, mahapakh, darga, qadma, telisha_qetana,
# yerah_ben_yomo) precede their melakhim — the servant goes
# before the king.
#
# This classification is binding for the pipeline's havdalah
# (separation) operation: only melakhim are valid phrase-cut
# points. Treating a mesharet as a phrase-end would falsify the
# text's own divisions — a small gilui_arayot (collapsing the
# disjunctor/conjunctor distinction).
#
# Source authority: this is the consensus Masoretic position
# documented in Yeivin's "Introduction to the Tiberian
# Masorah" (1980) §174-§268. The lists below match what
# build_l1.py emits from the OSHB JSON; if OSHB ever encodes a
# ta'am with a different name, BOTH lists must be updated.
@melekh_marks ~w(sof_pasuk etnachta segol_accent shalshelet zaqef_qatan
zaqef_gadol tipcha revia zarqa pashta yetiv tevir geresh
gershayim pazer telisha_gedola meteg_siluq zinor geresh_muqdam)