407 lines
21 KiB
Markdown
407 lines
21 KiB
Markdown
# 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.
|