epiphany/spec/PLAN_P13S2_CMN24.md

21 KiB
Raw Blame History

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 noneinvariants.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 bysec: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 spaceratified

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: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.