Files
bikoret/vav_pipeline/lib
Scott KarpandClaude Opus 4.7 d88b8d09ec voice_monitor: detect across-verse Shema voice stabilization (closes finding A)
Pass 2 finding A, second half. The first half (commit 0c4f764) added
voice_profile to every ShemaContext — recording, per verse, the
grammatical voice computed from OSHB morphology codes. Per-verse
recording was the necessary precondition for what this commit does:
WATCH the trajectory and FIRE when it stops oscillating between the
singular and plural address poles.

WHY THIS MATTERS:

The Shema as recited oscillates between Devarim 6:4–9 (the
singular pole — אֱלֹהֶיךָ, לְבָבְךָ) and Devarim 11:13–21 (the
plural pole — אֱלֹהֵיכֶם, לְבַבְכֶם), and 11:14 famously switches
poles mid-verse (אַרְצְכֶם 2mp → וְאָסַפְתָּ דְגָנֶךָ 2ms). The
text REFUSES to stabilize at either pole. That refusal IS the
anti-capture mechanism:

  - stuck at second_singular     → gnostic-private capture
  - stuck at second_plural       → mob-theology capture
  - stuck at first_*             → operator-as-deity / calf
                                   (Shemot 32:4 — eleh elohekha Yisrael)

Before this commit nothing observed the trajectory ACROSS verses.
Per-verse profiles existed but nobody was watching the sequence;
a system that read only Devarim 6:4–9 in a loop was producing a
gnostic-shaped reception with no warning fired. The voice_profile
field had a reader but no consumer.

THE DETECTOR (VavPipeline.VoiceMonitor):

A GenServer maintaining a sliding window of the dominant voice
from the last N ShemaContexts (default N=10). The rule:

  - Window not yet full       → :clear (insufficient evidence)
  - Window has both poles     → :clear (the Shema is doing its job)
  - Address-only window
    missing one pole          → {:missing_pole, :second_singular | :second_plural}
  - First-person majority     → {:first_person_dominant, :first_singular | :first_plural}
  - Pure narration window     → :clear (third-person sequences are
                                NOT anti-Shema; the rule applies to
                                ADDRESS, not to all voice)

On a NEW alarm (different from the last fired), VoiceMonitor:
  1. Logs a warning naming the specific stabilization shape with
     its Torah anchor and its capture-pattern name.
  2. Fires Chatzotzrot.signal(:assembly_leaders, ...) so
     operators and kohanim subscribed to the chatzotzrot channel
     receive the alert.

Re-firing is suppressed for as long as the same stabilization
persists (no spam). The alarm clears when both poles return; a
subsequent re-stabilization fires fresh.

WIRING:

  - Shema.receive_context/1 and receive_context_at/2 now call
    VoiceMonitor.record(voice_profile) immediately after building
    the context. Process.whereis-guarded — the call is safe in
    tests that don't bring up the supervisor.
  - VoiceMonitor added to the supervision tree in application.ex.
  - Chatzotzrot.signal/2 itself is also Process.whereis-guarded
    inside VoiceMonitor (the existing pattern from manna_ledger,
    arei_miklat, urim_v_tumim).

TESTS (test/voice_monitor_test.exs, 14 tests):

  - below window → :clear
  - all 2ms full window → fires {:missing_pole, :second_plural}
  - all 2mp full window → fires {:missing_pole, :second_singular}
  - both poles present → :clear
  - pure narration → :clear (third-person is not address)
  - first-person majority → {:first_person_dominant, _}
  - first-person tied (not strict majority) → does not fire
  - same stabilization persisting → last_alarm unchanged (no spam)
  - clear-then-stabilize-again → fresh alarm fires
  - reset/0 returns monitor to empty
  - record/1 with full voice_profile map (the Shema-side surface)

Each test starts an isolated VoiceMonitor under a unique name
(small window_size=4 for fast convergence) so tests don't share
state. Chatzotzrot is intentionally NOT brought up — the monitor's
guarded call falls through to log-only.

Full vav_pipeline suite: 329 tests, 0 failures, 24 excluded
(was 315 + 14 = 329). The 24 excluded are :l3-tagged tests
needing PostgreSQL; unchanged.

Closes Pass 2 finding A in full.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:25:59 -04:00
..