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:
parent
beb61fca2f
commit
357e8b9eeb
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue