Push 4a design gate: pin the transpose algebra, freeze the old operation

An audit reopened P12-K2, whose Pass-12 pin promised the repair would be "a
payload schema-major landing with the Chapter 4 tuning catalog". Both halves
were wrong, and the operation was more broken than the pin admitted.

Measured through EditorSession, not inferred. On a C4: +12 yields alteration
12 (six double-sharps, not C5); +128 clamps to 127 and still reports Applied;
targets [p, p] transposes twice; a non-Cmn position is silently untouched.
transpose(1000) then transpose(-1000) lands on -128, so the operation is not
invertible. Nothing downstream is at fault -- prepass::accidental_ids renders
alteration 12 faithfully. The defect is entirely in what Transpose means.

The false coupling is why this looked big. Pitch has orthogonal scale_position
and acoustic fields. Transposition adds an interval to a scale position;
tuning decides what frequency a scale position sounds at. Adding a fifth to C4
needs no tuning catalog. The same weld had spread: PitchSpaceMismatch was
"Reserved: requires the Chapter 4 tuning catalog" (it reads a discriminant),
and TranspositionInterval was "ADVISORY until the Chapter 4 tuning catalog
pins interval algebra". Push 4 splits: 4a is the algebra and needs no catalog;
4b is the catalog, which has its own blockers (cmn-24 is in the pitch-space
table but cannot exist while Cmn.alteration is i8 semitones).

Ratified by the user:

- New kind, freeze the old. An operation is history; a corrected reduction
  rule would rewrite every score that used one. Transpose (disc 9) keeps its
  exact semantics, now written as normative replay semantics rather than as
  apologies. TransposeInterval takes disc 30. This is cheap: appending a kind
  at >= 30 is a schema MINOR, and the payload's constituents are all major-0
  layouts, so it stamps major 0. No major 3, no migration.

- Diatonic + chromatic interval, reusing TranspositionInterval -- which
  already existed in graph.rs at major 2 for Instrument.transposition, already
  codec'd, byte-for-byte the required pair. Minting an Interval beside it
  would have been a second normative listing of one type, the drift P13-I1
  just closed. Declared once now, in Chapter 2; Chapter 5 references it.

- Atomic refusal. Non-Cmn, AbsoluteHz, or an out-of-range result refuses the
  whole operation. Never saturate, never partially apply. Tombstoned and
  SYSTEM_DERIVED targets are still skipped: a deleted pitch is not an
  untransposable pitch, it is one the operation has nothing to say about.

targets becomes CanonicalSet<PitchId> at the type level, not a Vec plus a
dedup() someone can forget (PitchId's Ord is its canonical byte order). This
was never a convergence bug -- every replica replaying [p, p] double-
transposes identically -- but a canonicalization one. It is free today because
no operation-payload decoder exists yet; once one lands in Push 5, dedup
normalization would change the meaning of stored operations. Push 4a blocks
Push 5, and that is why.

Spec: req:pitch:transposition (algebra + the three refusals), and four
req:opcat:transpose-* requirements. Operation Catalog 0.7.0 -> 0.8.0; Binary
Format 0.6.0 -> 0.7.0 (disc 30, and a seq-strictly-increasing notation whose
decoder must reject a duplicate rather than normalize it away).

This commit is the design gate: the spec now declares MUSTs the code does not
yet satisfy -- editor-core still authors Transpose, and TransposeInterval does
not exist. The implementation follows in this push.

Also recorded: the two existing transpose tests are false locks. Gutting
graph_transpose_pitch leaves both green -- they call base-free reduce(), where
graph is None and the function never runs, and assert only OperationEffect.
Only editor-core's undo_and_redo_a_transpose, three crates away, catches it.

Gate: clippy 0, 30 targets / 964 passed / 0 failed, docs 0 under -D warnings,
conformance 8/8, all three spec documents build with no undefined references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-09 15:25:27 -04:00
parent beb61fca2f
commit 357e8b9eeb
8 changed files with 449 additions and 37 deletions

View File

@ -510,3 +510,44 @@ missing every kind/volta anchor, contradicting its own doc). All flat walks
now consume the method (the classified per-site invariant check keeps its
exhaustive match for message attribution); the index gap is
regression-locked in `indexes_build_and_answer_queries`.
## The interval algebra, and the type that already existed (Push 4a, 2026-07-09)
`req:pitch:transposition` (core Ch2, §"Transposition and the Interval Type")
pins the action of a `TranspositionInterval { d, c }` on a
`PitchSpacePosition::Cmn`. With `n` the nominal's normative discriminant and
`s = nominal.chromatic() + alteration + 12*octave`:
nominal' = CmnNominal((n + d).rem_euclid(7))
octave' = octave + (n + d).div_euclid(7)
alteration' = (s + c) - (nominal'.chromatic() + 12*octave')
The diatonic component alone picks the nominal and octave; the alteration
absorbs the residue. C4 + (7, 12) = C5, not "C with twelve sharps". C4 + (0, 1)
= C#4, so the editor's sharpen keeps its exact current behaviour.
**The type was already here.** `TranspositionInterval` has lived in `graph.rs`
since schema major 2, carrying `Instrument.transposition` (a B-flat clarinet is
`-1` diatonic, `-2` chromatic), already codec'd, already exported. It is
byte-for-byte the pair transposition needs, so Push 4a reuses it rather than
minting an `Interval` beside it. The spec now lists it once, in Chapter 2; the
Chapter 5 `Instrument` block references that listing instead of repeating it.
Two normative listings of one struct is the drift the P13-I1 fix just closed.
Its doc claimed it was "ADVISORY until the Chapter 4 tuning catalog pins
interval algebra". That was the P12-K2 false coupling, repeated. Transposition
acts on `scale_position`; tuning acts on `acoustic`; the two never touch. The
algebra is pinned here with no tuning catalog anywhere in sight. What remains
genuinely unimplemented is the *automatic application* of an instrument's
interval at the written/sounding boundary — nothing respells a written part
into a sounding one — so `Instrument.transposition` stays advisory, for that
reason and not the stated one.
**Refusal, not saturation.** `alteration` and `octave` are `i8`. A
transposition whose result does not fit refuses; so does one against a
non-`Cmn` position (no nominal to move) or an `AcousticRealization::AbsoluteHz`
pitch (which overrides the tuning system, so moving the scale position moves
the notehead without moving the sound). Saturation is the worst possible
failure here because it is invisible: it produces a pitch nobody asked for,
reports success, and destroys the evidence. See `epiphany-ops/DECISIONS.md`
§"Push 4a" for the operation-level consequences and the frozen `Transpose`.

