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
+583
-12
No files matched your search
@@ -6,23 +6,24 @@ uv'd'mim, Yoma 62a). One goat to HaShem (processing); one goat to
|
|||||||
Azazel (disposal). The two channels are provisioned at parity, from
|
Azazel (disposal). The two channels are provisioned at parity, from
|
||||||
the same pool, with assignment blind to the operator.
|
the same pool, with assignment blind to the operator.
|
||||||
|
|
||||||
Build state: commit 1 — Skeleton + stages. See BUILD_AZAZEL.md for
|
Build state: commit 2 — vidui added. See BUILD_AZAZEL.md for the full
|
||||||
the full 10-commit staging plan and the 12-property → code mapping
|
10-commit staging plan and the 12-property → code mapping from
|
||||||
from azazel_engineering_reading.md §14.
|
azazel_engineering_reading.md §14.
|
||||||
|
|
||||||
This commit covers:
|
Properties defended so far:
|
||||||
- Property 11 (classification discipline) — AzazelStage and
|
- Property 3 (enumerative commit) — Vidui + confess()
|
||||||
AzazelStaged are non-interconvertible with korban_processor.
|
- Property 11 (classification discipline) — AzazelStage*
|
||||||
Staged.
|
- Property 12 (unmanaged destination) — ERETZ_GEZERAH
|
||||||
- Property 12 (unmanaged destination) — ERETZ_GEZERAH sentinel
|
|
||||||
is the only legitimate way to refer to the disposal destination.
|
|
||||||
|
|
||||||
Public surface so far:
|
Public surface:
|
||||||
AzazelStage, AzazelStaged, AzazelStageError — stage-gating types
|
AzazelStage, AzazelStaged, AzazelStageError — stage gating
|
||||||
ERETZ_GEZERAH, is_eretz_gezerah — the destination
|
ERETZ_GEZERAH, is_eretz_gezerah — the destination
|
||||||
|
Vidui, confess, EmptyConfessionError — enumerative commit
|
||||||
|
confess_empty_vital, confess_halt,
|
||||||
|
confess_boundary_violation,
|
||||||
|
confess_malformed_input — categorization helpers
|
||||||
|
|
||||||
Pending in subsequent commits:
|
Pending in subsequent commits:
|
||||||
vidui (commit 2 — enumerative commit)
|
|
||||||
shaliach + tevilah (commit 3 — role-separated operator)
|
shaliach + tevilah (commit 3 — role-separated operator)
|
||||||
goral (commit 4 — blind routing)
|
goral (commit 4 — blind routing)
|
||||||
azazel orchestrator (commit 5 — full state machine)
|
azazel orchestrator (commit 5 — full state machine)
|
||||||
@@ -31,6 +32,15 @@ Pending in subsequent commits:
|
|||||||
|
|
||||||
from azazel_processor.eretz_gezerah import ERETZ_GEZERAH, is_eretz_gezerah
|
from azazel_processor.eretz_gezerah import ERETZ_GEZERAH, is_eretz_gezerah
|
||||||
from azazel_processor.stages import AzazelStage, AzazelStaged, AzazelStageError
|
from azazel_processor.stages import AzazelStage, AzazelStaged, AzazelStageError
|
||||||
|
from azazel_processor.vidui import (
|
||||||
|
EmptyConfessionError,
|
||||||
|
Vidui,
|
||||||
|
confess,
|
||||||
|
confess_boundary_violation,
|
||||||
|
confess_empty_vital,
|
||||||
|
confess_halt,
|
||||||
|
confess_malformed_input,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ERETZ_GEZERAH",
|
"ERETZ_GEZERAH",
|
||||||
@@ -38,4 +48,11 @@ __all__ = [
|
|||||||
"AzazelStage",
|
"AzazelStage",
|
||||||
"AzazelStaged",
|
"AzazelStaged",
|
||||||
"AzazelStageError",
|
"AzazelStageError",
|
||||||
|
"Vidui",
|
||||||
|
"EmptyConfessionError",
|
||||||
|
"confess",
|
||||||
|
"confess_empty_vital",
|
||||||
|
"confess_halt",
|
||||||
|
"confess_boundary_violation",
|
||||||
|
"confess_malformed_input",
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""
|
||||||
|
Vidui — the enumerative commit (property 3 of azazel_engineering_reading.md §14).
|
||||||
|
|
||||||
|
Vayikra 16:21 — וְהִתְוַדָּה עָלָיו אֶת־כָּל־עֲוֺנֹת בְּנֵי יִשְׂרָאֵל
|
||||||
|
וְאֶת־כָּל־פִּשְׁעֵיהֶם לְכָל־חַטֹּאתָם.
|
||||||
|
And he shall confess upon it ALL the iniquities (avonot) of the children
|
||||||
|
of Israel, and ALL their transgressions (peshaim), for ALL their sins
|
||||||
|
(chatot).
|
||||||
|
|
||||||
|
The verb v'hitvadah is hitpa'el — REFLEXIVE. The confession is
|
||||||
|
self-addressed before it is addressed to the system. The high priest
|
||||||
|
must STATE the errors to himself in the act of transferring them. You
|
||||||
|
discover what you are disposing of in the act of disposing of it.
|
||||||
|
|
||||||
|
The word kol appears three times: kol avonot, kol pish'eihem, l'chol
|
||||||
|
chatotam. The enumeration is comprehensive by specification. All three
|
||||||
|
categories. All items in each category named.
|
||||||
|
|
||||||
|
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 THE VIDUI IS OPERATOR-PRODUCED, NOT AUTO-DERIVED:
|
||||||
|
|
||||||
|
The Mishkan high priest speaks the confession aloud. The system did
|
||||||
|
not generate it for him from a classification enum. In software terms:
|
||||||
|
the caller of confess() MUST supply the descriptions, not have them
|
||||||
|
auto-generated. If a classification → string table generated the
|
||||||
|
confession, the operator could dispose state without ever naming it
|
||||||
|
themselves; the confession would be an autogenerated label, not a
|
||||||
|
self-addressed enumeration. That's the failure mode property 3
|
||||||
|
defends against.
|
||||||
|
|
||||||
|
The convenience helpers (confess_empty_vital, confess_halt, etc.)
|
||||||
|
DO NOT hide this requirement. Each helper still requires the operator
|
||||||
|
to supply the specific item (the korban summary, the halt detail,
|
||||||
|
the boundary description). The helper just routes which category it
|
||||||
|
goes into; the operator still names the item.
|
||||||
|
|
||||||
|
THE THREE CATEGORIES:
|
||||||
|
|
||||||
|
avonot — iniquities. CATEGORICAL errors: halt-trips
|
||||||
|
(avodah_zarah, gilui_arayot, shfichut_damim — the
|
||||||
|
cardinal sins of the system that triggered yehareg
|
||||||
|
v'al ya'avor). Sins of orientation.
|
||||||
|
|
||||||
|
peshaim — transgressions. BOUNDARY violations: distinction
|
||||||
|
collapses (gilui-shaped — read/write conflation,
|
||||||
|
ketiv/qere collapse, specification/interpretation
|
||||||
|
merge), L3 schema rejections (the trace was not
|
||||||
|
L1-grounded). Sins of boundary-keeping.
|
||||||
|
|
||||||
|
chatot — sins. TARGET errors: things offered but unprocessable
|
||||||
|
(empty-vital korban, malformed external responses,
|
||||||
|
content that arrived but had no extractable Torah).
|
||||||
|
Sins of target-missing (the literal meaning of chet —
|
||||||
|
missing the mark).
|
||||||
|
|
||||||
|
Most disposals fill exactly ONE category. The Vidui dataclass
|
||||||
|
REQUIRES all three fields, but the lists may be empty. What is
|
||||||
|
forbidden is the absence of the slot, not the absence of items in
|
||||||
|
the slot. An untagged disposal — confess() called with all three
|
||||||
|
slots empty — raises EmptyConfessionError; you cannot dispose
|
||||||
|
without naming.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyConfessionError(ValueError):
|
||||||
|
"""Raised when confess() is called with all three slots empty.
|
||||||
|
|
||||||
|
An untagged disposal — the failure mode property 3 defends
|
||||||
|
against. There must be SOMETHING in at least one of avonot,
|
||||||
|
peshaim, or chatot. Empty in all three would mean disposal
|
||||||
|
without naming, which is loss disguised as disposal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Vidui:
|
||||||
|
"""The structured enumerative commit.
|
||||||
|
|
||||||
|
All four fields are required. avonot/peshaim/chatot are tuples
|
||||||
|
(immutable) so the dataclass-frozen guarantee extends through
|
||||||
|
the contents — no path to mutating the confession after the
|
||||||
|
moment of confession.
|
||||||
|
|
||||||
|
confessed_at and confessor are non-optional: anonymous or
|
||||||
|
untimed confession would defeat the operator-attribution
|
||||||
|
requirement (property 7's `authorized_by` field on the
|
||||||
|
AzazelRecord traces back to confessor).
|
||||||
|
"""
|
||||||
|
|
||||||
|
avonot: tuple[str, ...]
|
||||||
|
peshaim: tuple[str, ...]
|
||||||
|
chatot: tuple[str, ...]
|
||||||
|
confessor: str
|
||||||
|
confessed_at: datetime = field(
|
||||||
|
default_factory=lambda: datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
# All three slots present (the dataclass enforces this) AND
|
||||||
|
# at least one is non-empty (property 3: no untagged
|
||||||
|
# disposal). The dataclass cannot enforce the "at least one
|
||||||
|
# non-empty" constraint at the type level; we enforce it
|
||||||
|
# here so misuse fires at construction, not at L3 write.
|
||||||
|
if not (self.avonot or self.peshaim or self.chatot):
|
||||||
|
raise EmptyConfessionError(
|
||||||
|
"Cannot confess with all three categories empty. "
|
||||||
|
"Property 3 (enumerative commit) requires that "
|
||||||
|
"SOMETHING be named — disposal without naming is "
|
||||||
|
"loss, not disposal. Vayikra 16:21 says kol three "
|
||||||
|
"times; at minimum, one of the kol's must be filled."
|
||||||
|
)
|
||||||
|
# All entries non-empty strings — empty descriptions would
|
||||||
|
# silently weaken the enumeration without raising.
|
||||||
|
for category, items in (
|
||||||
|
("avonot", self.avonot),
|
||||||
|
("peshaim", self.peshaim),
|
||||||
|
("chatot", self.chatot),
|
||||||
|
):
|
||||||
|
if any((not isinstance(s, str)) or (not s.strip()) for s in items):
|
||||||
|
raise EmptyConfessionError(
|
||||||
|
f"Confession {category} contains an empty or "
|
||||||
|
f"non-string entry: {items!r}. Each item must be a "
|
||||||
|
f"named description; empty strings would degrade the "
|
||||||
|
f"enumeration to a count."
|
||||||
|
)
|
||||||
|
if not isinstance(self.confessor, str) or not self.confessor.strip():
|
||||||
|
raise EmptyConfessionError(
|
||||||
|
"Vidui requires a named confessor (operator/kohen "
|
||||||
|
"identifier). Anonymous confession defeats property 7 "
|
||||||
|
"(authorized routing) — the routing decision must be "
|
||||||
|
"ownable back to the confessor."
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_text(self) -> str:
|
||||||
|
"""Render the vidui as readable confession text — the
|
||||||
|
Vayikra 16:21 structure preserved.
|
||||||
|
|
||||||
|
Each non-empty category is rendered as a kol-prefixed clause.
|
||||||
|
The output is what gets persisted in L3 azazel_records.vidui
|
||||||
|
and what the public observability endpoint summarizes (in
|
||||||
|
categorical form only — the public endpoint shows which
|
||||||
|
categories were filled, not the items themselves).
|
||||||
|
"""
|
||||||
|
parts = []
|
||||||
|
if self.avonot:
|
||||||
|
parts.append("kol avonot: " + "; ".join(self.avonot))
|
||||||
|
if self.peshaim:
|
||||||
|
parts.append("kol pish'eihem: " + "; ".join(self.peshaim))
|
||||||
|
if self.chatot:
|
||||||
|
parts.append("l'kol chatotam: " + "; ".join(self.chatot))
|
||||||
|
return " | ".join(parts)
|
||||||
|
|
||||||
|
def category_summary(self) -> dict[str, int]:
|
||||||
|
"""Return a {category → item count} map. The shape exposed by
|
||||||
|
the public observability endpoint (commit 8) — counts only,
|
||||||
|
no item content. Property 9 (observable disposal) at the
|
||||||
|
public boundary."""
|
||||||
|
return {
|
||||||
|
"avonot": len(self.avonot),
|
||||||
|
"peshaim": len(self.peshaim),
|
||||||
|
"chatot": len(self.chatot),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def confess(
|
||||||
|
*,
|
||||||
|
avonot: Iterable[str] = (),
|
||||||
|
peshaim: Iterable[str] = (),
|
||||||
|
chatot: Iterable[str] = (),
|
||||||
|
confessor: str,
|
||||||
|
) -> Vidui:
|
||||||
|
"""Produce a vidui — the operator-produced enumerative commit.
|
||||||
|
|
||||||
|
The caller MUST supply the items in each category they are
|
||||||
|
confessing. There is no auto-derivation from a classification
|
||||||
|
table; the act of naming is the operator's, not the system's.
|
||||||
|
|
||||||
|
All three category arguments default to empty tuples for
|
||||||
|
convenience (most disposals fill exactly one), but at least
|
||||||
|
one must be non-empty — see EmptyConfessionError.
|
||||||
|
|
||||||
|
confessor is required keyword-only — there is no default.
|
||||||
|
Anonymous confession is not a confession; the kohen-gadol
|
||||||
|
identity is what makes the routing decision authorizable
|
||||||
|
(property 7).
|
||||||
|
"""
|
||||||
|
return Vidui(
|
||||||
|
avonot=tuple(avonot),
|
||||||
|
peshaim=tuple(peshaim),
|
||||||
|
chatot=tuple(chatot),
|
||||||
|
confessor=confessor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Convenience helpers — categorize at the call site
|
||||||
|
# ============================================================
|
||||||
|
#
|
||||||
|
# Each helper still REQUIRES the operator to supply the specific
|
||||||
|
# item being confessed. The helper only routes which category the
|
||||||
|
# item goes into; it does not auto-generate the description.
|
||||||
|
# Property 3 stays defended.
|
||||||
|
|
||||||
|
|
||||||
|
def confess_empty_vital(
|
||||||
|
korban_summary: str, *, confessor: str
|
||||||
|
) -> Vidui:
|
||||||
|
"""Confess a chatot-class disposal: an offering whose zerikah
|
||||||
|
returned no vital content. The operator must supply a summary
|
||||||
|
naming what was offered; the helper routes it into chatot
|
||||||
|
(target-missing). The literal meaning of chet — missing the mark
|
||||||
|
— is exactly the empty-vital case: the system fired and
|
||||||
|
nothing landed."""
|
||||||
|
return confess(
|
||||||
|
chatot=(f"empty-vital offering: {korban_summary}",),
|
||||||
|
confessor=confessor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def confess_halt(
|
||||||
|
condition: str, detail: str, *, confessor: str
|
||||||
|
) -> Vidui:
|
||||||
|
"""Confess an avonot-class disposal: content that triggered a
|
||||||
|
halt rule (avodah_zarah, gilui_arayot, shfichut_damim — the
|
||||||
|
yehareg-v'al-ya'avor sins).
|
||||||
|
|
||||||
|
The condition is the halt name; the detail is the operator's
|
||||||
|
description of what specifically tripped the halt. The vidui
|
||||||
|
preserves both so the L3 record names the orientation-failure
|
||||||
|
that occurred."""
|
||||||
|
return confess(
|
||||||
|
avonot=(f"halt {condition}: {detail}",),
|
||||||
|
confessor=confessor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def confess_boundary_violation(
|
||||||
|
violation: str, *, confessor: str
|
||||||
|
) -> Vidui:
|
||||||
|
"""Confess a peshaim-class disposal: content that collapsed a
|
||||||
|
distinction the system requires kept (read/write, ketiv/qere,
|
||||||
|
specification/interpretation, etc.). The operator names the
|
||||||
|
specific collapse."""
|
||||||
|
return confess(
|
||||||
|
peshaim=(f"boundary collapse: {violation}",),
|
||||||
|
confessor=confessor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def confess_malformed_input(
|
||||||
|
description: str, *, confessor: str
|
||||||
|
) -> Vidui:
|
||||||
|
"""Confess a chatot-class disposal: an input that arrived at the
|
||||||
|
intake but could not be parsed or processed at all (malformed
|
||||||
|
JSON, truncated response, encoding failure). Different from
|
||||||
|
empty-vital — empty-vital is content with no Torah; malformed is
|
||||||
|
content with no SHAPE."""
|
||||||
|
return confess(
|
||||||
|
chatot=(f"malformed input: {description}",),
|
||||||
|
confessor=confessor,
|
||||||
|
)
|
||||||
@@ -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