Audit finding 1, reproduced and fixed. Pitch::transposed did its arithmetic in
i32 while the interval's own components are i32, so intermediates overflowed:
diatonic_steps = i32::MAX panicked at `12 * new_octave`, chromatic_steps =
i32::MAX at `semitone + c`. TranspositionInterval::inverse negated i32::MIN.
The comment above the arithmetic even said "widen before arithmetic" -- it
widened i8 to i32, which is exactly not wide enough. All of it now widens to
i64, where the largest intermediate is bounded by ~3.7e9.
Refusing is the contract. Panicking on a value the public type admits is not.
I checked whether this was worse than a panic. The workspace sets
overflow-checks = true in release, but epiphany-core is a library and a
consumer's default release profile has them off, where these expressions wrap.
A 10.5M-case sweep of wrapping-vs-exact arithmetic (175 base pitches x 60225
interval pairs, edges plus random) found ZERO inputs where wrapping produced a
wrong Ok rather than a refusal. So this was a panic, not silent corruption, and
the audit's characterisation was exactly right.
inverse() now returns Option: -i32::MIN is not an i32. An interval whose
inverse cannot be written down is a fact about the type, and a caller composing
undo out of inverses must see it. Both regressions mutation-verified by
restoring the i32 arithmetic and the bare negation.
Also in this commit, two documentation corrections:
- The reducer's test-harness comment claimed both old transpose_* tests "now
reduce ONTO a base and assert the pitch value". They do not, and should not;
DECISIONS.md already said so. The comment now matches.
- The catalog's undo semantics for BOTH transpose kinds claimed value-restoring
undo recovers the pre-transpose pitch from the write chain. Neither kind
records into that chain, so this was false. The frozen Transpose reverts to
its honest pre-Push-4a statement -- undo does not negate it (P11-C8) -- and,
per the freeze doctrine, that is now pinned: making it record would change
what a stored {Transpose, UndoTransaction} history replays to. TransposeInterval's
paragraph states the requirement the next commit implements.
Gate: clippy 0, 30 targets / 985 passed / 0 failed, docs 0 under -D warnings,
conformance 8/8, zero golden churn, catalog rebuilds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>