View File

@ -1158,3 +1158,123 @@ referent re-anchoring stays deferred, ratified events-only** (the spanner
discipline; extending the referent index to region/measure tombstones is the
larger change the user parked). Regression:
`create_cross_cutting_spanner_preconditions_region_measure_anchors`.
## Push 4a — the transpose algebra (design gate, 2026-07-09)
`TransposeOp { targets: Vec<PitchId>, chromatic_steps: i32 }` was pinned in
Pass 12 (P12-K2) as a prototype whose repair would be "a payload schema-major
landing with the Chapter 4 tuning catalog". An audit reopened it. Both halves
of that pin were wrong, and the operation had more defects than the pin
admitted.
**What is actually broken.** Measured, not inferred — a probe through
`EditorSession` on a C4:
| `chromatic_steps` | result |
|---|---|
| +1 | C#4 |
| +12 | C4 with `alteration: 12` — six double-sharps, not C5 |
| +128 | `alteration: 127`, silently clamped, reports `Applied` |
| `targets: [p, p]`, +1 | `alteration: 2` |
Also: a non-`Cmn` position is left untouched while the operation reports
`Applied`. And `transpose(1000)` then `transpose(-1000)` lands on
`alteration: -128`, so the operation is not invertible.
Nothing downstream is at fault. `prepass::accidental_ids` faithfully renders
`alteration: 12` as six double-sharps; the engraver draws what it is handed.
The whole defect is in what `Transpose` *means*.
**The false coupling (the reason this looked big).** `Pitch` has two
orthogonal fields: `scale_position` and `acoustic`. Transposition adds an
interval to a *scale position*. Tuning decides what frequency a scale position
sounds at. Adding a fifth to C4 needs no tuning catalog. P12-K2's deferral
welded together two things that do not touch, and the weld propagated: it is
also why `PreconditionFailureReason::PitchSpaceMismatch` was documented as
"Reserved: requires the Chapter 4 tuning catalog" (detecting a non-`Cmn`
position reads a discriminant), and why `TranspositionInterval` was marked
"ADVISORY until the Chapter 4 tuning catalog pins interval algebra". Push 4
therefore splits: **4a is the transpose algebra, and needs no tuning catalog;
4b is the Chapter 4 catalog**, which has its own unrelated blockers (`cmn-24`
is in the spec's pitch-space table but cannot exist while `Cmn.alteration` is
`i8` semitones and a quarter-tone is half of one).
**Four ratified calls (user, 2026-07-09).**
1. **Split 4a / 4b.** Above.
2. **A new operation kind; freeze the old.** An operation is history. A replica
replaying a stored `Transpose` must reconstruct the state its author saw, so
a "corrected" reduction rule would silently rewrite every score that used
one. `Transpose` (wire disc 9) keeps its exact semantics — saturation,
nominal-blindness, duplicate-sensitivity — now written down as *normative
replay semantics* rather than as apologies. `TransposeInterval` takes wire
disc 30 and is what authoring emits.
This turned out to be **cheap**: appending an `OperationKind` discriminant
at ≥ 30 is a schema **minor** (`req:binfmt:kind-discriminants`, the Phase-3
precedent), and every constituent of the new payload — a sequence of ids and
two `i32`s — is a major-0 layout, so it stamps major 0 under minimal
stamping. No schema major 3, no migration, no stored operation changes
meaning. (I told the user it would cost a major before checking. It does
not.)
3. **Diatonic + chromatic interval.** Reuses `TranspositionInterval`, which
**already existed** in `graph.rs` at schema major 2 for `Instrument`'s
written-versus-sounding interval, is already codec'd, and is byte-for-byte
the pair required. Minting a new `Interval` struct would have been a second
normative listing of one type — the drift hazard P13-I1 just closed. The
spec now declares it once, in Chapter 2 where transposition lives, and
Chapter 5 references it.
4. **Atomic refusal.** A target that cannot be faithfully transposed — non-`Cmn`
position, `AbsoluteHz` realization, or an `alteration'`/`octave'` that does
not fit — refuses the *whole* operation, changing nothing. Never saturate,
never partially apply. A chord transposed except for one note is not a
transposed chord.
**Refusal is distinguished from skipping.** Tombstoned and `SYSTEM_DERIVED`
targets are still *skipped*, exactly as for `Transpose`. A deleted pitch is
not an untransposable pitch; it is a pitch the operation has nothing to say
about. An `AbsoluteHz` pitch, by contrast, declares its own frequency: it
can be respelled or resounded, but a transposition claiming both does one
and lies about the other.
**`targets` becomes a set at the type level.** `CanonicalSet<PitchId>` (a
`BTreeSet`), not a `Vec` plus a `dedup()` someone can forget. `PitchId: Ord`
**is** its canonical byte order (locked by `ids.rs`), so the set iterates in
canonical order for free. This is not a convergence bug in the old op — every
replica replaying `[p, p]` double-transposes identically — it is a
canonicalization bug: the catalog said *set*, `sorted_canonical` sorts without
deduplicating, and the reduction is multiset-sensitive.
**Timing matters here.** No operation-payload decoder exists yet (only
`OperationKindTag` implements `CanonicalDecode`), so making `targets` a set is
free *today*. Once the decoder lands — Push 5, the decode-fuzz P2 tranche — any
dedup normalization would silently change the meaning of stored operations.
**Push 4a blocks Push 5**, and that is why.
**Spelling propagation.** Core Chapter 2 already requires that operations which
transpose a pitch produce `SpellingSource::Propagated` attachments. `Transpose`
produces none, and `RespellPitch` can only materialize `UserChosen` — so an
authored spelling survives a transposition still pinned to the notehead it was
authored against. `TransposeInterval` MUST emit the propagated attachment; the
interval's diatonic component already determines the nominal, and the
attachment is how that determination reaches the engraved layer.
**Spec:** `req:pitch:transposition` (core Ch2, the algebra and the three
refusals), `req:opcat:transpose-frozen`, `req:opcat:transpose-interval-targets`,
`req:opcat:transpose-interval-atomic`,
`req:opcat:transpose-interval-spelling`. Operation Catalog 0.7.0 → 0.8.0,
Binary Format 0.6.0 → 0.7.0 (disc 30; the new `seq^⇑` strictly-increasing
notation). New `PreconditionFailureReason`: `AcousticRealizationPinned` (14),
`TranspositionOutOfRange` (15); `PitchSpaceMismatch` (6) un-reserved.
**The two existing transpose tests are false locks.** Verified by mutation:
gutting `graph_transpose_pitch` entirely leaves
`transpose_skips_tombstoned_targets_and_shifts_the_live_ones` and
`transpose_skips_system_derived_targets_p12_k3` both green. They call base-free
`reduce()`, where `graph` is `None` and the function never runs, and they
assert only `OperationEffect`. Only `editor-core`'s `undo_and_redo_a_transpose`
— three crates away — catches it. Both are rewritten against `reduce_onto()`
with asserted pitch values as part of the implementation.

