epiphany/spec/PLAN_P13S2_CMN24.md

407 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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