diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index 550ba0b..850531e 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -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`. diff --git a/crates/epiphany-ops/DECISIONS.md b/crates/epiphany-ops/DECISIONS.md index 4ef8b44..1145019 100644 --- a/crates/epiphany-ops/DECISIONS.md +++ b/crates/epiphany-ops/DECISIONS.md @@ -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, 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` (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. diff --git a/spec/binary_format.pdf b/spec/binary_format.pdf index 4dcdc3f..320c7d5 100644 Binary files a/spec/binary_format.pdf and b/spec/binary_format.pdf differ diff --git a/spec/binary_format.tex b/spec/binary_format.tex index 5d9b4d7..7968ad6 100644 --- a/spec/binary_format.tex +++ b/spec/binary_format.tex @@ -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} diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index 7e3eab9..53a9394 100644 Binary files a/spec/core_spec.pdf and b/spec/core_spec.pdf differ diff --git a/spec/core_spec.tex b/spec/core_spec.tex index 3c4cd7b..b6658e9 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -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); -/// 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 diff --git a/spec/operation_catalog.pdf b/spec/operation_catalog.pdf index d03dc53..f3f3475 100644 Binary files a/spec/operation_catalog.pdf and b/spec/operation_catalog.pdf differ diff --git a/spec/operation_catalog.tex b/spec/operation_catalog.tex index 50156fa..15a0c29 100644 --- a/spec/operation_catalog.tex +++ b/spec/operation_catalog.tex @@ -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, -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, 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}