Pass 4 / Finding F, commit 5 of 10 from BUILD_AZAZEL.md. Largest
commit so far; pulls together vidui + shaliach + goral + stages and
adds the state machine + in-flight persistence.
THE STATE MACHINE (six checkpoints, persisted):
STAGED → AzazelStaged envelope tagged; payload held by shaliach
CONFESSED → vidui carried into the staged value
ROUTED → routing committed (always ERETZ_GEZERAH); authorized_by
+ shaliach attribution recorded
DESTROYED → payload references cleared from shaliach;
gc.collect() called to release; payload becomes
unreachable from any orchestrator-visible code path
ACKNOWLEDGED → out-of-band ack callback invoked (commit 9 wires
Chatzotzrot.signal(:moadim) here)
RECORDED → AzazelRecord persisted via callback (commit 7 wires
the cross-language L3 file-watcher bridge here)
After RECORDED completes, the in-flight checkpoint file is shredded —
the disposal is now permanent in L3, the in-flight artifact must not
linger.
PROPERTY COVERAGE:
Property 5 (provisioned infrastructure / sukkot):
Each checkpoint persists state to <in_flight_dir>/<case_ref>.json
via write-then-rename (atomic against crash mid-write). list_in_flight
returns partial disposals so resume tooling can pick them up.
The Mishkan analog: the sukkot along the wilderness route where
the ish itti can rest if the disposal halts.
Property 7 (authorized routing):
AzazelRecord.authorized_by carries the kohen_gadol identifier;
AzazelRecord.shaliach carries the ish itti identifier. The
routing destination is the ERETZ_GEZERAH sentinel (property 12).
The routing decision is OWNED and traceable; the destination
is not.
Property 10 (hard termination):
The DESTROYED checkpoint clears handler._payload_refs and calls
gc.collect(). The AzazelRecord contains the vidui — what was
disposed — but NOT the disposed payload itself. The to_storage_dict
serialization explicitly excludes payload-shaped fields.
CREATED:
src/azazel_processor/azazel_record.py ~95 lines
AzazelRecord — frozen dataclass; fields:
case_ref, vidui, routing, authorized_by, shaliach,
triggering_call, goral_outcome_id, disposed_at
.to_storage_dict() — JSON-safe serialization for L3 + in-flight
src/azazel_processor/azazel.py ~225 lines
AzazelInput — what is being disposed
dispatch_for_disposal(input, *, handler, confess_with,
authorized_by, in_flight_dir,
acknowledger, recorder, goral_outcome_id)
— the single legitimate
entry point; walks the
six checkpoints
list_in_flight(in_flight_dir) — surfaces partial disposals
DEFAULT_IN_FLIGHT_DIR — /tmp/shorashim/azazel/in_flight
test/test_azazel.py 21 tests covering:
HAPPY PATH:
- dispatch returns AzazelRecord
- record carries authorization + shaliach attribution
- record routes to ERETZ_GEZERAH
- record carries the full vidui
PROPERTY 10 CI INVARIANTS:
- record does not carry payload (by attribute name)
- serialized record does not contain the payload's marker string
- STRUCTURAL: AzazelRecord dataclass field set has no
payload-shaped names (catches future regression)
- STRUCTURAL: no recover/undispose/restore/rehydrate/retrieve
function in azazel_processor public surface (catches
future regression)
- payload references cleared from shaliach at DESTROYED
PROPERTY 5 CHECKPOINT TESTS:
- in-flight file shredded after RECORDED
- checkpoints persist incrementally (verified by inspecting the
file at ack/rec callback fire-time)
- list_in_flight returns partial disposals
- list_in_flight empty when no disposals
- persistence is atomic (file is valid JSON at every visit)
ROLE GATE (property 4 still enforced):
- dispatch refuses inactive handler
- dispatch refuses non-IshItti
FILENAME SAFETY:
- case_ref with slash → ValueError
- case_ref with NUL → ValueError
AzazelRecord VALIDATION:
- authorized_by required
- shaliach required
- to_storage_dict omits payload
- frozen (cannot mutate)
DEFAULTS:
acknowledger and recorder default to no-op callables. This is
deliberate for commit 5 — the state machine works end-to-end without
external side effects, and commits 7 (L3) and 9 (chatzotzrot) plug
the real handlers into the same callback slots. Tests can supply
their own to inspect mid-route state.
Tests: azazel_processor 109 passed (was 88, +21). korban_processor
unchanged at 203 passed, 1 skipped. No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>