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:
parent
043c18cabf
commit
e7ebd8ac9c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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).**
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue