P13-S2: the alteration is space-relative, and the core fails closed

`cmn-24` was declared in the built-in pitch-space table as "CMN extended with
24-EDO quarter-tone accidentals" while Chapter 2 fixed a `Cmn` position's
absolute semitone at `nominal.chromatic() + alteration + 12*octave` for every
space, unqualified. Two ratified MUSTs, each correct in its own chapter,
jointly unsatisfiable -- P13-I1's two-listings drift in another costume.

The tracker parked this as "a data-model major". It is not.
`req:binfmt:frozen-layout` names `PitchSpacePosition` as one of four open
value-layer vocabularies, so three of the four available options cost no schema
event at all. The one that *is* a major -- regrading `alteration` onto a finer
fixed unit -- is also the only one that rewrites the canonical bytes of every
pitch ever authored, and it buys a single grid while `edo-31`/`53`/`72` stay
unrepresentable. It is explicitly ruled out.

Ratified instead: a `Cmn` alteration and a `CmnChromatic` modification are
denominated in steps of the enclosing pitch space's chromatic layer. One rule,
not a special case for one catalog row -- it settles `cmn-24`, `maqam-base`,
and `PitchSpaceModification::CmnChromatic` together. `cmn-12`'s step is the
semitone, so every score in existence keeps its meaning and **not one canonical
byte moves**; `canonical_pitch_bytes` writes the space id before the position,
so a `cmn-24` E-half-flat and a `cmn-12` E-flat already derive different
`PitchId`s.

The defect was larger than `cmn-24`. Nothing bound a `Cmn` position to a
12-chromatic space: `ScalePosition { space: "edo-31", position: Cmn { .. } }`
constructed, validated, encoded and transposed, and meant nothing. Neither
`Pitch::transposed` nor `twelve_tet_semitone` ever read the space. Until Push
4b resolves `PitchSpaceId` to a `DiatonicOverChromatic` structure, both now
fail closed outside provable built-in `cmn-12` -- a capability check, not a
claim that the identifier defines the structure. False refusal for a
score-defined 12-chromatic space is accepted over silently wrong arithmetic.

No wire change. `TransposeRefusal::PitchSpaceUnavailable` is a new *diagnostic*
mapping to the existing `PreconditionFailureReason::PitchSpaceMismatch` (6);
appending a discriminant for a guard Push 4b must delete would have reserved a
permanent wire value for a temporary mechanism, in exactly the artifact the
fail-closed ruling exists to protect. Assignments 10-15 are untouched.
Operation Catalog 0.9.0 records the broadened case *and* amends the 0.8.0
rationale, which asserted that discriminant 6 never consults a pitch-space
registry -- true of the non-`Cmn` check, false of this one.

Two spec-side fixes found in review, neither visible to any test: the
`TranspositionInterval` listing had lost its `pub chromatic_steps: i32,`
declaration to a comment rewrite, leaving a one-field struct above the sentence
"Both components are load-bearing"; and `pitch.rs` still documented
`alteration` as "in semitones", contradicting the requirement landing beside
it. Both new requirements are now cited from the code that implements them --
the label checker catches cited-but-undefined, not the reverse.

Push 4b keeps the registry work and loses the contradiction. P13-S4 is the last
item open in Batch 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-22 09:00:09 -04:00
parent 043c18cabf
commit e7ebd8ac9c
12 changed files with 657 additions and 104 deletions

View File

