epiphany/spec/CONTRACT_PUSH4B_PITCHSPACES.md

221 lines
11 KiB
Markdown
Raw Permalink 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.

# Contract: Push 4b tranche 1 — pitch spaces become structure, and the interim guard dies
Repo root `/home/jeans/Repos/active/epiphany`. The plan is
`spec/PLAN_PUSH4B_TUNING.md`; its four rulings are **granted** and are law here.
Read this file in full before editing anything.
This is the first tranche of Push 4b's implementation. It is deliberately a
**vertical slice**, not a layer: types, the data that fills them, and the
consumer that reads them, landing together.
## Why it is a slice and not just "the type surface"
The plan lists the type surface, the built-in catalog, and retiring the P13-S2
guard as three steps. They ship as one here, because this repository has twice
been burned by the alternative. `Staff::default_clef` sat unread while a
bass-clef staff engraved as treble. `NOTEHEAD_ANCHORS` was hand-written,
unconsumed, wrong in two independent ways, and survived passes *because* nothing
read it. A Chapter 4 type surface with no consumer is that shape exactly.
So the acceptance test for this tranche is behavioural, not structural: **a
`cmn-24` pitch transposes end-to-end.** If that works, the types are real.
## Blast radius
* `crates/epiphany-core/src/` — a new module for the Chapter 4 pitch-space
vocabulary, plus changes to `pitch.rs`.
* `crates/epiphany-core/DECISIONS.md` — record the tranche.
* Test files under `crates/epiphany-core/`.
You edit **no `.tex` file**. The specification is correct as written and is your
input, not your output. You add no requirement, so all three counts in
`crates/epiphany-testkit/tests/requirement_labels.rs` **must not move** — if one
does, stop and report rather than updating it.
## The central prohibition: no `Codec` impls
**Do not write a `Codec` impl for any type you add. Do not add any of them to a
`struct_codec!`. Do not add a field to `Score` or `ScoreTuningContext`.**
This is not a style preference, it is Ruling C, and it is the entire reason the
ruling was affordable. `req:binfmt:frozen-layout` says adding a field is a MAJOR
change *regardless of its type* — there is no "downgrade to minor" for an
`Option`. Anything that reaches the wire is frozen there permanently, sight
unseen, before it has ever had a consumer. These types stay in memory precisely
so they remain free to change when the second tranche discovers they are wrong.
An agent that adds a `Codec` impl to `PitchSpace` has quietly undone the ruling
that made this tranche cheap. Canonical bytes must be **byte-identical** before
and after your change; the conformance suite and the fuzz digest will say so.
## What to build
### 1. The pitch-space vocabulary, in memory
From `core_spec.tex` §"Pitch Spaces" (`:2850` onward). At minimum
`PositionStructure` (`:2889`, all four variants) and the `PitchSpace` type that
carries it. Take the shapes from the specification's own listings — field names,
types, and order — and do not improve them.
`req:tuning:diatonic-chromatic-mapping` (`:2932`) constrains
`DiatonicOverChromatic` with **three** clauses, all of which the constructor must
enforce: `nominal_to_chromatic` MUST have length `nominals_per_octave`, each
entry MUST be strictly less than `chromatic_positions_per_octave`, and the
mapping MUST be **strictly increasing**. Enforce them in a checked constructor,
the way `KeySignature::new` rejects out-of-range fifths — not in a comment.
(Both built-in mappings satisfy all three, which means a constructor that
enforces only two still passes every test built from the catalog. Test the
rejections directly.)
Only the types this tranche's consumer needs. `TuningSystem`,
`TuningResolution`, the accidental registries, and Chapter 4's glyph and
engraving vocabulary are **out of scope** — they belong to the tranche that adds
the codec, and landing them here would create exactly the unconsumed surface
this contract's opening section exists to avoid.
### 2. The thirteen built-in pitch spaces, as data
`core_spec.tex:3598-3646` is the normative table. Seven are fully determined by
it and are transcription:
* `cmn-12``DiatonicOverChromatic`, 7 nominals over 12, mapping
`[0,2,4,5,7,9,11]` (given in `PositionStructure`'s own listing at `:2889`).
* `cmn-24``DiatonicOverChromatic`, 7 over 24, mapping
`[0,4,8,10,14,18,22]` (given in the table).
* `edo-19`, `edo-22`, `edo-31`, `edo-53`, `edo-72``Chromatic`, with
`positions_per_octave` from the identifier.
**Six are not fully determined, and this is the part that matters.** The three
`ji-*` spaces give a limit and a prime basis but `JiLattice.generators` is
`Vec<JiRatio>`, whose values the table does not state. `maqam-base`,
`gamelan-slendro`, and `gamelan-pelog` are described in prose that does not
determine a `PositionStructure` at all.
**Do not invent values for these.** Represent each honestly as unresolved —
whatever mechanism you choose, an unresolved space must **fail closed** at every
consumer, never fall back to a guess. Then **report each one** in your final
report, saying what the specification does and does not determine. A structure
you inferred and wrote down as though the spec stated it is the exact defect
this project has paid for twice; six half-plausible lattices would be a worse
outcome than six honest gaps.
### 3. Retire the P13-S2 interim guard — replace it, do not preserve it
`Pitch::transposed` (`pitch.rs`) currently refuses on a **string comparison**:
```rust
if self.scale_position.space.as_str() != "cmn-12" {
return Err(TransposeRefusal::PitchSpaceUnavailable);
}
```
`twelve_tet_semitone` is gated the same way. Both were ratified as **explicitly
temporary** in `PLAN_P13S2_CMN24.md` Ruling B, which says the identifier check
"must be replaced—not preserved as policy". `req:pitch:space-capability-refusal`
is the requirement.
Replace with structural resolution: look the space's `PositionStructure` up in
the built-in catalog, and
* if it is `DiatonicOverChromatic`, transpose using **that structure's**
`nominal_to_chromatic` and `chromatic_positions_per_octave` — which is what
makes `cmn-24` work, and what `req:pitch:alteration-unit` and
`req:pitch:transposition` already specify space-relatively;
* otherwise — `Chromatic`, `JiLattice`, `Registered`, unknown identifier, or one
of the six unresolved entries — **refuse**, with the existing
`TransposeRefusal::PitchSpaceUnavailable`.
**A version that adds structural lookup *beside* the name check has preserved
the temporary mechanism as policy.** The string `"cmn-12"` must not survive as a
control-flow condition anywhere in `pitch.rs`.
The fail-closed contract is unchanged. What the core can *prove* widens from one
space to the ones the specification actually determines.
### 4. Do not rename `twelve_tet_semitone`
The plan flags that this name "becomes a lie once non-12 spaces resolve", and
suggests renaming now. **Do not.** The name only becomes a lie if the function
starts answering for non-12-chromatic spaces. Keep it returning `None` unless
the resolved structure has exactly 12 chromatic positions — then the name
becomes *true* rather than false, structurally rather than by identifier, and no
breaking change lands in a public API with no consumer waiting for it.
Its six callers across three crates keep working unchanged. If you find a case
where the name genuinely misleads after your change, **report it** rather than
renaming.
## Proof of life
A test that transposes a `cmn-24` pitch and asserts the resulting scale position
**not** merely that it returns `Ok`. This is the tranche's acceptance
criterion. `cmn-24`'s alteration steps are quarter-tones, so a flat is 2 and a
half-flat is 1 (`core_spec.tex:3606`); a transposition that silently applies
12-chromatic arithmetic to a 24-chromatic space is the defect this whole tranche
exists to prevent, and a test that only checks `is_ok()` cannot see it.
Also assert the refusals: a `JiLattice` space, an unknown identifier, and one of
the six unresolved spaces each refuse rather than guess.
## Verification
**Mutation-verify every new test.** For each, re-introduce the bug it exists to
catch, confirm the test fails, then restore. Report the mutation and the test
that died. A test asserting `is_ok()` on `cmn-24` will survive the mutation that
matters, which is how you will know it is the wrong test.
Before substituting, **assert the anchor is present** — a string replacement
that silently matches nothing produces a no-op mutation, and a no-op mutation is
indistinguishable from a passing test. This project has been caught by exactly
that, when rustfmt had rewrapped the line the replacement targeted.
Then the full gate, reporting actual commands and actual output:
1. `cargo fmt --all --check`
2. `cargo clippy --workspace --all-targets`**0 warnings**
3. `cargo test --workspace` → 0 failed
4. `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` → 0
5. `cargo run -q -p epiphany-testkit --example conformance_suite`**8/8**
6. `cargo test -p epiphany-testkit --test requirement_labels` → 6 passed, and
the three counts unchanged at 212 / 282 / 282.
**Zero golden churn and zero canonical-byte movement are expected.** You are
adding in-memory types; if any golden file or fuzz digest moves, something
reached the wire that should not have. Stop and report — do not re-bless a
golden.
CI pins clippy at 1.95.0 and the MSRV at 1.85, and lints run only on the pinned
stable. Local `cargo clippy` is the same toolchain CI uses, so a clean local run
is meaningful; `clippy::incompatible_msrv` will catch any API above the 1.85
floor.
## Citations
If you cite a requirement in a doc comment, **read the requirement first and
confirm it says what your sentence claims.** The checker proves only that a
`req:*` string resolves to a real label; a citation that resolves cleanly while
supporting nothing passes a fully green gate. This is filed as P13-S9 after
three instances in one day, one of them in a contract like this one. The
relevant labels here are `req:pitch:space-capability-refusal`,
`req:pitch:alteration-unit`, `req:pitch:transposition`, and
`req:tuning:diatonic-chromatic-mapping`.
## Do not
* Write a `Codec` impl, extend `struct_codec!`, or add a field to `Score` or
`ScoreTuningContext`.
* Edit any `.tex` file, or any DECISIONS.md other than `epiphany-core`'s.
* Invent a `PositionStructure` for the six the specification underdetermines.
* Keep the `"cmn-12"` string comparison alongside the new lookup.
* Rename `twelve_tet_semitone`.
* Re-bless a golden file or update a requirement count to make a test pass.
* Run `cargo fmt --all` (use `--check`; format only what you wrote).
## Report
State: the types added and where; how you represented the six underdetermined
spaces and what the spec does and does not fix for each; the mutation for every
new test and which test died; the actual gate output; whether any golden or
digest moved; and anything you chose not to do and why.