Binary file not shown.

View File

@ -235,7 +235,7 @@
{\Large\scshape\color{epiphanyslate}Binary Format}\\[6pt]
{\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt]
{\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt]
{\normalsize\color{epiphanyink}Version 0.6.0 --- Repeat authoring (OperationKind/Tag 28/29; the born-at-v2 create, the major-0 delete)}\\[4pt]
{\normalsize\color{epiphanyink}Version 0.7.0 --- Transpose algebra (OperationKind/Tag 30; the major-0 minor append, and the strictly-increasing sequence)}\\[4pt]
{\small\color{epiphanyslate}Normative for the byte layouts it defines}
\vfill
\end{titlepage}
@ -1310,7 +1310,12 @@ is the Operation Catalog's (the cited section governs semantics).
\texttt{CanonicalValue} bytes of a \texttt{T}
(Section~\ref{sec:values:canonical-value}); bare identifiers are raw 16
big-endian bytes; ``$\mathrm{seq}^{\uparrow}$'' is a \texttt{push\_seq}
sequence sorted ascending by canonical bytes.
sequence sorted ascending by canonical bytes, and
``$\mathrm{seq}^{\Uparrow}$'' is the same sequence constrained to be
\emph{strictly} increasing --- a set, not a multiset. A decoder \MUST{}
reject a $\mathrm{seq}^{\Uparrow}$ containing a duplicate rather than
normalize it away, since the duplicate is the only evidence that the writer
and the reader disagree about the field's type.
\begingroup\footnotesize
\begin{longtable}{p{0.32in} >{\raggedright\arraybackslash}p{1.5in} >{\raggedright\arraybackslash}p{2.35in} >{\raggedright\arraybackslash}p{1.35in}}
@ -1353,7 +1358,8 @@ sequence sorted ascending by canonical bytes.
\sectionsc{ModifyEvent} \\
\tablenums{9} & \texttt{Transpose} &
$\mathrm{seq}^{\uparrow}$(\texttt{PitchId}) \cat{}
\texttt{chromatic\_steps} (\texttt{i32} LE, 4) &
\texttt{chromatic\_steps} (\texttt{i32} LE, 4). Frozen: the sequence is a
multiset, \emph{not} $\mathrm{seq}^{\Uparrow}$ &
\sectionsc{Transpose} \\
\tablenums{10} & \texttt{InsertIdentifiedPitch} &
\texttt{event} (16) \cat{} $\mathrm{lp}$(\texttt{IdentifiedPitch}) &
@ -1426,6 +1432,11 @@ sequence sorted ascending by canonical bytes.
\tablenums{29} & \texttt{DeleteRepeatStructure} &
\texttt{repeat} (16) &
\sectionsc{Repeat Structures} \\
\tablenums{30} & \texttt{TransposeInterval} &
$\mathrm{seq}^{\Uparrow}$(\texttt{PitchId}) \cat{}
\texttt{diatonic\_steps} (\texttt{i32} LE, 4) \cat{}
\texttt{chromatic\_steps} (\texttt{i32} LE, 4) &
\sectionsc{TransposeInterval} \\
\bottomrule
\end{longtable}
\endgroup
@ -1494,6 +1505,7 @@ discriminants and wrong lengths are decode errors. Append-only past
\tablenums{24} & \texttt{InsertStaff} & \tablenums{25} & \texttt{SetTimeSignature} \\
\tablenums{26} & \texttt{SetTempoSegment} & \tablenums{27} & \texttt{SetStaffLayout} \\
\tablenums{28} & \texttt{CreateRepeatStructure} & \tablenums{29} & \texttt{DeleteRepeatStructure} \\
\tablenums{30} & \texttt{TransposeInterval} & & \\
\bottomrule
\end{longtable}
\endgroup
@ -2607,7 +2619,11 @@ The major is assigned per payload type, exactly as in major~1:
is \texttt{Some}, else \textbf{0};
\texttt{CreateRepeatStructure} $\Rightarrow$ always \textbf{2}
(born at v2); \texttt{DeleteRepeatStructure} $\Rightarrow$
\textbf{0} (a bare identifier under a minor kind append); every
\textbf{0} (a bare identifier under a minor kind append);
\texttt{TransposeInterval} $\Rightarrow$ \textbf{0} (a sequence of
identifiers and two \texttt{i32}s --- every constituent is a major-0
layout, so the kind append is a schema \emph{minor}, and a
\texttt{TransposeInterval} rides in a major-0 block); every
other payload keeps its prior major. A block stamps the maximum
over its payloads.
(Operation names are the Operation Catalog's; core Chapter~6's
@ -3177,6 +3193,20 @@ layouts of Section~\ref{sec:values:representative}.
with the pair. Semantics: Operation Catalog \sectionsc{Repeat
Structures}, 0.7.0; re-anchoring row (nearest-surviving-anchor across
every anchor site, cascade when none): core Chapter~6 rule table. \\
\today & Operation vocabularies & 0.7.0 --- Transpose algebra (Push~4a):
appends \texttt{OperationKind} \tablenums{30}
(\texttt{TransposeInterval}: $\mathrm{seq}^{\Uparrow}$(\texttt{PitchId})
\cat{} two \texttt{i32}s) and the matching \texttt{OperationKindTag}
\tablenums{30}. Every constituent is a major-0 layout, so the payload
stamps major~\tablenums{0} and the discriminant is a schema-\emph{minor}
vocabulary append --- no major~3. Introduces the
$\mathrm{seq}^{\Uparrow}$ notation (strictly increasing; a decoder
\MUST{} reject a duplicate rather than normalize it) and marks
\tablenums{9} (\texttt{Transpose}) frozen, its
$\mathrm{seq}^{\uparrow}$ being a multiset. No existing assignment
changed. Semantics: Operation Catalog \sectionsc{TransposeInterval},
0.8.0; algebra: core Chapter~2, \sectionsc{Transposition and the
Interval Type}. \\
\bottomrule
\end{longtable}

Binary file not shown.

View File

@ -1018,6 +1018,7 @@ specification.
\end{rationale}
\subsection{The \texttt{CmnNominal} Type}
\label{sec:pitch:nominal}
\begin{lstlisting}[language=Rust]
#[repr(u8)]
@ -1027,6 +1028,96 @@ pub enum CmnNominal { C = 0, D = 1, E = 2, F = 3, G = 4, A = 5, B = 6 }
The discriminants are normative: they define the diatonic step ordering
used by transposition algorithms.
\subsection{Transposition and the \texttt{Interval} Type}
\label{sec:pitch:interval}
Transposition acts on \emph{scale position}, not on acoustic realization.
It moves a pitch's analytical identity through the pitch space and leaves
the tuning context that sounds it untouched. A transposition therefore
requires no tuning catalog (Chapter~\ref{ch:tuning}) --- tuning decides what
frequency a scale position sounds at, not how to add an interval to one. It
requires an interval:
\begin{lstlisting}[language=Rust]
/// A signed interval: the transposition primitive. Defined here and
/// referenced by Instrument.transposition (Chapter 5), which is the
/// written-versus-sounding interval of a transposing instrument
/// (a B-flat clarinet is -1 diatonic, -2 chromatic).
pub struct TranspositionInterval {
/// Signed diatonic steps: how far the nominal moves along C..B.
/// A perfect fifth up is +4 (C -> G); an octave up is +7.
pub diatonic_steps: i32,
/// Signed chromatic steps, in semitones. A perfect fifth up
/// is +7; an octave up is +12.
pub chromatic_steps: i32,
}
\end{lstlisting}
Both components are load-bearing. The diatonic component fixes the
\emph{spelling} --- which nominal, hence which staff line the notehead
occupies --- and the chromatic component fixes the \emph{sound}. Neither
determines the other: an augmented second $(+1, +3)$ and a minor third
$(+2, +3)$ sound alike and are written differently, while a diminished
sixth $(+5, +7)$ and a perfect fifth $(+4, +7)$ are written a step apart
and sound the same. A scalar semitone count cannot express the difference,
and an implementation carrying only one is not transposing but altering.
\begin{requirement}
\label{req:pitch:transposition}
Let $n$ be the \texttt{nominal}'s normative discriminant
(Section~\ref{sec:pitch:nominal}), and let a
\texttt{PitchSpacePosition::Cmn} position's \emph{absolute semitone} be
\[
s \;=\; \texttt{nominal.chromatic()} \;+\; \texttt{alteration}
\;+\; 12 \cdot \texttt{octave}.
\]
Transposing that position by \texttt{TranspositionInterval \{ d, c \}}
\MUST{} produce
\begin{align*}
\texttt{nominal}' &= \textsc{CmnNominal}\big((n + d) \bmod 7\big), \\
\texttt{octave}' &= \texttt{octave} + \lfloor (n + d) / 7 \rfloor, \\
\texttt{alteration}' &= (s + c) - \big(\texttt{nominal}'\texttt{.chromatic()}
+ 12 \cdot \texttt{octave}'\big),
\end{align*}
where $\bmod$ and $\lfloor \cdot \rfloor$ are Euclidean (the remainder is
non-negative). The diatonic component alone selects the nominal and the
octave; the alteration absorbs exactly the residue.
A transposition \MUST{} be refused --- atomically, changing nothing --- when
any target satisfies any of:
\begin{itemize}
\item its \texttt{scale\_position.position} is not \texttt{Cmn}, so the
\texttt{Interval} above has no defined action on it;
\item its \texttt{acoustic.realization} is
\texttt{AcousticRealization::AbsoluteHz}, which overrides the tuning
system, so moving the scale position would move the notehead without
moving the sound;
\item the computed $\texttt{alteration}'$ or $\texttt{octave}'$ does not
fit its field.
\end{itemize}
An implementation \MUSTNOT{} saturate, clamp, or partially apply a
transposition in any of these cases.
\end{requirement}
\begin{rationale}
The refusal cases are the three ways a transposition can silently lie. A
non-CMN position has no nominal to move, so a semitone shift would have to
invent one. An \texttt{AbsoluteHz} pitch declares its own frequency: it can
be respelled or it can be resounded, but a transposition that claims to do
both does one and pretends to the other. And saturation is the worst of the
three, because it is invisible --- a transposition that clamps has produced
a pitch nobody asked for, reported success, and destroyed the information
needed to notice.
Refusing atomically, rather than skipping the offending target, follows from
the same reasoning: a chord transposed except for one note is not a
transposed chord, it is a different chord. Contrast the treatment of
\emph{tombstoned} targets, which are skipped rather than refused --- a
deleted pitch is not an untransposable pitch, it is a pitch the operation
has nothing to say about.
\end{rationale}
\section{Acoustic Realization}
\label{sec:pitch:acoustic}
@ -4731,17 +4822,18 @@ pub struct Instrument {
/// verbatim and never interprets them. Empty is the default.
pub struct SoundConfiguration(pub Vec<u8>);
/// A written-versus-sounding transposition (defined schema major 2).
/// Structural only: the diatonic and chromatic step counts of the
/// interval (e.g., B-flat clarinet = -1 diatonic, -2 chromatic).
/// Semantically ADVISORY until the Chapter 4 tuning catalog pins
/// interval algebra (the same P12-K2 pinning discipline as the
/// Transpose operation); nothing in the core resolves it to
/// frequencies or respells through it yet.
pub struct TranspositionInterval {
pub diatonic_steps: i32,
pub chromatic_steps: i32,
}
// TranspositionInterval (defined schema major 2) is specified in
// Chapter 2, "Transposition and the Interval Type": the diatonic and
// chromatic step counts of an interval, and its action on a scale
// position. Instrument.transposition is the written-versus-sounding
// interval of a transposing instrument (a B-flat clarinet is
// -1 diatonic, -2 chromatic).
//
// Its ACTION is pinned (Chapter 2). What remains unimplemented is its
// AUTOMATIC APPLICATION at the instrument boundary: nothing in the core
// yet respells a written part into a sounding one, or resolves either to
// a frequency. Instrument.transposition is therefore still advisory --
// but for that reason, not for want of interval algebra.
/// One playable member of an unpitched instrument (defined schema
/// major 2): the identifier UnpitchedEvent.instrument_member

Binary file not shown.

View File

@ -226,7 +226,7 @@
{\Large\scshape\color{epiphanyslate}Operation Catalog}\\[6pt]
{\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt]
{\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt]
{\normalsize\color{epiphanyink}Version 0.7.0 --- Schema-major-2 repeat authoring (the CreateRepeatStructure / DeleteRepeatStructure pair)}\\[4pt]
{\normalsize\color{epiphanyink}Version 0.8.0 --- Transpose algebra (TransposeInterval; the frozen Transpose)}\\[4pt]
{\small\color{epiphanyslate}Normative for the operation kinds it defines}
\vfill
\end{titlepage}
@ -305,6 +305,26 @@ Appended vocabulary (minor, append-only):
\texttt{RecreateContentMismatch} (13, P12-K9) in
\texttt{PreconditionFailureReason}.
\medskip
\noindent\textbf{Version 0.8.0 (Push~4a, transpose algebra).} Adds one
operation kind, \texttt{TransposeInterval}
(Section~\ref{sec:k0:transpose-interval}), carrying the existing
\texttt{TranspositionInterval} of core Chapter~2. It transposes scale
position, refuses
atomically rather than saturating, takes a genuine \emph{set} of targets, and
propagates the resulting spelling. \texttt{Transpose}
(Section~\ref{sec:k0:transpose}) is \textbf{frozen}: its saturation, its
nominal-blindness, and its duplicate-sensitivity are pinned as normative
replay semantics and no longer authored. This supersedes the 0.6.0 pin of the
Transpose prototype (P12-K2), which anticipated a payload schema-major landing
with the tuning catalog; neither proved necessary. Appended vocabulary (minor,
append-only): \texttt{AcousticRealizationPinned} (14) and
\texttt{TranspositionOutOfRange} (15) in \texttt{PreconditionFailureReason};
\texttt{PitchSpaceMismatch} (6) is un-reserved and now produced, its previous
dependency on the tuning catalog having been an error --- detecting a
non-\texttt{Cmn} position reads a discriminant, not a pitch-space registry.
% ===========================================================================
\chapter{The Catalog Framework}
\label{ch:framework}
@ -589,34 +609,65 @@ tombstoned pitch is the deferred resurrection case, P11-C8).
a no-op; tombstoning a pitch runs the cross-cutting re-anchoring table over any
structure that referenced it (see DeleteEvent).
\section{Transpose}
\section{Transpose (frozen; replay only)}
\label{sec:k0:transpose}
\begin{requirement}
\label{req:opcat:transpose-frozen}
\texttt{Transpose} is \textbf{frozen}. Its reduction rule is fixed at the
semantics described in this section, including the saturation and the
duplicate-sensitivity, for as long as any stored operation carries it. A
conforming implementation \MUST{} reduce it exactly as specified here and
\MUSTNOT{} emit it from new authoring; new transpositions are authored as
\texttt{TransposeInterval} (Section~\ref{sec:k0:transpose-interval}).
\end{requirement}
\textbf{Payload schema.} \texttt{TransposeOp \{ targets: Vec<PitchId>,
chromatic\_steps: i32 \}}. Pitch identifiers are preserved; only acoustic content
changes.
chromatic\_steps: i32 \}}. Pitch identifiers are preserved. The operation
rewrites \emph{scale position} (the \texttt{Cmn} alteration), not acoustic
content.
\textbf{Canonical encoding.} The canonically-ordered \texttt{targets} set, then
\texttt{chromatic\_steps} as a little-endian \texttt{i32}.
\textbf{Canonical encoding.} \texttt{targets} in ascending canonical-byte order,
then \texttt{chromatic\_steps} as a little-endian \texttt{i32}. The sequence is
\emph{sorted but not deduplicated}: it is a multiset on the wire.
\textbf{Reduction rule.} An order-dependent content overwrite. Each live target
pitch is shifted by \texttt{chromatic\_steps}; reduction is order-dependent in the
general case (interval composition need not commute), so the resolved value is
the composition in canonical reduction order. In this prototype
\texttt{chromatic\_steps} is a minimal CMN alteration shift that commutes except
at the alteration's \texttt{i8} saturation bound; rich interval algebra is
deferred (Chapter~4 tuning catalog; P12-K2). \emph{Pinned (Pass~12):} the
prototype semantics --- CMN alteration shift with documented \texttt{i8}
saturation --- are the declared v1 behavior; replacing the payload with a
faithful interval representation is a \emph{payload schema-major} under the
Binary Format companion's evolution rule, landing with the tuning catalog.
\textbf{Reduction rule.} An order-dependent content overwrite. For each live,
non-system target \emph{occurrence} in \texttt{targets}, the target's \texttt{Cmn}
\texttt{alteration} is shifted by \texttt{chromatic\_steps} and clamped to the
\texttt{i8} bound. Consequences, all normative for replay:
\begin{itemize}
\item A target appearing $k$ times is shifted $k$ times.
\item The nominal and octave never change: transposing C4 by $+12$ yields C4
with \texttt{alteration} $= 12$, not C5.
\item A shift past the \texttt{i8} bound saturates silently, and the operation
still reports \texttt{Applied}. Transposition under this rule is therefore
not invertible.
\item A target whose position is not \texttt{Cmn} is left unchanged, and the
operation still reports \texttt{Applied}.
\end{itemize}
\begin{rationale}
Every bullet above is a defect (P12-K2), and none of them may be repaired in
place. An operation is history: a replica replaying a stored
\texttt{Transpose} must reconstruct the state its author saw, and a
``corrected'' reduction rule would silently rewrite every score that ever
used one. The defects are therefore \emph{pinned}, not fixed, and the repair
ships as a new operation kind whose discriminant no historical operation
carries. Ratified Push~4a; supersedes the Pass~12 pin, which anticipated a
payload schema-major and a dependency on the tuning catalog. Neither proved
necessary: transposition acts on scale position, and appending an operation
kind is a schema \emph{minor} (Binary Format companion, requirement
\texttt{req:binfmt:kind-discriminants}).
\end{rationale}
\textbf{Conflict cases.} None --- composition is deterministic in canonical order
(a deterministic repair, not a conflict).
\textbf{Undo semantics.} Transpose mints nothing, so the prototype's minted-object
undo (Section~\ref{sec:k0:undo}) does not negate it; an inverse-interval undo is a
Phase-3 refinement (P11-C8).
undo (Section~\ref{sec:k0:undo}) does not negate it; value-restoring undo recovers
the pre-transpose pitch from the write chain rather than by applying an inverse
interval --- which, under saturation, would not exist.
\textbf{Re-anchoring.} Tombstoned targets are skipped (the transpose applies only
to live pitches). \texttt{SYSTEM\_DERIVED}-namespace targets are likewise
@ -625,6 +676,82 @@ and an in-place alteration shift would desynchronize the content from the id's
derivation inputs. A transpose whose live targets are \emph{all} system-derived
reduces as a precondition no-op (\texttt{SystemDerivedContentImmutable}).
\section{TransposeInterval}
\label{sec:k0:transpose-interval}
\textbf{Payload schema.} \texttt{TransposeIntervalOp \{ targets:
CanonicalSet<PitchId>, interval: TranspositionInterval \}}, reusing the
diatonic/chromatic pair the core specification defines in Chapter~2,
\sectionsc{Transposition and the Interval Type} --- the same value an
\texttt{Instrument} carries as its written-versus-sounding interval. Pitch
identifiers are preserved; the operation rewrites scale position and leaves
acoustic realization untouched.
\begin{requirement}
\label{req:opcat:transpose-interval-targets}
\texttt{targets} is a \textbf{set}. It \MUST{} be encoded as a sequence in
ascending canonical-byte order with \emph{strictly increasing} elements; a
decoder \MUST{} reject a sequence containing a duplicate rather than
normalize it. A transposition is a function of \emph{which} pitches it names,
never of how many times it names them.
\end{requirement}
\textbf{Canonical encoding.} The strictly-increasing \texttt{targets} sequence,
then \texttt{interval.diatonic\_steps} and \texttt{interval.chromatic\_steps},
each a little-endian \texttt{i32}.
\textbf{Reduction rule.} An order-dependent content overwrite. Tombstoned and
\texttt{SYSTEM\_DERIVED} targets are skipped, exactly as for \texttt{Transpose};
if no mutable target remains the operation reduces as the corresponding
precondition no-op. Otherwise, every remaining target \MUST{} be transposable
under core specification requirement \texttt{req:pitch:transposition}, and each
is transposed by \texttt{interval}.
\begin{requirement}
\label{req:opcat:transpose-interval-atomic}
If any mutable target is \emph{not} transposable, the operation \MUST{}
reduce to a no-op that changes no pitch, reporting the precondition failure
of the first such target in canonical order. It \MUSTNOT{} transpose the
remaining targets. The three cases and their
\texttt{PreconditionFailureReason} discriminants:
\begin{itemize}
\item a non-\texttt{Cmn} \texttt{scale\_position.position} $\Rightarrow$
\texttt{PitchSpaceMismatch} (6);
\item an \texttt{AcousticRealization::AbsoluteHz} realization
$\Rightarrow$ \texttt{AcousticRealizationPinned} (14);
\item an \texttt{alteration}$'$ or \texttt{octave}$'$ that does not fit
its field $\Rightarrow$ \texttt{TranspositionOutOfRange} (15).
\end{itemize}
\end{requirement}
\begin{requirement}
\label{req:opcat:transpose-interval-spelling}
A \texttt{TransposeInterval} that changes a pitch's spelling \MUST{} record
that spelling as a \texttt{SpellingAttachment} with source
\texttt{SpellingSource::Propagated}, per the core specification's Chapter~2,
\sectionsc{Spelling Sources}. The attachment's \texttt{from} is the
transposed pitch's own identifier, which the operation preserves.
\end{requirement}
\begin{rationale}
Without the propagated attachment the spelling pre-pass re-infers a spelling
for the moved pitch, and an authored \texttt{UserChosen} spelling from before
the transposition stays pinned to a notehead that has moved --- a C-sharp the
author spelled deliberately, transposed up a fifth, re-inferred as A-flat
against their wish. The interval's diatonic component already determines the
correct nominal; the attachment is how that determination survives into the
engraved layer.
\end{rationale}
\textbf{Conflict cases.} None --- composition is deterministic in canonical order.
\textbf{Undo semantics.} As \texttt{Transpose}: nothing is minted, and
value-restoring undo recovers the pre-transpose pitch from the write chain. Here
an inverse interval does exist ($(-d, -c)$), because the reduction never
saturates; undo does not rely on it.
\textbf{Re-anchoring.} As \texttt{Transpose}.
\section{CreateCrossCutting}
\label{sec:k0:create-cross-cutting}
@ -1236,10 +1363,12 @@ undo) design, which subsumes this question.
\emph{Still deferred} (P11-C8, narrowed): re-introducing content tombstoned by
\emph{delete} primitives (a deterministic resurrection needs a system-derived
identifier derivation the ratified closed tag set does not yet include);
\texttt{Transpose} inversion (interval algebra, P12-K2); and
identifier derivation the ratified closed tag set does not yet include); and
\texttt{Cascade}'s dependent-closure computation --- \texttt{Cascade} remains
\texttt{StrictInverse} over the same set.
\texttt{StrictInverse} over the same set. \texttt{Transpose} inversion is no
longer listed: \texttt{TransposeInterval} has an exact inverse $(-d, -c)$
because it never saturates (Push~4a, closing P12-K2), though value-restoring
undo does not need one. The frozen \texttt{Transpose} still has none.
% ===========================================================================
\chapter{v0 \texorpdfstring{$\rightarrow$}{->} v1 Payload Migration}