Text Projection requires decoding operation envelopes. No such decoder existed,
and the hole was bigger than the task: the format was WRITE-ONLY for operations.
A bundle's envelope blocks decoded to opaque byte strings, OperationKind had an
encoder and no decoder, nothing outside epiphany-bundle even called decode_block,
and nothing anywhere reconstructed an OperationEnvelope. Chapter 6 holds that a
score's canonical state IS the set of operations committed to it -- so a bundle
could be written and its score never reopened. The envelope's byte layout was
fully pinned in the Binary Format companion. Nobody wrote the inverse.
epiphany_ops::decode_envelope is that inverse. The first thing built on it is
testkit/tests/bundle_reopen.rs: create a bundle from 400 generated envelopes,
commit, take the bytes, reopen from nothing but bytes, decode every envelope,
rebuild the OperationSet, reduce -- and get the same canonical state. That test
could not have been written before this commit.
Strict in two layers, per the P2 lesson. A whole-envelope re-encode-and-compare
guard, sound here because every sequence in this encoding is normalized by its
encoder. Plus per-site checks where the rule deserves its own error and a future
encoder change must not silently relax it: TransposeInterval.targets is a SET
(seq-strictly-increasing; a duplicate is rejected, never absorbed by the BTreeSet
it collects into), and the frozen Transpose.targets is a MULTISET (non-decreasing,
duplicates preserved). That is the rule Push 4a wrote into the wire table and left
for whoever built this decoder. And a bounded count(): a declared count past the
bytes remaining is rejected before it can drive an allocation.
Coverage measured, not assumed -- again. The obvious oracle (gen_envelope_set,
4000 envelopes) reaches only 28 of 31 kinds and 1 of 4 payload variants.
ChangeRegionTimeModel, DeclareTransaction, Registered and all three meta payloads
were untouched, and they hold the trickiest decoders: PositionRemapping, NFC
strings, ResolutionAction, EnvelopeHash. So the exhaustive test drives a match on
OperationKindTag, and the compiler forces a sample for every future kind.
Two mutations verified. Removing the seq-strictly-increasing check still rejects
-- the guard is a real backstop there -- but with the wrong error, so the per-site
check earns its place on the error rather than the verdict. Removing the
whole-envelope guard leaves every round-trip test green, because round-trips only
ever feed canonical bytes; an_unsorted_sequence_is_rejected_by_the_whole_envelope_guard
is the test that locks it, and it fails under that mutation.
A trap worth remembering: PitchId::new(ReplicaId(7), 1) and
OperationId::new(ReplicaId(7), 1) have identical canonical bytes -- typed ids
share their byte form -- so a byte-patching test that searches for an id finds the
envelope's own leading id first.
Gate: fmt clean, clippy 0, 31 targets / 1031 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8, zero golden churn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>