diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index 7013e12..f043477 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -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 diff --git a/crates/epiphany-core/src/pitch.rs b/crates/epiphany-core/src/pitch.rs index 3cd3e01..533df80 100644 --- a/crates/epiphany-core/src/pitch.rs +++ b/crates/epiphany-core/src/pitch.rs @@ -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 { 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 { 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] diff --git a/crates/epiphany-ops/DECISIONS.md b/crates/epiphany-ops/DECISIONS.md index 32a2f3b..c3fda28 100644 --- a/crates/epiphany-ops/DECISIONS.md +++ b/crates/epiphany-ops/DECISIONS.md @@ -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).** diff --git a/crates/epiphany-ops/src/effect.rs b/crates/epiphany-ops/src/effect.rs index db5130d..6244750 100644 --- a/crates/epiphany-ops/src/effect.rs +++ b/crates/epiphany-ops/src/effect.rs @@ -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, diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index 73b348e..999cd5d 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -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 diff --git a/crates/epiphany-testkit/tests/requirement_labels.rs b/crates/epiphany-testkit/tests/requirement_labels.rs index a5253b1..06c028c 100644 --- a/crates/epiphany-testkit/tests/requirement_labels.rs +++ b/crates/epiphany-testkit/tests/requirement_labels.rs @@ -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 diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index 553b183..20a0958 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -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) | diff --git a/spec/PLAN_P13S2_CMN24.md b/spec/PLAN_P13S2_CMN24.md new file mode 100644 index 0000000..f431abb --- /dev/null +++ b/spec/PLAN_P13S2_CMN24.md @@ -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. diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index e6be19d..beee6a7 100644 Binary files a/spec/core_spec.pdf and b/spec/core_spec.pdf differ diff --git a/spec/core_spec.tex b/spec/core_spec.tex index 9b17ce9..45ebcfd 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -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}} diff --git a/spec/operation_catalog.pdf b/spec/operation_catalog.pdf index 04e5df9..593cbe9 100644 Binary files a/spec/operation_catalog.pdf and b/spec/operation_catalog.pdf differ diff --git a/spec/operation_catalog.tex b/spec/operation_catalog.tex index 550daf5..c0f7554 100644 --- a/spec/operation_catalog.tex +++ b/spec/operation_catalog.tex @@ -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