refactor(message): canonical Outcome sum type on OutboxInnerMessage#145
Merged
Conversation
Replace the three loosely-coupled intent fields (terminal_failure_reason, pending_delay_seconds, state_set) with one canonical `outcome` holding a sum type Ack | Retry(delay) | Terminal(reason). Success becomes an explicit Ack() instead of the implicit "all None" state, and "reason and delay both set" is now unrepresentable. Option A (surgical): outcome is the single canonical field; the three old attributes become read-only @Property views of it, so the five other subscriber readers and every read-assertion in the tests are unchanged. dispatch_one matches on the disjoint variant -- which dissolves the terminal-before-retry ordering invariant (mutually exclusive variants have no ordering dependence). Only the 12 test sites that *wrote* the old fields change, to set `outcome`. DLQFailureReason (the public wire contract) is unchanged -- it is now Terminal.reason's type. dlq_table=None stays bit-for-bit identical. Truth-home promotion in architecture/dlq.md + the CLAUDE.md DLQ invariant line. Behavior-preserving. Full suite green (603 passed, coverage 100%), lint + ty + check-planning clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The deferred deepening from candidate #2.
OutboxInnerMessagerecorded delivery intent across three loosely-coupled fields (terminal_failure_reason,pending_delay_seconds,state_set), where "success" was the implicit "all None" state and "reason and delay both set" was representable-but-never-used. Replace them with one canonicalOutcomesum type.Approach — Option A (canonical outcome + derived views)
outcomeis the single canonical field;_ack/_nack/_reject/allow_deliveryset it. Success is now an explicitAck(); the impossible "reason and delay both set" is unrepresentable.terminal_failure_reason/pending_delay_seconds/state_setbecome read-only@propertyviews ofoutcome— zero-drift derivations, not the parallel-state smell refactor(message): remove dead to_delete intent flag #140 removed. So the five other subscriber readers (_flush_terminal,_flush_retry,_terminal_has_dlq, the shutdown-race guard, metric tags) and every read-assertion in the tests are unchanged.dispatch_onematches on the disjoint variant, which dissolves the terminal-before-retry / before-last_exceptionordering invariant (mutually exclusive variants have no ordering dependence). Only the 12 test sites that wrote the old fields change, to setoutcome.DLQFailureReason(the public wire contract operators query) is unchanged — it is nowTerminal.reason's type.dlq_table=Nonestays bit-for-bit identical.Promotion
architecture/dlq.mdrouting section + theCLAUDE.mdDLQ invariant line updated to the Outcome model. Lightweight change file added.Verification
just test— 603 passed, Postgres 17, coverage 100.00% (message.py+usecase.pyfully covered)just lint— ruff + ty cleanjust check-planning— OKReviewed the diff adversarially:
_acksetsAck()(else success never records state), every setter path assignsoutcome,dispatch_onebehavior is branch-for-branch identical, and no field writes remain.🤖 Generated with Claude Code