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>
This commit is contained in:
1 parent
d06ecd244d
commit
a2744c012a
3 files changed
+584
-13
No files matched your search
@@ -0,0 +1,283 @@
|
||||
"""
|
||||
Tests for vidui — property 3 (enumerative commit) of
|
||||
azazel_engineering_reading.md §14.
|
||||
|
||||
The vidui must:
|
||||
- Have all THREE category slots present (avonot/peshaim/chatot),
|
||||
even when individual slots are empty
|
||||
- Refuse construction when ALL slots are empty (no untagged
|
||||
disposal — the failure mode property 3 defends against)
|
||||
- Be immutable after construction (the moment of confession
|
||||
is the moment of fixing)
|
||||
- Carry an attributable confessor (anonymous confession defeats
|
||||
property 7's authorized-routing requirement)
|
||||
- Render to readable text preserving the kol-three-times structure
|
||||
- Expose category counts WITHOUT the items themselves (for the
|
||||
public observability endpoint, commit 8 — property 9)
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from azazel_processor.vidui import (
|
||||
EmptyConfessionError,
|
||||
Vidui,
|
||||
confess,
|
||||
confess_boundary_violation,
|
||||
confess_empty_vital,
|
||||
confess_halt,
|
||||
confess_malformed_input,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# The three-fold structure
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_vidui_has_exactly_the_five_expected_fields():
|
||||
"""Structural test: the dataclass has exactly avonot, peshaim,
|
||||
chatot, confessor, confessed_at — no silent additions and no
|
||||
missing fields. A future contributor who adds a sixth field
|
||||
(or removes one) breaks this and forces an explicit decision."""
|
||||
fields = {f.name for f in Vidui.__dataclass_fields__.values()}
|
||||
assert fields == {"avonot", "peshaim", "chatot", "confessor", "confessed_at"}
|
||||
|
||||
|
||||
def test_confess_with_avonot_only_succeeds():
|
||||
v = confess(avonot=("halt avodah_zarah",), confessor="kohen_gadol")
|
||||
assert v.avonot == ("halt avodah_zarah",)
|
||||
assert v.peshaim == ()
|
||||
assert v.chatot == ()
|
||||
|
||||
|
||||
def test_confess_with_peshaim_only_succeeds():
|
||||
v = confess(peshaim=("ketiv/qere collapse",), confessor="kohen_gadol")
|
||||
assert v.peshaim == ("ketiv/qere collapse",)
|
||||
|
||||
|
||||
def test_confess_with_chatot_only_succeeds():
|
||||
v = confess(chatot=("empty-vital korban",), confessor="kohen_gadol")
|
||||
assert v.chatot == ("empty-vital korban",)
|
||||
|
||||
|
||||
def test_confess_with_all_three_categories_filled_succeeds():
|
||||
v = confess(
|
||||
avonot=("halt 1",),
|
||||
peshaim=("collapse 1",),
|
||||
chatot=("missing 1", "missing 2"),
|
||||
confessor="kohen_gadol",
|
||||
)
|
||||
assert v.avonot == ("halt 1",)
|
||||
assert v.peshaim == ("collapse 1",)
|
||||
assert v.chatot == ("missing 1", "missing 2")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Untagged disposal is forbidden
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_confess_with_all_slots_empty_raises():
|
||||
"""Property 3: no untagged disposal. The act of naming what is
|
||||
being disposed is load-bearing. confess() with all three slots
|
||||
empty is the system trying to dispose without naming — refuse."""
|
||||
with pytest.raises(EmptyConfessionError) as exc:
|
||||
confess(confessor="kohen_gadol")
|
||||
assert "without naming" in str(exc.value).lower() or "loss" in str(exc.value).lower()
|
||||
|
||||
|
||||
def test_confess_with_explicit_empty_tuples_in_all_slots_raises():
|
||||
with pytest.raises(EmptyConfessionError):
|
||||
confess(avonot=(), peshaim=(), chatot=(), confessor="kohen_gadol")
|
||||
|
||||
|
||||
def test_vidui_construction_with_empty_string_item_raises():
|
||||
"""An empty string in a category list would degrade the enumeration
|
||||
to a count — refuse at construction."""
|
||||
with pytest.raises(EmptyConfessionError):
|
||||
confess(avonot=("",), confessor="kohen_gadol")
|
||||
|
||||
|
||||
def test_vidui_construction_with_whitespace_only_item_raises():
|
||||
with pytest.raises(EmptyConfessionError):
|
||||
confess(chatot=(" ",), confessor="kohen_gadol")
|
||||
|
||||
|
||||
def test_vidui_construction_with_non_string_item_raises():
|
||||
with pytest.raises(EmptyConfessionError):
|
||||
confess(avonot=(None,), confessor="kohen_gadol") # type: ignore[arg-type]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Confessor attribution
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_confessor_required():
|
||||
"""Anonymous confession defeats property 7 (authorized routing).
|
||||
confess() must take a confessor (keyword-only) — no default."""
|
||||
with pytest.raises(TypeError):
|
||||
# No confessor argument
|
||||
confess(avonot=("x",)) # type: ignore[call-arg]
|
||||
|
||||
|
||||
def test_confessor_must_be_non_empty_string():
|
||||
with pytest.raises(EmptyConfessionError):
|
||||
confess(avonot=("x",), confessor="")
|
||||
|
||||
|
||||
def test_confessor_must_be_non_whitespace():
|
||||
with pytest.raises(EmptyConfessionError):
|
||||
confess(avonot=("x",), confessor=" ")
|
||||
|
||||
|
||||
def test_confessor_recorded_on_vidui():
|
||||
v = confess(avonot=("x",), confessor="aharon_ha_kohen")
|
||||
assert v.confessor == "aharon_ha_kohen"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Immutability
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_vidui_is_frozen():
|
||||
"""The moment of confession is the moment of fixing. No path to
|
||||
mutating the vidui after construction — that would let an
|
||||
operator re-write what they confessed."""
|
||||
v = confess(avonot=("x",), confessor="kohen_gadol")
|
||||
with pytest.raises((AttributeError, Exception)):
|
||||
v.confessor = "someone_else" # type: ignore[misc]
|
||||
|
||||
|
||||
def test_vidui_categories_are_tuples_not_lists():
|
||||
"""Tuples are immutable; lists are not. Even though the dataclass
|
||||
is frozen, a list-valued field could be mutated in place
|
||||
(frozen=True doesn't propagate through container types). Tuples
|
||||
are the right shape for the enumeration."""
|
||||
v = confess(avonot=("x",), peshaim=("y",), chatot=("z",), confessor="x")
|
||||
assert isinstance(v.avonot, tuple)
|
||||
assert isinstance(v.peshaim, tuple)
|
||||
assert isinstance(v.chatot, tuple)
|
||||
|
||||
|
||||
def test_iterable_input_normalized_to_tuple():
|
||||
"""confess() should accept any iterable for category lists and
|
||||
normalize to tuple at construction. A list passed in cannot be
|
||||
held as a list-valued field — that would re-introduce the
|
||||
mutability gap."""
|
||||
v = confess(avonot=["x", "y"], confessor="kohen_gadol")
|
||||
assert isinstance(v.avonot, tuple)
|
||||
assert v.avonot == ("x", "y")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Timestamp
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_confessed_at_set_to_current_utc_time():
|
||||
before = datetime.now(timezone.utc)
|
||||
v = confess(avonot=("x",), confessor="kohen_gadol")
|
||||
after = datetime.now(timezone.utc)
|
||||
assert before - timedelta(seconds=1) <= v.confessed_at <= after + timedelta(seconds=1)
|
||||
|
||||
|
||||
def test_confessed_at_is_timezone_aware_utc():
|
||||
v = confess(avonot=("x",), confessor="kohen_gadol")
|
||||
assert v.confessed_at.tzinfo is not None
|
||||
assert v.confessed_at.utcoffset() == timedelta(0)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Rendering
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_to_text_preserves_kol_structure_for_all_three():
|
||||
v = confess(
|
||||
avonot=("a1", "a2"),
|
||||
peshaim=("p1",),
|
||||
chatot=("c1",),
|
||||
confessor="kohen",
|
||||
)
|
||||
text = v.to_text()
|
||||
assert "kol avonot" in text
|
||||
assert "kol pish'eihem" in text
|
||||
assert "l'kol chatotam" in text
|
||||
assert "a1" in text and "a2" in text
|
||||
assert "p1" in text
|
||||
assert "c1" in text
|
||||
|
||||
|
||||
def test_to_text_omits_empty_categories():
|
||||
v = confess(avonot=("only-avonot",), confessor="kohen")
|
||||
text = v.to_text()
|
||||
assert "kol avonot" in text
|
||||
assert "kol pish'eihem" not in text
|
||||
assert "l'kol chatotam" not in text
|
||||
|
||||
|
||||
def test_category_summary_returns_counts_without_items():
|
||||
"""The shape exposed by the public observability endpoint —
|
||||
counts only, no item content. Property 9 at the public boundary."""
|
||||
v = confess(
|
||||
avonot=("a1", "a2"),
|
||||
peshaim=("p1",),
|
||||
chatot=(),
|
||||
confessor="kohen",
|
||||
)
|
||||
summary = v.category_summary()
|
||||
assert summary == {"avonot": 2, "peshaim": 1, "chatot": 0}
|
||||
# Items themselves are NOT in the summary — only counts.
|
||||
assert "a1" not in str(summary)
|
||||
assert "p1" not in str(summary)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Convenience helpers
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_confess_empty_vital_routes_to_chatot():
|
||||
"""The literal meaning of chet — missing the mark — is exactly
|
||||
the empty-vital case: the system fired and nothing landed."""
|
||||
v = confess_empty_vital("Claude response on bereishit 1:1", confessor="x")
|
||||
assert v.chatot
|
||||
assert v.avonot == ()
|
||||
assert v.peshaim == ()
|
||||
assert "empty-vital" in v.chatot[0]
|
||||
|
||||
|
||||
def test_confess_halt_routes_to_avonot():
|
||||
"""Halt-trips are categorical orientation failures —
|
||||
yehareg-v'al-ya'avor cardinal sins."""
|
||||
v = confess_halt("avodah_zarah", "claude_model treated as authority", confessor="x")
|
||||
assert v.avonot
|
||||
assert "avodah_zarah" in v.avonot[0]
|
||||
assert "claude_model" in v.avonot[0]
|
||||
|
||||
|
||||
def test_confess_boundary_violation_routes_to_peshaim():
|
||||
"""Boundary-keeping failures (gilui-shaped) — distinction
|
||||
collapses."""
|
||||
v = confess_boundary_violation("ketiv/qere collapsed", confessor="x")
|
||||
assert v.peshaim
|
||||
assert "boundary collapse" in v.peshaim[0]
|
||||
|
||||
|
||||
def test_confess_malformed_input_routes_to_chatot():
|
||||
"""Different from empty-vital — empty-vital is content with no
|
||||
Torah; malformed is content with no SHAPE."""
|
||||
v = confess_malformed_input("truncated JSON, missing closing brace", confessor="x")
|
||||
assert v.chatot
|
||||
assert "malformed" in v.chatot[0]
|
||||
|
||||
|
||||
def test_helpers_still_require_confessor():
|
||||
"""The convenience helpers do not weaken property-3's
|
||||
operator-attribution requirement."""
|
||||
with pytest.raises(TypeError):
|
||||
confess_empty_vital("x") # type: ignore[call-arg]
|
||||
Reference in new issue
Block a user