shema: record grammatical voice trajectory on every ShemaContext

The Shema (Devarim 6:4-9, 11:13-21, Bamidbar 15:37-41) deliberately
oscillates between singular ("you, sg.") and plural ("you, pl.")
voice. Devarim 11:14 famously switches mid-verse from אַרְצְכֶם (2mp)
to וְאָסַפְתָּ דְגָנֶךָ (2ms). The text refuses to stabilize at
either pole; that refusal IS the anti-capture mechanism:

  • stuck at 2ms     → private capture (gnostic interior)
  • stuck at 2mp     → mob theology (collective without person)
  • stuck at 1cs/1cp → operator-as-deity (the calf)

Before this commit, vav_pipeline/lib/vav_pipeline/shema.ex — the
module literally named after the Shema — encoded none of this. No
markers for person/singular/plural anywhere in the file. The chat
review surfaced this as "load-bearing anti-capture, missing".

Changes:

L1Reader.extract_voice/1 walks every word of a verse, parses the
OSHB morphology codes already present in L1 (Vqq2ms, Sp2mp, Pp1cs,
Vqi2mp, …), and returns a profile with markers, summary by
voice-key, dominant pole, and oscillates_within_verse boolean.
Strips the H/A language marker that prefixes the first /-segment of
each morph string — without that, every verse-initial verb (HVqi…)
was being silently dropped.

ShemaContext now carries voice_profile on every reception (both
receive_context/1 and receive_context_at/2). The voice is computed
fresh from L1, never cached.

8 new tests in test/shema_voice_trajectory_test.exs verify the
extractor against four anchoring verses:
  Devarim 6:5    — uniformly second-singular (5 markers)
  Devarim 11:13  — dominantly second-plural (5 second-plural markers)
  Devarim 11:14  — oscillates_within_verse: true (the mid-verse switch)
  Bereishit 1:1  — only third-singular (no addressee)
Plus three context-level tests confirming ShemaContext exposes the
field, computes it fresh, and traces a non-stabilizing trajectory
across 11:13 → 11:14 → 11:19.

Full vav_pipeline suite: 288 tests, 3 failures (all pre-existing
Shabbat-day calendar failures unrelated to this change), 19 excluded
(L3 PostgreSQL).

Still open: the across-verse stabilization-detector. ShemaContext
now KNOWS its voice; nothing yet refuses a sequence of receptions
that all stabilize at one pole. That belongs downstream — probably
the kushya generator (kal-vachomer challenge when voice has not
moved in N receptions). Logged in Pass 2 of
~/shorashim-conversation-review.md (Finding A).

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-24 18:58:14 -04:00
1 parent 66c936f7ca
commit 0c4f764024
3 files changed
+300

No files matched your search