@ -555,35 +555,38 @@ not silent corruption. `inverse()` now returns `Option`, because an interval
whose inverse is not representable is a fact about the type.)
`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`.
refuses; so does one against a non-`Cmn` position (no nominal to move), a
`Cmn` position whose enclosing chromatic structure cannot be established, or
an `AcousticRealization::AbsoluteHz` pitch (which overrides the tuning system,
so moving the scale position moves the notehead without moving the sound).
Saturation or guessed pitch-space arithmetic is the worst possible failure
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`.
## Parked: Push 4b (the Chapter 4 tuning catalog) — what must be decided first
## Push 4b (the Chapter 4 tuning catalog) — remaining implementation blockers
Push 4a proved that transposition needs no tuning catalog. What the catalog is
still needed for — resolving a scale position to a frequency, and applying an
instrument's `transposition` at the written/sounding boundary — remains open,
and Chapter 4 is not implementable as written. Verified blockers:
Push 4a proved that built-in `cmn-12` transposition needs no tuning catalog.
P13-S2 subsequently made the general algebra space-relative: interpreting a
`Cmn` alteration requires the enclosing space's chromatic cardinality and
nominal map. The specification contradiction is resolved, but the registry
implementation is now an explicit part of Push 4b. Remaining blockers:
- **`cmn-24` cannot exist** (P13-S2). The built-in pitch-space table declares it
as "CMN extended with 24-EDO quarter-tone accidentals", but
`PitchSpacePosition::Cmn.alteration` is an `i8` of *whole semitones*. Either
`cmn-24` is not `Cmn`-representable, or `alteration` changes unit — a
data-model major.
- **Resolve pitch-space structure, then remove the interim name gate.** Until
the registry exists, `Pitch::transposed` and the `twelve_tet_*` helpers fail
closed for `Cmn` positions outside built-in `cmn-12`. Push 4b must resolve
`PitchSpaceId` to `PositionStructure::DiatonicOverChromatic`, use its
`chromatic_positions_per_octave` and `nominal_to_chromatic` mapping, and
replace identifier recognition rather than preserving it as policy
(P13-S2; `req:pitch:alteration-unit`,
`req:pitch:space-capability-refusal`).
- **The core stores only the default space, tuning, and reference.** The
overrides, accidental extensions, and SMuFL target Chapter 4 requires are
absent, and none of the catalog/resolver types exist
(`ScoreTuningContext` in `graph.rs` is the whole surface today).
- **Chapter 4's nine requirement blocks are unlabeled**, so none is
independently citable in a conformance claim. This is not special to
Chapter 4 — 169 of 207 core_spec requirements are unlabeled (P13-S1) — but it
means Push 4b cannot declare conformance requirement-by-requirement without
first labelling what it implements.
P13-S1 removed the former requirement-label blocker; Chapter 4's requirements
are now independently citable.
Two further claims from the Push-4a audit are **unverified** and should be
checked, not inherited: that the JI dimension convention conflicts with its own

View File

@ -240,6 +240,12 @@ pub enum TransposeRefusal {
/// The position is not [`PitchSpacePosition::Cmn`], so it has no nominal
/// for the interval's diatonic component to move.
NonCmnPosition,
/// The enclosing pitch space's chromatic layer or nominal mapping cannot
/// be established, so applying the interval would require guessed
/// arithmetic (`req:pitch:space-capability-refusal`). The pre-registry
/// implementation recognizes only built-in `cmn-12`; Push 4b replaces that
/// identifier check with structural pitch-space resolution.
PitchSpaceUnavailable,
/// The pitch's [`AcousticRealization::AbsoluteHz`] overrides the tuning
/// system, so moving the scale position would move the notehead without
/// moving the sound.
@ -252,7 +258,8 @@ impl Pitch {
/// Transposes this pitch by `interval`, per Chapter 2
/// `req:pitch:transposition`.
///
/// With `n` the nominal's normative discriminant and the absolute semitone
/// With `n` the nominal's normative discriminant and, for the proven
/// `cmn-12` structure, the absolute semitone
/// `s = nominal.chromatic() + alteration + 12*octave`, transposing by
/// `{ d, c }` yields
///
@ -262,6 +269,10 @@ impl Pitch {
/// alteration' = (s + c) - (nominal'.chromatic() + 12*octave')
/// ```
///
/// Until Push 4b provides structural pitch-space resolution, a CMN
/// position outside built-in `cmn-12` is refused rather than interpreted
/// with this 12-chromatic map.
///
/// The diatonic component alone selects the nominal and octave; the
/// alteration absorbs exactly the residue. So `C4 + (7, 12)` is `C5`, not
/// "C with twelve sharps", and `C4 + (0, 1)` is `C#4`.
@ -284,6 +295,9 @@ impl Pitch {
else {
return Err(TransposeRefusal::NonCmnPosition);
};
if self.scale_position.space.as_str() != "cmn-12" {
return Err(TransposeRefusal::PitchSpaceUnavailable);
}
// Widen to `i64` before any arithmetic. The `i8` bound is a *result*
// constraint, not an intermediate one, so an octave that overflows on
@ -325,7 +339,10 @@ pub enum PitchSpacePosition {
/// Common Music Notation: diatonic nominal + chromatic alteration + octave.
Cmn {
nominal: CmnNominal,
/// Chromatic alteration in semitones, conventionally `-2..=+2`.
/// Chromatic alteration in steps of the enclosing pitch space's
/// chromatic layer (`req:pitch:alteration-unit`), conventionally
/// `-2..=+2`. One step is a semitone in `cmn-12` and a quarter-tone in
/// `cmn-24`, so a flat is respectively `-1` and `-2`.
alteration: i8,
/// Scientific Pitch Notation octave; middle C is C4.
octave: i8,
@ -469,31 +486,38 @@ impl Pitch {
}
/// The 12-TET pitch class (`0..=11`) of this pitch's *scale position*, when
/// that is computable from the position alone: CMN positions and 12-EDO
/// integer positions. Returns `None` for positions whose 12-TET class is
/// not determinable without a tuning resolver (JI vectors, non-12 EDOs,
/// registered grammars). Octave-blind — for sounding comparison use
/// its 12-chromatic structure is established: CMN positions in built-in
/// `cmn-12` and 12-EDO integer positions. Returns `None` for unresolved CMN
/// spaces and positions whose 12-TET class is not determinable without a
/// tuning resolver (JI vectors, non-12 EDOs, registered grammars).
/// Octave-blind — for sounding comparison use
/// [`Pitch::twelve_tet_semitone`].
pub fn twelve_tet_class(&self) -> Option<u8> {
self.twelve_tet_semitone().map(|s| s.rem_euclid(12) as u8)
}
/// The *absolute* 12-TET semitone of this pitch's scale position, octave
/// included, when computable from the position alone. For CMN this is
/// included, when its 12-chromatic structure is established. For built-in
/// `cmn-12` this is
/// `octave*12 + nominal.chromatic() + alteration` (so C4 and C5 differ by a
/// full octave); for 12-EDO integer positions it is the absolute `index`.
/// `None` for positions not determinable without a tuning resolver.
/// `None` for unresolved CMN spaces
/// (`req:pitch:space-capability-refusal`) and positions not determinable
/// without a tuning resolver.
///
/// The CMN and 12-EDO frames use different zero references, so the absolute
/// value is only meaningful *within* a frame; [`Pitch::enharmonic_equivalent`]
/// compares only same-frame positions for that reason.
/// value is only meaningful *within* a frame;
/// [`Pitch::enharmonic_equivalent`] compares only same-frame positions for
/// that reason.
pub fn twelve_tet_semitone(&self) -> Option<i32> {
match &self.scale_position.position {
PitchSpacePosition::Cmn {
nominal,
alteration,
octave,
} => Some(*octave as i32 * 12 + nominal.chromatic() as i32 + *alteration as i32),
} if self.scale_position.space.as_str() == "cmn-12" => {
Some(*octave as i32 * 12 + nominal.chromatic() as i32 + *alteration as i32)
}
PitchSpacePosition::Integer { space_size, index } if *space_size == 12 => Some(*index),
_ => None,
}
@ -1260,6 +1284,19 @@ mod tests {
);
}
#[test]
fn unresolved_cmn_space_refuses_transposition_and_twelve_tet_conversion() {
let mut p = cmn(CmnNominal::E, -1, 4);
p.scale_position.space = PitchSpaceId::new("edo-31");
assert_eq!(
p.transposed(iv(4, 7)),
Err(TransposeRefusal::PitchSpaceUnavailable)
);
assert_eq!(p.twelve_tet_semitone(), None);
assert_eq!(p.twelve_tet_class(), None);
}
#[test]
fn a_transposition_refuses_a_pitch_pinned_to_a_frequency() {
// AbsoluteHz overrides the tuning system: moving the scale position
@ -1307,10 +1344,9 @@ mod tests {
let other_frame = Pitch {
scale_position: ScalePosition {
space: PitchSpaceId::new("cmn-19"),
position: PitchSpacePosition::Cmn {
nominal: CmnNominal::C,
alteration: 0,
octave: 4,
position: PitchSpacePosition::Integer {
space_size: 12,
index: 48,
},
},
acoustic: AcousticPitch {
@ -1318,6 +1354,7 @@ mod tests {
realization: AcousticRealization::Implicit,
},
};
assert_eq!(other_frame.twelve_tet_semitone(), Some(48));
assert_eq!(range.contains(&other_frame), None);
}
@ -1353,12 +1390,27 @@ mod tests {
#[test]
fn enharmonic_requires_the_same_pitch_space() {
// Same 12-TET class but different spaces -> not directly comparable.
let mut other_space = cmn(CmnNominal::C, 1, 4); // C#4
other_space.scale_position.space = PitchSpaceId::new("edo-31");
// Both operands are independently computable as the same absolute
// 12-TET semitone. The rejection therefore proves the space-frame
// branch rather than passing through conversion unavailability.
let cis = cmn(CmnNominal::C, 1, 4);
assert!(cis.enharmonic_equivalent(&cmn(CmnNominal::D, -1, 4))); // same space
assert!(!cis.enharmonic_equivalent(&other_space)); // different space
let other_space = Pitch {
scale_position: ScalePosition {
space: PitchSpaceId::new("edo-31"),
position: PitchSpacePosition::Integer {
space_size: 12,
index: 49,
},
},
acoustic: AcousticPitch {
tuning: TuningReference::Inherit,
realization: AcousticRealization::Implicit,
},
};
assert_eq!(cis.twelve_tet_semitone(), Some(49));
assert_eq!(other_space.twelve_tet_semitone(), Some(49));
assert!(cis.enharmonic_equivalent(&cmn(CmnNominal::D, -1, 4)));
assert!(!cis.enharmonic_equivalent(&other_space));
}
#[test]

View File

@ -1185,19 +1185,22 @@ 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).
**The false coupling, and its limit.** `Pitch` has two orthogonal fields:
`scale_position` and `acoustic`. Transposition adds an interval to a scale
position; tuning decides what frequency that position sounds at. Push 4a
therefore needed no tuning resolver for the built-in `cmn-12` arithmetic.
It also corrected the claim that every
`PreconditionFailureReason::PitchSpaceMismatch` needed the Chapter 4 catalog:
detecting a non-`Cmn` position reads only its discriminant.
P13-S2 later exposed the boundary of that result. Generalized `Cmn`
transposition is space-relative, so its chromatic cardinality and nominal map
do require pitch-space registry resolution. Until Push 4b lands that resolver,
the core refuses a `Cmn` position outside provable built-in `cmn-12`, and
`TransposeInterval` maps that distinct capability refusal to the existing
`PitchSpaceMismatch` (6) effect. Push 4 therefore remains split: 4a owns the
operation algebra and frozen replay semantics; 4b owns structural resolution,
not a second transposition rule.
**Four ratified calls (user, 2026-07-09).**

View File

@ -143,13 +143,13 @@ pub enum PreconditionFailureReason {
PositionOutsideRegion,
/// A pitch-space or tuning-context precondition failed.
///
/// Produced by `TransposeInterval` against a target whose
/// `scale_position.position` is not `Cmn`, so the interval's diatonic
/// component has no nominal to move (operation_catalog
/// §TransposeInterval). This was once documented as reserved pending the
/// Chapter 4 tuning catalog; that was an error — detecting a non-`Cmn`
/// position reads a discriminant, not a pitch-space registry. A genuine
/// tuning-context precondition would also land here.
/// Produced by `TransposeInterval` when a target position is not `Cmn`, so
/// the interval's diatonic component has no nominal to move, or when a CMN
/// target's enclosing chromatic structure cannot be established
/// (operation_catalog §TransposeInterval). Detecting the former reads only
/// the position discriminant. The latter intentionally fails closed until
/// Push 4b replaces built-in identifier recognition with structural
/// pitch-space resolution.
PitchSpaceMismatch,
/// The operation targeted a voice that does not exist or is tombstoned.
VoiceMissing,

View File

@ -5816,7 +5816,8 @@ impl<'a> Reducer<'a> {
Ok(next) => next,
Err(refusal) => {
let reason = match refusal {
TransposeRefusal::NonCmnPosition => {
TransposeRefusal::NonCmnPosition
| TransposeRefusal::PitchSpaceUnavailable => {
PreconditionFailureReason::PitchSpaceMismatch
}
TransposeRefusal::AcousticPinned => {
@ -9460,6 +9461,28 @@ mod tests {
assert_eq!(cmn_of(&pitch_of(&score, pid)), (CmnNominal::C, 0, 127));
}
#[test]
fn unresolved_cmn_space_maps_to_canonical_pitch_space_mismatch() {
let mut unresolved = cmn_pitch(CmnNominal::E, -1, 4);
unresolved.scale_position.space = epiphany_core::PitchSpaceId::new("cmn-24");
let expected = unresolved.clone();
let (base, pid) = base_with_pitch(unresolved);
let (effect, score) = run_transpose(&base, &[pid], interval(4, 14));
assert_eq!(
effect,
OperationEffect::NoOp {
reason: NoOpReason::PreconditionFailedUnderReduction {
reason: PreconditionFailureReason::PitchSpaceMismatch,
},
}
);
let mut canonical = Vec::new();
effect.encode_canonical(&mut canonical);
assert_eq!(canonical, vec![4, 3, 6]);
assert_eq!(pitch_of(&score, pid), expected);
}
#[test]
fn transpose_interval_propagates_the_spelling_it_determined() {
// req:opcat:transpose-interval-spelling. Core Ch2: "editing operations

View File

@ -9,9 +9,9 @@ use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
const CORE_REQUIREMENT_COUNT: usize = 207;
const SUITE_REQUIREMENT_COUNT: usize = 277;
const SUITE_LABEL_COUNT: usize = 277;
const CORE_REQUIREMENT_COUNT: usize = 209;
const SUITE_REQUIREMENT_COUNT: usize = 279;
const SUITE_LABEL_COUNT: usize = 279;
/// The normative chapter-to-area assignment. Keeping this as data makes adding a
/// requirement under the wrong chapter fail without encoding chapter names in

View File

@ -47,11 +47,11 @@ Three candidates, so the pass reopens per the house rule. P13-S3 arrived as a
**live incorrectness** — a Push-4a follow-up audit found that the spelling-set
chain introduced to make `TransposeInterval` undoable had only one writer, so
undo erased an ordinary `RespellPitch` on either side of it. It is resolved.
S1 and S2 remain open.
S1, S2, and S3 are resolved; S4 remains open.
| Id | One-line statement | Filed in | Status |
|---|---|---|---|
| P13-S1 | **169 of core_spec's 207 `requirement` blocks carry no `\label`**, so no conformance claim can cite them. Chapter 4 (`Tuning Systems and Pitch Spaces`) is 9/9 unlabeled and Chapter 11 (`Determinism Contract`) is 15/15, but the gap is universal, not local: `Semantic Operations` 24/27, `The Score Graph` 22/28, `Pitch` 10/13. The requirements *are* normative and *are* implemented; they simply cannot be named. Every `req:*` label the repo cites was added ad hoc by the pass that needed it | this file | **resolved** (all 207 core_spec requirements labelled, suite 277/277; and the pass found that labelling alone was insufficient — no document *numbered* its requirements, so a `\label` bound to the enclosing section and 61 of 207 shared a rendered number, one shared by six. All six documents now carry a real counter. Locked by `requirement_labels.rs`) |
| P13-S2 | `cmn-24` is declared in the built-in pitch-space table (`core_spec.tex` §"Built-in Catalog") as "CMN extended with 24-EDO quarter-tone accidentals", but **cannot be represented**: `PitchSpacePosition::Cmn.alteration` is an `i8` documented as *whole semitones*, and a quarter-tone is half of one. Either the space is not `Cmn`-representable (and needs `Integer`/`Registered`), or `alteration` needs a finer unit — a data-model major | `crates/epiphany-core/DECISIONS.md` (Push 4b blockers) | **open** (blocks Push 4b) |
| P13-S2 | `cmn-24` was declared in the built-in pitch-space table as CMN with quarter-tone accidentals while `PitchSpacePosition::Cmn.alteration`, transposition, and `CmnChromatic` were all specified in fixed semitones. The specification now denominates both CMN alteration sites and interval chromatic steps in the enclosing space's chromatic layer, with `cmn-24`'s map fixed explicitly. Until Push 4b provides structural registry resolution, the core fails closed outside provable built-in `cmn-12` instead of applying guessed 12-chromatic arithmetic | `spec/PLAN_P13S2_CMN24.md`; `crates/epiphany-core/DECISIONS.md` | **resolved** (schema and wire layout unchanged; `PitchSpaceUnavailable` maps to existing `PitchSpaceMismatch` 6; Operation Catalog 0.9.0; registry-backed generalization remains a Push 4b implementation blocker) |
| P13-S3 | The `engraved_spelling_chain` introduced with `TransposeInterval`'s undo had a **single writer**. `RespellPitch` mutates the same graph attachments but recorded only on `respell_chain`, so (a) undoing a transposed transaction after a prior respell restored the pitch and **erased the respell**, and (b) a respell landing canonically *after* the transaction was invisible to the chain, so a `StrictInverse` undo reported `Applied` and **wiped the newer authoring** instead of refusing as superseded. A `BestEffort` undo could also restore the pre-transpose pitch while leaving a spelling authored against the transposed one | `crates/epiphany-ops/DECISIONS.md` (P13-S3) | **resolved** (both operations now record on the shared key; pitch value + spelling set undo as one unit; new `req:opcat:spelling-set-chain`. The chain stays *physically* separate from `respell_chain`, which is `RespellPitch`'s LWW conflict state — folding transposes in would make a concurrent respell conflict with a transpose and move the canonical bytes of every existing history) |
| P13-S4 | **No labelled requirement governs vertical-band *heights*.** Pass 12 twice recorded a behavioural fix to the inter-staff solve — realizing an `InterStaffGap` band's declared `preferred_height` rather than a constructor default — and both times cited `req:layoutir:vertical-bands`, which never existed. The two real band requirements (`req:layoutir:primitive-band-ownership`, `req:layoutir:resolved-band-ownership`) govern *ownership*: which band a primitive belongs to, and that a resolved primitive retains it. Nothing states what a band's height means or that the solver must realize it, so the shipped behaviour is unspecified and the log invented a name for the gap | this file | **open** (found in P13-S1 review: an agent 'corrected' the dangling citations to the two ownership requirements, which replaced a visibly broken pointer with a silently wrong one) |

406
spec/PLAN_P13S2_CMN24.md Normal file
View File

@ -0,0 +1,406 @@
# P13-S2 — `cmn-24` cannot exist: scope and plan
Status: **implemented and verified.**
Prepared against `master` @ `043c18c`. Every claim below was checked against the
source; where I ran a probe I give the file and line.
**Landing constraint:** this plan and the Chapter 2 amendment land atomically.
The implementation defines and cites `req:pitch:alteration-unit` in the same
change, so requirement-citation integrity remains a commit invariant rather
than follow-up cleanup.
---
## 1. The tracker's framing is wrong in one load-bearing way
P13-S2 is filed as: *"Either the space is not `Cmn`-representable (and needs
`Integer`/`Registered`), or `alteration` needs a finer unit — **a data-model
major**."*
The diagnosis is right. The parenthetical is wrong, and it is the reason this
item has sat parked.
`binary_format.tex` Requirement `req:binfmt:frozen-layout` (line 1168) names
exactly four **open** value-layer vocabularies, and `PitchSpacePosition` is one
of them:
> At the value layer, the open vocabularies are those with a `Registered` escape
> variant — `PitchSpacePosition`, `SpellingNominal`, `StaffGroupKind`,
> `TieClass` — through which extensions attach **without any wire change at
> all**.
and `sec:evolution:additive` (line 2378) repeats it: *"Extension through an
escape variant is **not** a schema change at all: the wire form is already
defined."* Appending a **new** variant to one of these is a schema-**minor**
change, not a major.
So of the four live options (§4), **three cost no major bump and move no
existing byte.** Only one — regrading `alteration` onto a finer fixed unit — is
the data-model major the tracker names, and it is also the only option that
rewrites the canonical bytes of every pitch ever authored. The item is
considerably cheaper than its parking notice claims, and the expensive option
is ruled out by Ruling A.
---
## 2. The defect is larger than `cmn-24`
`cmn-24` is one instance of a rule nobody wrote down: **nothing binds a `Cmn`
position to a space whose chromatic layer is twelve.**
| what I checked | result |
|---|---|
| `ScalePosition` carries the space beside the position | yes — `pitch.rs:344` |
| any code consults that space when interpreting `Cmn` | **none** |
| an invariant rejects `Cmn` in a non-diatonic space | **none**`invariants.rs` never validates the space against the position variant |
| `PitchSpacePosition::Cmn` match sites in the workspace | **52**, across 15 files |
`ScalePosition { space: PitchSpaceId::new("edo-31"), position: Cmn { .. } }`
constructs, validates, encodes, and transposes today. It means nothing, and the
core will happily do twelve-semitone arithmetic on it.
Four core surfaces bake in "`Cmn` ⟹ 12":
* **`Pitch::transposed`** (`pitch.rs:270`) — `nominal.chromatic() + alteration +
12 * octave`, never reading `self.scale_position.space`. This is the normative
equation of `req:pitch:transposition`, so it is a *specified* assumption, not
an implementation shortcut.
* **`Pitch::twelve_tet_semitone`** (`pitch.rs:490`) — same expression; feeds
`twelve_tet_class`, `enharmonic_equivalent`, `PitchRange::contains`
(`pitch.rs:612`), the spelling pre-pass (`prepass.rs:675`), and the transpose
reducer (`reduce.rs:5899`).
* **`CmnNominal::chromatic()`** (`pitch.rs:172`) — returns the cmn-12 map
`{C:0, D:2, E:4, F:5, G:7, A:9, B:11}` unconditionally, for every space.
* **The spelling pre-pass** (`prepass.rs:524-746`) — line-of-fifths, which is
`7·lof mod 12`. Structurally 12-chromatic. `req:pitch:spelling-algorithm`
specifies it that way and gates it on nothing.
Two further instances the tracker does not name:
* **`maqam-base`** is in the same built-in table (`core_spec.tex:3460`) as
*"Skeletal maqam framework with quarter-flat and quarter-sharp accidentals."*
Same quarter-tone requirement, and no ruling on `cmn-24` settles it unless the
ruling is about the *unit* rather than about one catalog row.
* **`PitchSpaceModification::CmnChromatic(i8)`** (`core_spec.tex:3043`) — the
accidental registry's modification type has the identical unit problem, and
`req:tuning:accidental-modification-compatibility` (line 3065) already ties it
to spaces with "`DiatonicChromatic` or compatible algebra". **There are two
sites with one unit decision between them**, and a ruling that fixes only
`PitchSpacePosition::Cmn.alteration` leaves the accidental half broken.
**The spec is already fully general where it counts.** `PositionStructure::
DiatonicOverChromatic { nominals_per_octave, chromatic_positions_per_octave,
nominal_to_chromatic }` (`core_spec.tex:2851`) expresses `cmn-24` exactly:
`{7, 24, [0,4,8,10,14,18,22]}`. `req:tuning:diatonic-chromatic-mapping`
constrains that mapping and **never fixes 24 or 12**. Chapter 4 was written for
an arbitrary chromatic layer; Chapter 2 was written for twelve. That is the
whole defect.
---
## 3. Two ratified MUSTs already contradict each other
This is not merely an implementation gap. Both of these are labelled,
implemented requirements:
* **`req:pitch:transposition`** (`core_spec.tex:1085`) fixes, for *every*
`Cmn` position with no space qualifier, `s = nominal.chromatic() + alteration
+ 12·octave`, where `nominal.chromatic()` is the cmn-12 map made normative by
`sec:pitch:nominal`.
* **`req:tuning:builtin-tuning-catalog`** (`core_spec.tex:3501`) makes every
built-in identifier MUST-resolve with the specified semantics, and the table
specifies `cmn-24` as *"CMN extended with 24-EDO quarter-tone accidentals."*
A conforming implementation is therefore required to admit a `cmn-24` `Cmn`
position and required to transpose it by semitone arithmetic. It is the same
family as P13-I1's two-listings drift: two normative statements, each correct in
its own chapter, jointly unsatisfiable. **Whatever else this pass does, one of
these two requirements changes.** That is what makes it a ratification exercise
rather than a bug fix.
---
## 4. The four options
### Option 1 — `cmn-24` is not `Cmn`-representable
Redefine the catalog row: `cmn-24` becomes `PositionStructure::Chromatic
{ positions_per_octave: 24 }`, positions are `Integer { space_size: 24, index }`.
*Cost:* one table row and a sentence. No code, no bytes, no schema event.
*What it buys:* the contradiction in §3 disappears immediately and honestly.
*What it costs:* the feature. An `Integer` position has no nominal, so a
half-flat E cannot be told to sit on the E line — the notehead has no staff
position and the spelling pre-pass has nothing to spell. `req:pitch:transposition`
refuses non-`Cmn` positions outright, so `cmn-24` scores become untransposable.
"CMN-compatible notation" is precisely what the catalog row promises and
precisely what this deletes.
### Option 2 — `alteration` is chromatic steps *of its space* — **ratified**
The `i8` keeps its type and its width. Its **unit** becomes one step of the
enclosing space's chromatic layer. In `cmn-12` that is a semitone; in `cmn-24` a
quarter-tone, so a flat is `-2` and a half-flat is `-1`.
*Migration: none.* Every score in existence is `cmn-12`, where the new unit and
the old unit are the same thing. Not one canonical byte moves — and I verified
the stronger property that makes this safe: `canonical_pitch_bytes`
(`pitch.rs:645`) writes the **space id first**, before the position, so a
`cmn-24` E-half-flat and a `cmn-12` E-flat derive different `PitchId`s despite
identical position bytes. The position is never interpreted without its space in
the byte stream either.
*What it requires:*
* `req:pitch:transposition` amended to `s = nominal.chromatic(space) +
alteration + C·octave`, with `C = chromatic_positions_per_octave`. The
diatonic half is unchanged — mod 7 stays mod 7, because the nominal count is
still seven.
* `CmnNominal::chromatic()` takes the space. It currently *is* the cmn-12 map;
in `cmn-24` the map is `[0,4,8,10,14,18,22]`. An API break in core, not a wire
break.
* **The pitch-space registry must exist**, because the core must resolve a
`PitchSpaceId` to its `chromatic_positions_per_octave`. That is Push 4b's
*other* blocker. So Option 2 does not unblock 4b from outside — **it is part
of 4b**, and the sequencing question in Ruling B follows from that.
* The `twelve_tet_*` family gets an honest gate: `None` unless `C == 12`.
*What it does not fix:* the spelling pre-pass. Line-of-fifths is 12-chromatic in
its bones; `cmn-24` pitches would land in `spelling_unavailable`, which
`prepass.rs:116` already has a bucket and a counter for. That is a defensible
first delivery — store, transpose, and engrave quarter-tones without inferring
their spelling — but it should be stated, not discovered.
### Option 3 — regrade `alteration` onto a finer fixed unit
Quarter-tones, cents, or a rational. **This is the data-model major the tracker
names, and the only option that is one.** Every existing `alteration` is
rescaled, so every `Pitch`'s canonical bytes move, so every `PitchId`,
`ContentHash` and `OperationId` derived from one moves, and every golden vector
regenerates. It also buys a single grid: pick 24 and `edo-31`, `edo-53`,
`edo-72` stay unrepresentable, so it pays a major and does not close the class.
Ruling A explicitly rules this out, so it cannot continue to make the selected
option look expensive.
### Option 4 — append a `Cmn`-microtonal variant
`CmnMicro { nominal, alteration_num, alteration_den, octave }` at discriminant
4. Schema-**minor** per §1; existing bytes decode unchanged.
*What it costs:* it forks the CMN path. All 52 `Cmn` match sites become
two-armed, and the ones that are not become sites that silently ignore
microtonal pitches — the exact failure mode this project keeps paying for, and
one no test would catch because no fixture would have a microtonal pitch in it.
It also still needs the space, to know what the denominator is denominated
against. Strictly more machinery than Option 2 for strictly less.
---
## 5. Rulings
### Ruling A — Option 2 governs both CMN chromatic units
**Ratified: Option 2.** A `Cmn` alteration and a `CmnChromatic`
modification are denominated in steps of the enclosing pitch space's chromatic
layer; `cmn-12`'s step is the semitone. This is one unit rule governing
`PitchSpacePosition::Cmn.alteration`,
`PitchSpaceModification::CmnChromatic`, `cmn-24`, and `maqam-base`—not a
special case for one catalog row.
The `i8` representation and wire layout stay unchanged. Existing `cmn-12`
scores require no migration because their unit remains the semitone. Option 3
is explicitly ruled out: rewriting every existing pitch and its derived
identifiers to obtain a fixed finer grid pays a data-model major without
closing the general class. Options 1 and 4 are not selected.
### Ruling B — specify now, generalize in Push 4b, fail closed meanwhile
**Ratified: land the normative unit and transposition changes under P13-S2
now.** This resolves the contradiction in §3 without waiting for a registry.
The generalized code path belongs to Push 4b: the registry,
space-relative nominal mapping, and structural `twelve_tet_*` gates must land
together because the core cannot derive them from an identifier alone.
**Also ratified: the interim guard is part of P13-S2.** Until Push 4b supplies
structural resolution, the core must fail closed:
* `Pitch::transposed` may use the current arithmetic only when the core can
establish that the enclosing space has the built-in `cmn-12` structure;
otherwise it returns a dedicated `TransposeRefusal`.
* `twelve_tet_semitone` returns `None` unless the core can establish a
twelve-chromatic layer; callers must propagate that unavailability instead
of deriving a 12-TET result.
This is a capability check, not a normative claim that the identifier
`cmn-12` defines the structure. In the pre-registry implementation, recognizing
that identifier is merely the only available proof of the capability. The
temporary consequence is explicit and accepted: a score-defined
diatonic-over-12 space refuses rather than silently receiving arithmetic the
core cannot validate. Push 4b must replace the identifier check with
`PositionStructure::DiatonicOverChromatic` resolution; it must not preserve the
name check as policy.
The refusal rule is the stable contract: unresolved structure must not produce
a guessed transposition or 12-TET value. Push 4b broadens what the core can
prove without changing that fail-closed behavior.
### Ruling C — distinct diagnostic, existing wire reason
**Ratified:** add a distinct
`TransposeRefusal::PitchSpaceUnavailable` diagnostic for the core's inability
to establish the pitch space structure required by the operation.
`epiphany-ops` must map it to the existing
`PreconditionFailureReason::PitchSpaceMismatch` discriminant 6, alongside
`TransposeRefusal::NonCmnPosition`.
No new `PreconditionFailureReason` variant or discriminant is permitted. That
vocabulary is canonical operation-effect bytes and append-only; permanently
reserving a wire value for the pre-registry identifier guard would preserve the
temporary mechanism in exactly the artifact Ruling B is trying to protect.
Discriminant 6 already covers a pitch-space or tuning-context precondition that
does not admit the operation. The operation-catalog description of that
existing case must be broadened accordingly, but the binary-format table and
wire bytes do not change.
### Ruling D — a reference pitch uses the score's default space
**Ratified:** when `PitchSpacePosition::Cmn` occurs bare as
`ScoreTuningContext.reference.position`, its alteration unit is the chromatic
step of `ScoreTuningContext.default_pitch_space`. This makes explicit the
relationship already required by `req:tuning:reference-pitch`: the reference
position must be valid within that default space.
`ReferencePitch::a440()` happens to be unit-independent because its alteration
is zero; that does not settle the type's admitted nonzero values. The normative
alteration-unit requirement must state the default-space rule rather than
letting implementations infer `cmn-12`. Any future context that embeds a bare
`ReferencePitch` must likewise identify the pitch space that denominates it.
---
## 6. What I verified, and what I did not
**Verified** (file:line given above): the open-vocabulary rule and the four named
vocabularies; the 52 `Cmn` sites in 15 files; that no invariant validates the
space against the position variant; that `canonical_pitch_bytes` writes the
space first; that `Pitch::transposed` and `twelve_tet_semitone` never read the
space; that `CmnNominal::chromatic()` is the unqualified cmn-12 map; that
`DiatonicOverChromatic` parameterizes the chromatic layer and
`req:tuning:diatonic-chromatic-mapping` does not fix it; that `maqam-base` and
`PitchSpaceModification::CmnChromatic` carry the same defect; that
`PitchSpace`, `PositionStructure`, `IntervalAlgebra`, `AccidentalRegistry`,
`AccidentalDefinition`, `PitchSpaceModification`, `TranspositionBehavior` and
`SpellingRuleSet` **exist only in the spec** — no Rust type in the workspace
corresponds to any of them (only the `*Id` catalog newtypes exist).
**Not verified, and inherited rather than checked:** the two Push-4a audit claims
already flagged as unconfirmed in `epiphany-core/DECISIONS.md` — that the JI
dimension convention conflicts with its own prime-2 requirement, and that the
named historical tunings lack exact deterministic ratio data. Neither bears on
this ruling. I also did not survey `epiphany-editor-core` or
`epiphany-layout-ir` exhaustively; I confirmed `editor-core:5319` performs
`alteration + 1` as a raise gesture (which becomes a quarter-tone raise under
Option 2 — arguably right, arguably a surprise) and that `constrained.rs` reads
`nominal`/`octave` for staff position, which is space-agnostic. A full
downstream survey belongs to the implementation wave, not to this scoping.
---
## 7. Work breakdown
Small enough that it does not fan out. One agent, or my own hands.
The implementation and this plan must land atomically. The Chapter 2 definition
of `req:pitch:alteration-unit` and every citation below are part of the same
change; `DISCUSSED_NOT_CITED` is not used because this is a real normative
dependency.
1. **Chapter 2.** Amend `req:pitch:transposition` to the space-relative
equation. Amend the `PitchSpacePosition::Cmn` listing comment
(`core_spec.tex:954`) from semitones to the space-relative unit. Add and
label `req:pitch:alteration-unit` to govern
`PitchSpacePosition::Cmn.alteration` and
`PitchSpaceModification::CmnChromatic`; this plan cites that requirement as
part of the same atomic change.
2. **Chapter 4.** Amend `PitchSpaceModification::CmnChromatic`'s comment to cite
the new requirement. Amend the existing reference-pitch requirement to make
`ScoreTuningContext.default_pitch_space` the unit source for its bare
position; this is not a third new requirement. Confirm the `cmn-24` and
`maqam-base` catalog rows now have a satisfiable reading, and state
`cmn-24`'s `nominal_to_chromatic` explicitly so readers cannot derive
different maps.
3. **Conformance note.** Record that `cmn-24` positions are
`spelling_unavailable` at this revision, and why.
4. **Interim guard and operation effect.** Add
`TransposeRefusal::PitchSpaceUnavailable`, the `Pitch::transposed` guard,
the `twelve_tet_*` gate, and the second new requirement: unresolved pitch
space structure must refuse rather than guess. Map the new core diagnostic
to `PreconditionFailureReason::PitchSpaceMismatch` (6); append no wire
vocabulary. Broaden the existing operation-catalog case for discriminant 6
to include it. State in the core doc comment that Push 4b replaces
identifier recognition with structural resolution.
This normative broadening is an independent Operation Catalog **0.9.0**
event: change the title-page version from 0.8.0 to 0.9.0 and add a 0.9.0
revision-history entry. That entry must state both that the unresolved-space
condition now maps to discriminant 6 and that this pass appends **no**
`PreconditionFailureReason`; assignments 10 through 15 remain exactly as
ratified.
Also narrow the 0.8.0 history sentence that currently generalizes from its
original non-`Cmn` case. Preserve the historical fact that detecting a
non-`Cmn` position needs only its discriminant, but stop claiming that every
use of `PitchSpaceMismatch` is registry-independent: the new capability
refusal exists precisely because the pitch-space structure cannot yet be
resolved. The 0.9.0 entry and amended 0.8.0 rationale must agree when read
together.
5. **Tests that preserve the contract.**
* Add purpose-built core tests proving that a non-`cmn-12` `Cmn` position
refuses `transposed` and makes `twelve_tet_semitone` unavailable.
* Add an ops test proving the new refusal becomes
`PitchSpaceMismatch` (6) in the canonical operation effect.
* Rework `pitch_range_contains_is_advisory_and_frame_aware` and
`enharmonic_requires_the_same_pitch_space` to use the computable
CMN-versus-`Integer { space_size: 12 }` pair. Assert both fixtures have a
`Some` 12-TET value before asserting frame/space rejection, so the tests
cannot pass through the new unavailability gate. Mutation-check the
intended frame/space branches.
* Do not add handling to `resolve_transposed_spellings`: its existing
`transposed.twelve_tet_semitone()?` correctly refuses the whole operation
when the gate returns `None`. Preserve that propagation.
* Expect zero golden churn. Existing generators and fixtures only emit
`cmn-12`, so unchanged goldens are not coverage; any churn is unexpected,
and the purpose-built tests above are mandatory.
6. **Requirement counts.** The two new requirements take
`CORE_REQUIREMENT_COUNT` from 207 to **209** and both suite/label counts from
277 to **279**. Set `requirement_labels.rs` to **209/279/279**, then verify
those values by counting; do not increment constants iteratively until the
test passes.
7. **Tracker.** Mark P13-S2 resolved spec-side; move the registry work to Push
4b's blocker list with the contradiction struck off.
Rebuild both independently versioned companions twice:
`core_spec.pdf` because the two inserted requirements renumber later
requirements and cross-references, and `operation_catalog.pdf` because the
normative case and catalog semver changed. No code check locks the Operation
Catalog's title-page version, so manually verify that its title page says
**0.9.0** and that the revision history contains the matching 0.9.0 entry.
---
## 8. Traps
* **A ruling about `cmn-24` alone does not settle `maqam-base` or
`CmnChromatic`.** Rule on the unit, not the row.
* **`nominal.chromatic()` is not a scale factor.** cmn-24's nominal map is
`[0,4,8,10,14,18,22]`, not `2 ×` the cmn-12 map — `E→F` is one chromatic step
in cmn-12 and two in cmn-24, not one and two respectively scaled. Anyone who
implements this as "multiply by `C/12`" gets F and B wrong.
* **The diatonic axis does not move.** Seven nominals, mod 7, in both spaces.
Only the chromatic axis is parameterized.
* **`twelve_tet_semitone` is not private.** It is public API with six callers
across three crates, and its name becomes a lie for `C != 12`. Renaming it is
the honest move and it is a breaking change in `epiphany-core`.
* **Nothing today rejects `Cmn` in a JI or EDO space.** Whatever is ratified,
the absence of that check is a live defect independent of `cmn-24`, and the
most likely way for this pass to declare victory while leaving the hole open.

Binary file not shown.

View File

@ -951,7 +951,7 @@ pub enum PitchSpacePosition {
/// alteration plus octave. The fast path for tonal Western music.
Cmn {
nominal: CmnNominal, // C, D, E, F, G, A, B
alteration: i8, // -2..=+2 in semitones
alteration: i8, // chromatic steps of the enclosing space
octave: i8, // scientific pitch notation
},
@ -978,6 +978,24 @@ pub enum PitchSpacePosition {
}
\end{lstlisting}
\begin{requirement}
\label{req:pitch:alteration-unit}
For a \texttt{PitchSpacePosition::Cmn} enclosed by a
\texttt{ScalePosition}, \texttt{alteration} \MUST{} be measured in steps of
that pitch space's chromatic layer. Its zero point for each nominal \MUST{}
be the space's \texttt{nominal\_to\_chromatic} mapping. A
\texttt{PitchSpaceModification::CmnChromatic} value \MUST{} use that same
unit in every pitch space whose accidental registry contains it. Thus one
step is a semitone in \texttt{cmn-12} and a quarter-tone in
\texttt{cmn-24}; a flat is respectively $-1$ and $-2$.
When a bare \texttt{PitchSpacePosition::Cmn} is embedded as
\texttt{ScoreTuningContext.reference.position}, the score's
\texttt{default\_pitch\_space} \MUST{} supply the chromatic layer and mapping
for this rule. Any other context embedding a bare CMN position \MUST{}
identify the pitch space that denominates it.
\end{requirement}
\begin{requirement}
\label{req:pitch:ji-vector-basis}
For \texttt{PitchSpacePosition::JiVector}:
@ -1066,8 +1084,9 @@ pub struct TranspositionInterval {
/// 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.
/// Signed steps in the enclosing pitch space's chromatic layer.
/// A perfect fifth in cmn-12 is +7 and an octave is +12.
/// The corresponding values are space-relative.
pub chromatic_steps: i32,
}
\end{lstlisting}
@ -1078,25 +1097,29 @@ 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.
and sound the same. A scalar chromatic-step count cannot express the
difference, and an implementation carrying only one component 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
(Section~\ref{sec:pitch:nominal}). Let the enclosing pitch space have a
\texttt{DiatonicOverChromatic} structure with $C$ chromatic positions per
octave and nominal mapping $m$. A
\texttt{PitchSpacePosition::Cmn} position's \emph{absolute chromatic
coordinate} is
\[
s \;=\; \texttt{nominal.chromatic()} \;+\; \texttt{alteration}
\;+\; 12 \cdot \texttt{octave}.
s \;=\; m(\texttt{nominal}) \;+\; \texttt{alteration}
\;+\; C \cdot \texttt{octave}.
\]
Transposing that position by \texttt{TranspositionInterval \{ d, c \}}
\MUST{} produce
Transposing that position by \texttt{TranspositionInterval \{ d, c \}},
where $c$ is measured in steps of the same chromatic layer, \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),
\texttt{alteration}' &= (s + c) -
\big(m(\texttt{nominal}') + C \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
@ -1107,6 +1130,8 @@ and an implementation carrying only one is not transposing but altering.
\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 enclosing pitch space cannot be resolved to a compatible
\texttt{DiatonicOverChromatic} structure and nominal mapping;
\item its \texttt{acoustic.realization} is
\texttt{AcousticRealization::AbsoluteHz}, which overrides the tuning
system, so moving the scale position would move the notehead without
@ -1114,19 +1139,32 @@ and an implementation carrying only one is not transposing but altering.
\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.
An implementation \MUSTNOT{} saturate, clamp, guess a pitch-space
structure, or partially apply a transposition in any of these cases.
\end{requirement}
\begin{requirement}
\label{req:pitch:space-capability-refusal}
An implementation \MUST{} fail closed when it cannot establish the
pitch-space structure required to interpret a position. In particular,
transposition \MUST{} report a refusal when the enclosing CMN chromatic
layer or nominal mapping is unavailable, and a computation claiming a
12-chromatic pitch class or absolute semitone \MUST{} report
unavailability unless it can establish a twelve-position chromatic layer.
The \texttt{Cmn} position discriminant alone \MUSTNOT{} be treated as proof
of either capability.
\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.
The refusal cases are the four ways a transposition can silently lie. A
non-CMN position has no nominal to move, so a chromatic shift would have to
invent one. An unresolved pitch space supplies neither the octave size nor
the nominal mapping, so arithmetic would have to guess both.
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 four, 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
@ -3040,8 +3078,8 @@ pub struct AccidentalDefinition {
\begin{lstlisting}[language=Rust]
pub enum PitchSpaceModification {
/// CMN-style integer chromatic alteration. -2 = double flat,
/// -1 = flat, +1 = sharp, +2 = double sharp, etc.
/// CMN-style integer offset in chromatic steps of the enclosing
/// pitch space (`req:pitch:alteration-unit`).
CmnChromatic(i8),
/// Integer step offset in an EDO position space.
@ -3318,7 +3356,10 @@ pub struct ReferencePitch {
\label{req:tuning:reference-pitch}
Every score \MUST{} declare a reference pitch. The reference pitch
\MUST{} be expressible as a valid position within the score's default
pitch space. The frequency \MUST{} be positive and finite.
pitch space. When that position is \texttt{Cmn}, the default pitch space
\MUST{} supply its chromatic layer and nominal mapping under
Requirement~\ref{req:pitch:alteration-unit}. The frequency \MUST{} be
positive and finite.
\end{requirement}
\subsection{Multiple Reference Pitches}
@ -3438,9 +3479,10 @@ identifiers with the specified semantics.
\texttt{cmn-12} & Common Music Notation, 7 diatonic nominals (A--G)
over 12 chromatic positions. Standard sharps, flats, double-sharps,
and double-flats. The default for Western tonal and post-tonal music. \\
\texttt{cmn-24} & CMN extended with 24-EDO quarter-tone accidentals.
Used for Arabic-derived microtonal practice in CMN-compatible
notation. \\
\texttt{cmn-24} & CMN over 24 chromatic positions with
\texttt{nominal\_to\_chromatic} $=[0,4,8,10,14,18,22]$ and quarter-tone
alteration steps. A flat is $-2$ and a half-flat is $-1$. Used for
Arabic-derived microtonal practice in CMN-compatible notation. \\
\texttt{edo-19} & 19-tone equal division of the octave.
Notable for its closer-to-just major thirds. \\
\texttt{edo-22} & 22-tone equal division. Distinguishes harmonic
@ -3458,13 +3500,21 @@ identifiers with the specified semantics.
\texttt{ji-11limit} & 11-limit JI lattice with HEJI accidentals.
Four-dimensional. \\
\texttt{maqam-base} & Skeletal maqam framework with quarter-flat
and quarter-sharp accidentals. Deep coverage of specific maqamat
is the province of grammar plugins. \\
and quarter-sharp \texttt{CmnChromatic} accidentals, denominated by the
space's chromatic layer under Requirement~\ref{req:pitch:alteration-unit}.
Deep coverage of specific maqamat is the province of grammar plugins. \\
\texttt{gamelan-slendro} & Five-tone slendro framework. \\
\texttt{gamelan-pelog} & Seven-tone pelog framework. \\
\bottomrule
\end{longtable}
\paragraph{Conformance note.}
At this revision the default spelling pre-pass is structurally
12-chromatic and therefore reports \texttt{spelling\_unavailable} for
\texttt{cmn-24} positions. This does not prevent storing or engraving an
explicitly authored spelling, nor does it change the space-relative
transposition algebra; automatic 24-chromatic spelling inference is deferred.
\subsection{Built-in Tuning Systems}
\begin{longtable}{p{4cm} p{9.5cm}}

Binary file not shown.

View File

@ -231,7 +231,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.8.0 --- Transpose algebra (TransposeInterval; the frozen Transpose)}\\[4pt]
{\normalsize\color{epiphanyink}Version 0.9.0 --- Space-relative CMN transposition}\\[4pt]
{\small\color{epiphanyslate}Normative for the operation kinds it defines}
\vfill
\end{titlepage}
@ -326,9 +326,22 @@ 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.
\texttt{PitchSpaceMismatch} (6) is un-reserved and now produced for a
non-\texttt{Cmn} position. That case reads the position discriminant and does
not require a pitch-space registry.
\medskip
\noindent\textbf{Version 0.9.0 (P13-S2, space-relative CMN).} Broadens the
\texttt{PitchSpaceMismatch} (6) case for \texttt{TransposeInterval}: a
\texttt{Cmn} target whose enclosing chromatic structure or nominal mapping
cannot be established now refuses under core requirement
\texttt{req:pitch:space-capability-refusal}, rather than receiving guessed
12-chromatic arithmetic. This capability check is distinct from the 0.8.0
non-\texttt{Cmn} discriminant check and is replaced by structural registry
resolution in Push~4b. No payload bytes change and no
\texttt{PreconditionFailureReason} is appended; assignments 10 through 15
remain exactly as ratified.
% ===========================================================================
\chapter{The Catalog Framework}
@ -728,11 +741,14 @@ is transposed by \texttt{interval}.
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
remaining targets. The four cases and their
\texttt{PreconditionFailureReason} discriminants:
\begin{itemize}
\item a non-\texttt{Cmn} \texttt{scale\_position.position} $\Rightarrow$
\texttt{PitchSpaceMismatch} (6);
\item a \texttt{Cmn} position whose enclosing chromatic structure or
nominal mapping cannot be established $\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