Commit Graph
3 Commits
Author SHA1 Message Date
Scott KarpandClaude Opus 4.7 85155b23fb azazel_processor: commit 3 — shaliach + tevilah (property 4, role-separated operator)
Pass 4 / Finding F, commit 3 of 10 from BUILD_AZAZEL.md.

Vayikra 16:21 — וְשִׁלַּח בְּיַד־אִישׁ עִתִּי הַמִּדְבָּרָה.
And he shall send the goat into the wilderness by the hand of a
designated man (ish itti).

Vayikra 16:26 — וְהַמְשַׁלֵּחַ … יְכַבֵּס בְּגָדָיו וְרָחַץ אֶת־בְּשָׂרוֹ
בַּמַּיִם וְאַחֲרֵי־כֵן יָבוֹא אֶל־הַמַּחֲנֶה.
The sender washes his clothes and bathes his flesh (tevilah), and
AFTERWARD comes into the camp.

Yoma 67a: the ish itti is a dedicated operator role, prepared in
advance, distinct from the kohanim who handle sacred processing.
Contact with the disposal channel produces state that must be
reset before the operator rejoins the trusted boundary.

THE THREE ROLES IN SHORASHIM:

  Owner       — submits the offering (upstream caller)
  Kohen       — performs shechitah/zerikah/haktarah (korban_processor
                stages.py enforces owner/kohen separation)
  Shaliach    — receives input the kohen REFUSED and disposes of it
                through the Azazel protocol (THIS module)

Without tevilah, payload references would persist across disposals;
disposal-N would contaminate disposal-N+1 with information from N.
The Mishkan analog: the ish itti who returns from the wilderness
without tevilah brings desert-state into the camp.

CREATED:

  src/azazel_processor/shaliach.py        ~210 lines

    IshItti class:
      __init__       — sets _immutable_identifier (no other immutable
                       attrs; everything else must be cleared by tevilah)
      activate()     — begin a disposal; refuses if already active (no
                       state-mixing across disposals)
      hold_payload() — register payload ref for later destruction
      tag()/get_tag()— intermediate metadata during the disposal route
      tevilah()      — Vayikra 16:26: clears ALL non-_immutable_
                       attributes (discovered dynamically via vars(),
                       so future state additions are auto-cleared)
      identifier     — propagates onto AzazelRecords (commit 7) for
                       shaliach attribution

    ShaliachError    — raised on role-separation violations
    assert_shaliach_active(handler) — runtime gate every disposal-channel
                       function calls at entry; the type-system gate is
                       AzazelStaged (property 11). Refuses non-IshItti
                       callers AND inactive handlers. Identity-checked
                       (isinstance), not duck-typed.

THE LOAD-BEARING CI INVARIANT (test_tevilah_clears_all_non_immutable_state):

Walks vars(handler) post-tevilah and asserts every non-_immutable_
attribute is in a 'fresh' state (empty container, False, None, 0).
A future contributor who adds `self._cache = {}` (or any state
attribute) without extending tevilah() to clear it breaks this and
the test fires before the regression ships. The discovery is
DYNAMIC — no hand-maintained list of attributes to clear; tevilah
is implemented to clear all non-_immutable_ attrs and the test
verifies it does.

  test/test_shaliach.py                   23 tests covering:
    - construction (named identifier required, validates non-empty,
      non-whitespace, string)
    - identifier immutability across activate→tevilah→reactivate
    - lifecycle: starts inactive; activate marks active; re-activate
      without tevilah raises; tevilah→reactivate succeeds
    - hold_payload/tag require active; both work when active
    - tevilah clears payload refs, intermediate tags, sets inactive
    - tevilah preserves _immutable_identifier
    - THE CI INVARIANT (dynamic walk)
    - assert_shaliach_active: passes for active IshItti, refuses
      inactive, refuses non-IshItti, refuses duck-typed impostor
    - integration: stale-state-across-disposals prevented end-to-end

NOTE ON PYTEST INVOCATION:

When running azazel_processor tests, use `--rootdir=.` from the
package directory. Without it, pytest auto-discovers /Users/scottkarp/
shorashim as rootdir and ignores the local pyproject.toml's
testpaths config. Documented here so future runs match.

  cd azazel_processor
  PYTHONPATH=src:../korban_processor/src python3 -m pytest --rootdir=.

Tests: azazel_processor 71 passed (was 48, +23). korban_processor
unchanged at 203 passed, 1 skipped. No regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 07:49:15 -04:00
Scott KarpandClaude Opus 4.7 a2744c012a azazel_processor: commit 2 — vidui (property 3, enumerative commit)
Pass 4 / Finding F, commit 2 of 10 from BUILD_AZAZEL.md.

Vayikra 16:21 — v'hitvadah alav et kol avonot b'nei Yisrael v'et kol
pish'eihem l'chol chatotam. The high priest confesses ALL iniquities,
ALL transgressions, for ALL sins. The verb is hitpa'el (reflexive) —
the confession is self-addressed before it is addressed to the
system. The kol-three-times structure is mandatory.

ENGINEERING READ:

The commit message for a disposal must enumerate what is being
disposed of. Untagged disposal is not disposal; it is loss. Azazel
does not accept uncommitted writes. The enumeration is produced at
the moment of commit, not retrieved from a pre-existing log.

WHY OPERATOR-PRODUCED, NOT AUTO-DERIVED:

The Mishkan high priest speaks the confession aloud. The system did
not generate it from a classification enum. In software: the caller
of confess() MUST supply the descriptions, not have them
auto-generated from a classification → string table. If the table
generated the confession, the operator could dispose state without
ever naming it themselves; the confession would become an
autogenerated label, not a self-addressed enumeration. That is the
failure mode property 3 defends against.

The convenience helpers (confess_empty_vital, confess_halt,
confess_boundary_violation, confess_malformed_input) DO NOT hide
this requirement. Each requires the operator to supply the specific
item; the helper only routes which category it goes into.

THE THREE CATEGORIES:

  avonot   — iniquities. CATEGORICAL errors: halt-trips
             (avodah_zarah, gilui_arayot, shfichut_damim — the
             cardinal sins). Sins of orientation.
  peshaim  — transgressions. BOUNDARY violations: distinction
             collapses (gilui-shaped), L3 schema rejections.
             Sins of boundary-keeping.
  chatot   — sins. TARGET errors: empty-vital, malformed responses.
             The literal meaning of chet — missing the mark.

Most disposals fill exactly one category. The Vidui dataclass
REQUIRES all three fields (the enumeration is comprehensive at the
shape level), but the lists may be empty. What is forbidden is the
ABSENCE of a slot — and the case where ALL THREE slots are empty
(EmptyConfessionError, no untagged disposal).

CREATED:

  src/azazel_processor/vidui.py            ~250 lines
    Vidui                — frozen dataclass: avonot, peshaim, chatot,
                           confessor, confessed_at (all required;
                           categories are tuples for deep immutability;
                           __post_init__ rejects all-empty, empty-string
                           items, non-string items, anonymous confessor)
    EmptyConfessionError — raised on untagged-disposal attempts
    confess(*, avonot, peshaim, chatot, confessor)
                         — the operator-produced builder
    Vidui.to_text()      — kol-three-times rendering for L3 storage
    Vidui.category_summary()
                         — counts only, no items (for the public
                           observability endpoint, commit 8 / property 9)

  Convenience helpers (each requires operator-supplied item):
    confess_empty_vital(korban_summary, *, confessor)
    confess_halt(condition, detail, *, confessor)
    confess_boundary_violation(violation, *, confessor)
    confess_malformed_input(description, *, confessor)

  test/test_vidui.py                       27 tests covering:
    - structural (exactly five fields, no silent additions)
    - all category-fill combinations succeed
    - all-empty / empty-string / whitespace / non-string items raise
    - confessor required (TypeError) + non-empty/non-whitespace
      validated (EmptyConfessionError)
    - frozen dataclass (cannot mutate after construction)
    - categories are tuples (deep immutability — frozen=True does
      not propagate through container types)
    - confessed_at: timezone-aware UTC, set to current time
    - to_text() preserves kol-three-times structure; omits empty
      categories
    - category_summary() returns counts WITHOUT items
    - all four convenience helpers route correctly + still require
      confessor

Public surface updated in __init__.py.

Tests: azazel_processor 48 passed (was 21, +27). korban_processor
unchanged at 203 passed, 1 skipped. No regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:49:34 -04:00
Scott KarpandClaude Opus 4.7 d06ecd244d azazel_processor: commit 1 — skeleton + stages (properties 11 & 12)
Pass 4 / Finding F, commit 1 of 10 from BUILD_AZAZEL.md. Lays the
load-bearing foundation: classification discipline (property 11) at
the type system level, and unmanaged-destination opacity (property
12) via a Python sentinel. Every later commit in the Azazel build
inherits these defenses for free.

CREATED:

  azazel_processor/
    pyproject.toml                      — Poetry config matching
                                          korban_processor; pythonpath
                                          includes ../korban_processor/src
                                          so the cross-package
                                          classification-discipline test
                                          can import both packages
    src/azazel_processor/
      __init__.py                       — public surface for commit 1:
                                          AzazelStage, AzazelStaged,
                                          AzazelStageError, ERETZ_GEZERAH,
                                          is_eretz_gezerah
      eretz_gezerah.py                  — the destination sentinel; module
                                          __getattr__ refuses any
                                          attribute beyond the two public
                                          names so introspection cannot
                                          discover further handles
      stages.py                          — AzazelStage enum (six
                                          checkpoints from the build doc:
                                          STAGED → CONFESSED → ROUTED →
                                          DESTROYED → ACKNOWLEDGED →
                                          RECORDED), AzazelStaged frozen
                                          dataclass, AzazelStageError
    test/
      test_eretz_gezerah.py             — 11 tests covering: sentinel
                                          identity stable across imports,
                                          not-a-string, no public attrs,
                                          repr leaks no destination info,
                                          identity (is) not equality (==),
                                          impostor with __eq__ rejected,
                                          module __getattr__ refuses
                                          unknown access, CI invariant
                                          forbidding destination-handle
                                          ASSIGNMENTS in source (catches
                                          regressions; mention in
                                          docstrings/__all__/getattr
                                          allowlist is fine)
      test_stages.py                    — 10 tests: intra-Azazel gating
                                          (happy path, mismatch raises
                                          naming both stages, frozen
                                          dataclass, six distinct
                                          checkpoints) and CROSS-PACKAGE
                                          classification discipline:
                                          KorbanStaged and AzazelStaged
                                          are distinct types; enum value
                                          spaces are disjoint; an Azazel
                                          function refuses a KorbanStaged
                                          input and vice versa; no
                                          conversion function exists in
                                          the public surface

PROPERTY COVERAGE THIS COMMIT:

  Property 11 (classification discipline):
    Defended by the type-distinctness of AzazelStaged vs.
    korban_processor.stages.Staged. Cross-package tests verify the
    two cannot be substituted at runtime; the absence of any
    korban_to_azazel/to_korban/from_korban/convert function in the
    public surface is verified by name-shape grep.

  Property 12 (unmanaged destination):
    Defended by the sentinel design: only object identity is exposed;
    no string handle exists; module __getattr__ refuses introspection;
    CI invariant catches assignment-shaped regressions across all of
    azazel_processor/src/.

PENDING (commits 2-10 per BUILD_AZAZEL.md):

  vidui (commit 2 — enumerative commit, property 3)
  shaliach + tevilah (commit 3 — role-separated operator, property 4)
  goral (commit 4 — blind routing, property 2)
  azazel orchestrator + state machine (commit 5 — properties 5, 7, 10)
  protocol envelope (commit 6 — protocol-restricted access, property 8)
  L3 azazel_records (commit 7 — operator-internal half of property 9)
  /peah/azazel_log endpoint (commit 8 — public half of property 9)
  Korban→Azazel routing on empty-vital + chatzotzrot wiring
    (commit 9 — properties 1, 6)
  Closure pass (commit 10)

Tests: azazel_processor 21 passed. korban_processor unchanged at
203 passed, 1 skipped (live-API). No regressions across the boundary.

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