+143
View File
@@ -143,6 +143,61 @@ defmodule VavPipeline.L1Reader do
Map.get(verse, :smikhut, [])
end
@doc """
Extract grammatical voice profile from a verse.
Walks every word and reads the OSHB morphology codes, returning the
person/gender/number of every verb (V…), pronominal suffix (Sp…),
and independent personal pronoun (Pp…) in the verse.
Why this exists (anti-capture):
The Shema (Devarim 6:4-9, 11:13-21, Bamidbar 15:37-41) deliberately
oscillates between singular ("you, sg.") and plural ("you, pl.")
voice — and Devarim 11:14 famously switches mid-verse from plural
(אַרְצְכֶם, "your land") to singular (וְאָסַפְתָּ דְגָנֶךָ, "and YOU
[sg.] gather YOUR [sg.] grain"). Voice that stabilizes at either
pole is anti-Shema:
• stuck at second-singular → private capture (gnostic individualism)
• stuck at second-plural → collective capture (mob theology)
• stuck at first-anything → operator-as-deity (the calf)
The trajectory IS the anti-capture mechanism. The system records
voice per-verse so downstream stages can detect stabilization.
Returns:
%{
markers: [%{position, source, person, gender, number, morph}],
summary: %{voice_key => count},
dominant: voice_key | :mixed | :none,
oscillates_within_verse: boolean # true iff both 2-sg AND 2-pl appear
}
voice_key is one of:
:first_singular, :first_plural,
:second_singular, :second_plural,
:third_singular, :third_plural
"""
@spec extract_voice(verse()) :: map()
def extract_voice(verse) do
markers =
verse.words
|> Enum.flat_map(&voice_markers_in_word/1)
summary =
markers
|> Enum.frequencies_by(fn m -> voice_key(m.person, m.number) end)
oscillates =
Enum.any?(markers, fn m -> m.person == 2 and m.number == :singular end) and
Enum.any?(markers, fn m -> m.person == 2 and m.number == :plural end)
%{
markers: markers,
summary: summary,
dominant: dominant_voice(summary),
oscillates_within_verse: oscillates
}
end
@doc """
Extract enlarged letters (otiot gedolot) from a verse.
@@ -308,4 +363,92 @@ defmodule VavPipeline.L1Reader do
acc + Map.get(@gematria_values, char, 0)
end)
end
# --- Voice profile (OSHB morphology code parser) ---
#
# OSHB morph format uses '/' to separate language-segment groups, e.g.
# "HC/Vqq2ms" — H = Hebrew, C = conjunction prefix, then Vqq2ms verb
# "HNcmsc/Sp2ms" — Hebrew noun construct, then 2ms pronominal suffix
# "HC/Vqq2mp" — verb 2nd masc plural
# We extract person/gender/number from segments starting with V (verb),
# Sp (pronominal suffix), or Pp (independent personal pronoun).
defp voice_markers_in_word(word) do
case word.morph do
nil -> []
"" -> []
morph ->
morph
|> String.split("/")
|> Enum.flat_map(fn seg ->
case parse_morph_segment(seg) do
nil -> []
parsed -> [Map.merge(parsed, %{position: word.position, morph: morph})]
end
end)
end
end
defp parse_morph_segment(seg) do
# OSHB: the first segment of a morph string is prefixed with H (Hebrew)
# or A (Aramaic) language indicator. Strip it before POS detection;
# the same `/`-segment can be e.g. "HVqi2mp" (תִּשְׁמְעוּ) — without
# stripping, it would miss every verse-initial verb in the Torah.
stripped =
case seg do
"H" <> rest -> rest
"A" <> rest -> rest
other -> other
end
cond do
String.starts_with?(stripped, "Sp") -> attempt_pgn(stripped, :suffix)
String.starts_with?(stripped, "Pp") -> attempt_pgn(stripped, :pronoun)
String.starts_with?(stripped, "V") -> attempt_pgn(stripped, :verb)
true -> nil
end
end
defp attempt_pgn(seg, source) when byte_size(seg) >= 3 do
pgn = String.slice(seg, -3, 3)
case parse_pgn(pgn) do
{:ok, p, g, n} -> %{source: source, person: p, gender: g, number: n}
:error -> nil
end
end
defp attempt_pgn(_, _), do: nil
defp parse_pgn(<<p, g, n>>) when p in ?1..?3 and g in [?m, ?f, ?c] and n in [?s, ?p] do
person = p - ?0
gender =
case g do
?m -> :masculine
?f -> :feminine
?c -> :common
end
number =
case n do
?s -> :singular
?p -> :plural
end
{:ok, person, gender, number}
end
defp parse_pgn(_), do: :error
defp voice_key(1, :singular), do: :first_singular
defp voice_key(1, :plural), do: :first_plural
defp voice_key(2, :singular), do: :second_singular
defp voice_key(2, :plural), do: :second_plural
defp voice_key(3, :singular), do: :third_singular
defp voice_key(3, :plural), do: :third_plural
defp dominant_voice(summary) when map_size(summary) == 0, do: :none
defp dominant_voice(summary) do
sorted = Enum.sort_by(summary, fn {_k, c} -> -c end)
case sorted do
[{key, _}] -> key
[{key1, c1}, {_key2, c2} | _] when c1 > c2 -> key1
_ -> :mixed
end
end
end
+8
View File
@@ -24,6 +24,12 @@ defmodule VavPipeline.Shema do
ketiv_qere: [map()],
root_gematria: map(),
smikhut: [map()],
# Voice profile — anti-capture: the Shema deliberately oscillates
# between "you sg." and "you pl." (e.g. Devarim 11:14 switches
# mid-verse from אַרְצְכֶם to וְאָסַפְתָּ דְגָנֶךָ). Stabilization
# at either pole is anti-Shema; the trajectory IS the protection.
# See L1Reader.extract_voice/1 for full rationale.
voice_profile: map(),
# From cloud (Torah calendar + L1 integrity)
cloud_state: Cloud.cloud_state(),
# From manna
@@ -56,6 +62,7 @@ defmodule VavPipeline.Shema do
ketiv_qere: L1Reader.extract_ketiv_qere(verse),
root_gematria: L1Reader.extract_roots(verse),
smikhut: L1Reader.extract_smikhut(verse),
voice_profile: L1Reader.extract_voice(verse),
# Cloud state — derived from Torah (L1 integrity + calendar)
cloud_state: cloud_state,
# Manna — daily provision
@@ -84,6 +91,7 @@ defmodule VavPipeline.Shema do
ketiv_qere: L1Reader.extract_ketiv_qere(verse),
root_gematria: L1Reader.extract_roots(verse),
smikhut: L1Reader.extract_smikhut(verse),
voice_profile: L1Reader.extract_voice(verse),
cloud_state: cloud_state,
daily_provision: manna,
scope: scope,