21 KiB
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
Registeredescape 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 readingself.scale_position.space. This is the normative equation ofreq:pitch:transposition, so it is a specified assumption, not an implementation shortcut.Pitch::twelve_tet_semitone(pitch.rs:490) — same expression; feedstwelve_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 is7·lof mod 12. Structurally 12-chromatic.req:pitch:spelling-algorithmspecifies it that way and gates it on nothing.
Two further instances the tracker does not name:
maqam-baseis 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 oncmn-24settles 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, andreq:tuning:accidental-modification-compatibility(line 3065) already ties it to spaces with "DiatonicChromaticor compatible algebra". There are two sites with one unit decision between them, and a ruling that fixes onlyPitchSpacePosition::Cmn.alterationleaves 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 everyCmnposition with no space qualifier, `s = nominal.chromatic() + alteration- 12·octave
, wherenominal.chromatic()is the cmn-12 map made normative bysec:pitch:nominal`.
- 12·octave
req:tuning:builtin-tuning-catalog(core_spec.tex:3501) makes every built-in identifier MUST-resolve with the specified semantics, and the table specifiescmn-24as "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 PitchIds despite
identical position bytes. The position is never interpreted without its space in
the byte stream either.
What it requires:
req:pitch:transpositionamended tos = nominal.chromatic(space) + alteration + C·octave, withC = 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; incmn-24the 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
PitchSpaceIdto itschromatic_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:NoneunlessC == 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::transposedmay use the current arithmetic only when the core can establish that the enclosing space has the built-incmn-12structure; otherwise it returns a dedicatedTransposeRefusal.twelve_tet_semitonereturnsNoneunless 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.
-
Chapter 2. Amend
req:pitch:transpositionto the space-relative equation. Amend thePitchSpacePosition::Cmnlisting comment (core_spec.tex:954) from semitones to the space-relative unit. Add and labelreq:pitch:alteration-unitto governPitchSpacePosition::Cmn.alterationandPitchSpaceModification::CmnChromatic; this plan cites that requirement as part of the same atomic change. -
Chapter 4. Amend
PitchSpaceModification::CmnChromatic's comment to cite the new requirement. Amend the existing reference-pitch requirement to makeScoreTuningContext.default_pitch_spacethe unit source for its bare position; this is not a third new requirement. Confirm thecmn-24andmaqam-basecatalog rows now have a satisfiable reading, and statecmn-24'snominal_to_chromaticexplicitly so readers cannot derive different maps. -
Conformance note. Record that
cmn-24positions arespelling_unavailableat this revision, and why. -
Interim guard and operation effect. Add
TransposeRefusal::PitchSpaceUnavailable, thePitch::transposedguard, thetwelve_tet_*gate, and the second new requirement: unresolved pitch space structure must refuse rather than guess. Map the new core diagnostic toPreconditionFailureReason::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 noPreconditionFailureReason; assignments 10 through 15 remain exactly as ratified.Also narrow the 0.8.0 history sentence that currently generalizes from its original non-
Cmncase. Preserve the historical fact that detecting a non-Cmnposition needs only its discriminant, but stop claiming that every use ofPitchSpaceMismatchis 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. -
Tests that preserve the contract.
- Add purpose-built core tests proving that a non-
cmn-12Cmnposition refusestransposedand makestwelve_tet_semitoneunavailable. - Add an ops test proving the new refusal becomes
PitchSpaceMismatch(6) in the canonical operation effect. - Rework
pitch_range_contains_is_advisory_and_frame_awareandenharmonic_requires_the_same_pitch_spaceto use the computable CMN-versus-Integer { space_size: 12 }pair. Assert both fixtures have aSome12-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 existingtransposed.twelve_tet_semitone()?correctly refuses the whole operation when the gate returnsNone. 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.
- Add purpose-built core tests proving that a non-
-
Requirement counts. The two new requirements take
CORE_REQUIREMENT_COUNTfrom 207 to 209 and both suite/label counts from 277 to 279. Setrequirement_labels.rsto 209/279/279, then verify those values by counting; do not increment constants iteratively until the test passes. -
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-24alone does not settlemaqam-baseorCmnChromatic. 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], not2 ×the cmn-12 map —E→Fis one chromatic step in cmn-12 and two in cmn-24, not one and two respectively scaled. Anyone who implements this as "multiply byC/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_semitoneis not private. It is public API with six callers across three crates, and its name becomes a lie forC != 12. Renaming it is the honest move and it is a breaking change inepiphany-core.- Nothing today rejects
Cmnin a JI or EDO space. Whatever is ratified, the absence of that check is a live defect independent ofcmn-24, and the most likely way for this pass to declare victory while leaving the hole open.