19 KiB
Push 4b — the Chapter 4 tuning catalog: scope and plan
Status: all four rulings granted; ready for dispatch.
Prepared against master @ b344925. Every claim was checked against the
source; where I ran a probe I give the file and line.
1. The two facts that shape this pass
4b is the first schema major 3. ScoreTuningContext is a frozen positional
struct carrying three of its six specified fields:
specified (core_spec.tex:3380) |
on the wire (codec.rs:1835) |
|---|---|
default_pitch_space |
✓ |
default_tuning_system |
✓ |
reference |
✓ |
accidental_extensions |
absent |
smufl |
absent |
overrides |
absent |
req:binfmt:frozen-layout is explicit that this is a major: "Adding a field is
a MAJOR change regardless of its type: an Option field is not 'optional' in
the wire sense… There is no 'downgrade to minor' for an Option addition."
Majors 0, 1 and 2 are defined; 3 is unopened, and epiphany-bundle/DECISIONS.md:413
already names it as next ("beyond-accept-set tests moved to major 3"). Push 4a
deliberately avoided opening it (binary_format.tex:3272: "no major~3").
So 4b opens a major. That is not a cost to minimize — it is a budget to spend deliberately, because the next one after it is major 4.
And 4b is a spec pass before it is a build. Zero of Chapter 4's types exist
in Rust — not PitchSpace, PositionStructure, IntervalAlgebra,
NominalRegistry, AccidentalRegistry, AccidentalDefinition,
PitchSpaceModification, TranspositionBehavior, SpellingRuleSet,
TuningSystem, TuningResolution, TuningOverride, nor TuningScope. Only
the *Id newtypes exist. There is nothing to extend; there is a chapter to
implement. And three defects stand in front of it.
2. Three spec defects block the build
Two were verified today from claims this repo had carried as unverified through two passes (P13-S5, P13-S6). The third I found while scoping this plan.
P13-S5 — the JI prime basis is specified at two lengths
req:pitch:ji-vector-basis says the built-in JI spaces order primes ascending
starting with 2, and components.len() MUST equal the basis size. The
built-in table says otherwise, each row exactly one short:
| space | requirement | table (core_spec.tex:3496) |
|---|---|---|
ji-5limit |
{2,3,5} → 3 | "Two-dimensional (prime axes 3, 5)" |
ji-7limit |
{2,3,5,7} → 4 | "Three-dimensional" |
ji-11limit |
{2,3,5,7,11} → 5 | "Four-dimensional" |
Both normative. A 5-limit vector is required to be both length 2 and length 3. The octave-reduction clause does not reconcile them — it normalizes the first component, it does not remove it.
P13-S6 — no built-in tuning resolution is pinned; 14 of 20 are undefined
req:tuning:builtin-tuning-catalog makes all 20 identifiers MUST-resolve with
normative semantics. Only the six tet-* are specified, structurally, by
TuningResolution::EqualTemperament. The other 14 are names:
pythagorean (3:2 given; fifth-chain construction and wolf placement not),
three meantone variants, werckmeister-iii/iv, vallotti,
kirnberger-ii/iii, young-ii, three ji-static-5limit-*, and
ji-adaptive-5limit. TuningResolution::Function delegates them to a
TuningFunctionId that Chapter 10 lists as an extension point; no built-in
is mapped to one, and none is pinned.
Against req:tuning:tuning-resolution-determinism — determinism MUST hold
across platforms — two conforming implementations may each pick a different
published Werckmeister III and both pass. This is the requirement 4b exists to
satisfy, so it cannot be deferred past it.
P13-S7 — the pitch-space registry does not exist (new; file it)
ScalePosition.space is documented as "References an entry in the score's
pitch-space registry" (core_spec.tex:934). req:pitch:default-pitch-space
says "Every score MUST define at least one pitch space… Scores MAY define
additional pitch spaces."
There is no such registry anywhere in the data model. Score has 13 fields
(core_spec.tex:3639) and none is a pitch-space or tuning-system registry.
ScoreTuningContext's six specified fields carry ids and accidental
extensions — no space or system definitions. So a score can name an
arbitrary PitchSpaceId and can never define one: the MAY is flatly
unsatisfiable and the MUST is satisfiable only by reading "define" as "select".
This is load-bearing for 4b in a way the other two are not: a registry-backed resolver has nothing to resolve against except the built-in catalog until it is answered. Ruling C answers it by moving the requirement, not the data model — "define" becomes "select", and the resolver's target is the built-in catalog by ratification rather than by omission.
3. What I verified
| claim | result |
|---|---|
| Chapter 4 types present in Rust | 0 of 13; only *Id newtypes |
ScoreTuningContext wire fields |
3 of 6 (codec.rs:1835) |
| schema majors defined | 0, 1, 2; 3 unopened |
| Chapter 4 labelled requirements | 9, all citable since P13-S1 |
| built-in tuning systems specified | 6 of 20 |
| score-local pitch-space storage | none exists |
| deferred items awaiting a major elsewhere | 1 (epiphany-core/DECISIONS.md:431, a configurable-order Score field with no consumer — not worth riding along) |
The P13-S2 interim guard is narrower than it looked. It refuses any Cmn
position outside built-in cmn-12, and PLAN_P13S2_CMN24.md's Ruling B
accepted "false refusal for score-defined 12-chromatic spaces" as its cost.
Given P13-S7, that cost is zero: no score can define a space at all, so no
false-refusal case exists. Ruling C keeps it at zero by ratification rather than
by omission — the accepted cost would only become real if score-local definition
later lands, and it is now a deliberate deferral with a stated reason.
4. Rulings
Ruling A — P13-S5 — RATIFIED: full register
The built-in table gains prime 2. ji-5limit becomes three-dimensional
(basis {2,3,5}), ji-7limit four, ji-11limit five.
req:pitch:ji-vector-basis is correct as written and does not change; the three
catalog rows do.
The data model forces it: a JiVector is an absolute position, and without
the prime-2 exponent ji-5limit cannot distinguish C4 from C5. The requirement
already says "Without such a declaration, full register is preserved." The
table's "two-dimensional (prime axes 3, 5)" is a theory presentation, in which
octave equivalence is assumed; the data model cannot assume it. Octave-reduction
would additionally have required a separate register carrier on JiVector — a
new field, and a second major-3 change for no gain.
Ruling B — P13-S6 — RATIFIED: keep all 20, pinned by construction
The first scoping priced this as "a musicological ratification, 14 times over." That was wrong, and the error was in what form a definition takes.
These temperaments have standard constructions — Vallotti narrows six consecutive fifths by 1/6 Pythagorean comma and leaves the rest pure; quarter-comma meantone narrows every fifth by 1/4 syntonic comma; Werckmeister III narrows four named fifths by 1/4 Pythagorean comma. What varies across published sources is overwhelmingly the rounding of cents tables, not the construction. Specifying by construction therefore removes the variance that specifying by cents table would introduce.
The arithmetic-determinism worry is already answered by existing machinery.
ToleranceClass::AcousticCents exists in epiphany-determinism, and its doc
comment names "Chapter 4 reference-pitch frequency resolution" outright.
req:determinism:canonical-floating-point lists tuning among the
floating-point contexts while barring floating point "where exact identity is
needed (object identifiers, operation ordering, graph membership, hash
identity)" — so a resolved frequency never enters canonical bytes. Bit-identity
was never achievable in any case: pow/exp are not correctly-rounded in
IEEE 754, so two libms may differ in the last ulp including for tet-12's
2^{n/12}. req:tuning:tuning-resolution-determinism forbids dependence on
floating-point rounding modes — the FPU mode — not libm variance, and the
residual is on the order of 10^{-13} cents.
So the split is not 6 specified / 14 unspecified. It is 16 transcription and 4 decisions:
- 16 pinned by construction in Chapter 4 — one line each, with a source citation. Transcription work; a decision only where sources genuinely differ.
- 3 ×
ji-static-5limit-*— which 12-note 5-limit scale (the comma choices for the chromatic degrees) is genuinely unsettled and needs one ratification. ji-adaptive-5limit— needs a real algorithm underreq:tuning:adaptive-tuning-purity. Apply the in-house pattern:req:pitch:spelling-algorithmpinsSpellingAlgorithmId "default"at version 1 to a named algorithm and errors on any other identifier. Do the same with a versionedAdaptiveTuningFunctionId "default".
Ruling C — P13-S7 — RATIFIED: the catalog is closed, for now
A score selects a pitch space and a tuning system from the built-in catalog; it does not define one. Concretely:
- Amend
req:pitch:default-pitch-space: "Every score MUST define at least one pitch space… Scores MAY define additional pitch spaces" becomes select, which is the only reading the data model has ever supported. - Correct
ScalePosition's comment (core_spec.tex:934) — "References an entry in the score's pitch-space registry" — to name the built-in catalog. The registry it points at does not exist and, under this ruling, will not. - No
pitch_spacesortuning_systemsfield is added. Score-local definition is recorded as a deferred major with its reason stated, not left as an unwritten intention.
This resolves P13-S7 rather than deferring it: the contradiction was between a requirement and a data model, and the requirement moves.
Implement all of it; freeze almost none of it. The ruling does not shrink
the implementation surface — the 22 Chapter 4 types still need Rust
definitions, because the built-in catalog's 13 pitch spaces and 20 tuning
systems have to be expressed somewhere, and the P13-S2 guard's structural
replacement is a lookup from PitchSpaceId to PositionStructure over exactly
that data. What the ruling removes is the wire surface: only
accidental_extensions, smufl and overrides are encoded (Ruling D). Every
other Chapter 4 type stays in-memory and therefore stays free to change,
instead of being frozen by req:binfmt:frozen-layout before it has ever had a
consumer.
That asymmetry is the whole argument. Twenty never-constructed types — including
SpellingRuleSet and TranspositionBehavior, themselves unimplemented — would
otherwise be frozen permanently, sight unseen. NOTEHEAD_ANCHORS was
hand-written, unconsumed, and wrong in two independent ways; it survived
precisely because nothing read it. If deferral proves wrong, a later major adds
the registries; if committing proves wrong, we live with the frozen structs
and still need the migration.
Consequence for P13-S2. The interim guard's accepted cost — "false refusal
for score-defined 12-chromatic spaces" — stays zero, because no score can
define a space. Push 4b still replaces the identifier check with structural
resolution over the built-in table, as req:pitch:space-capability-refusal
requires; the fail-closed contract is unchanged and what the core can prove
widens from one space to thirteen.
Ruling D — RATIFIED: one major, and accidental_extensions is in it
ScoreTuningContext completes to all three missing fields in major 3:
accidental_extensions, smufl, overrides. One migration, not two.
accidental_extensions is a subtree, not a field, and that is the price of
this ruling stated honestly. ScoreAccidentalExtensions { base, additions, overrides } carries Vec<AccidentalDefinition>, which pulls in
AccidentalEngraving, AccidentalCombination, PitchSpaceModification,
GlyphReference, a bounding box, and AnchorPoint — none of which exists in
epiphany-core, which depends only on epiphany-determinism.
The correction: they are homonyms, not shared types
The first scoping said GlyphReference and BoundingBox "exist but live in
epiphany-layout-ir" and recommended moving them down. That was wrong, and
a move would have relocated the wrong types:
GlyphReference. Chapter 4's isenum { Smufl(u32), Custom(CustomGlyphId), Composite(Vec<GlyphReference>) }(core_spec.tex:3181) — a recursive codepoint reference. layout-ir's isstruct GlyphReference(pub Cow<'static, str>)(glyph.rs:50) — a glyph name, a rendering concern. Same name, unrelated types.BoundingBox. The one layout-ir holds is Chapter 7's (core_spec.tex:8650), built onStaffSpace(pub f32). It belongs where it is.
And a defect: AccidentalEngraving cannot be canonical as written
AccidentalEngraving mixes bounding_box: BoundingBox — Chapter 7's
StaffSpace, single precision by deliberate choice (spatial.rs:3) — with
advance_width: SpaceUnit, which is CanonicalF64. Resolved layout is
non-canonical (binary_format.tex:2521, :3211), so f32 is correct there.
But this ruling puts accidental_extensions inside ScoreTuningContext, which
is canonical state, and req:determinism:canonical-floating-point requires
canonical floats to be "finite IEEE 754 binary64". As specified, the field
would put single-precision floats into canonical bytes.
RATIFIED: Chapter 4 gets its own SpaceUnit-based engraving box. All four
edges become SpaceUnit, the type advance_width already uses. This makes
AccidentalEngraving wholly core-typed, removes the backwards Chapter 4 →
Chapter 7 dependency, and satisfies Appendix D. Chapter 7's BoundingBox is
untouched and stays in layout-ir.
So there is no crate move. Step 0 is define Chapter 4's glyph and engraving
vocabulary as new core types, and layout-ir's homonyms are left alone. The two
GlyphReferences must not be unified — they are different concepts that happen
to share a word.
Nothing else rides along. The survey found one other deferred field addition
(epiphany-core/DECISIONS.md:431, a configurable-order Score field with no
consumer), deliberately excluded — a field with no consumer is how the
Staff::default_clef and NOTEHEAD_ANCHORS findings started.
Ruling C's registries, if granted, must land in this major or wait for a
later one; they must not split ScoreTuningContext's completion across two
migrations.
5. Work breakdown
Genuinely large — the first pass in a while that wants a real fan-out, but not until §4 is answered. Sketch, in dependency order:
1a. Spec corrections (mechanical). S5's three catalog rows to full register;
S7's "define" → "select" and the ScalePosition comment; Chapter 4's
AccidentalEngraving onto a SpaceUnit-based box (Ruling D). No new
requirements, so the requirement_labels.rs counts must not move — if
they do, something unintended happened.
1b. S6 temperament constructions, drafted for review. A non-normative
artifact with a cited source per construction, not an edit to
core_spec.tex. An agent writing Werckmeister III from memory into a
normative document is the NOTEHEAD_ANCHORS failure exactly: hand-written,
unverifiable in-tree, and load-bearing. Review gates promotion.
2. Chapter 4 vocabulary in core. The glyph and engraving types as new core
types (Ruling D). Do not unify with layout-ir's homonyms.
3. Major-3 delta. Written against the real types once they exist, with the
byte-for-byte migration sec:evolution:migration requires
(sec:evolution:major1/major2 are the models — a major MUST ship a
documented byte-for-byte migration per sec:evolution:migration).
2. Types (in-memory). The Chapter 4 type surface, unfrozen: PitchSpace,
PositionStructure, IntervalAlgebra, the registries, TuningSystem,
TuningResolution and their kin. No Codec impls for these — Ruling C
keeps them off the wire, which is what keeps them free to change.
3. The built-in catalog as data. The 13 pitch spaces and 20 tuning systems
as constants, with the 16 constructible temperaments expressed by their
construction (Ruling B) rather than as cent tables, so the spec and the code
state the same rule in the same form.
4. Codec — the narrow part. ScoreTuningContext's three added fields only,
with dec_*_v2 sub-decoders preserving the frozen prior layout exactly as
major 2 did for the cross-cutting bodies. This is the entire wire delta.
5. The resolver. req:tuning:tuning-resolution-order's five-scope walk,
per-component and independent; req:tuning:tuning-system-compatibility's
rejection rule.
6. Retire the P13-S2 interim guard. Replace the cmn-12 identifier check in
Pitch::transposed and twelve_tet_semitone with
PositionStructure::DiatonicOverChromatic resolution over the built-in
table — replace, not preserve (req:pitch:space-capability-refusal, and
Ruling B of PLAN_P13S2_CMN24.md says so explicitly). The fail-closed
contract stays; what the core can prove widens from one space to thirteen.
cmn-24 transposition working end-to-end is this pass's proof of life.
7. Accidental registries and SMuFL. req:tuning:smufl-version-fallback,
req:tuning:accidental-modification-compatibility, and the engrave-side
consumers.
8. Conformance. A new step gating the built-in catalog: every promised
identifier resolves, resolution is reproducible across runs, and each pinned
temperament is checked against its construction rather than against a table
copied from the implementation.
6. Traps
- A major MUST ship a documented migration.
sec:evolution:migrationis explicit, and the v0→v1 and v1→v2 entries are the shape to match. A major that lands without one is the defect this project would notice last. ScoreTuningContextis positional and frozen. Field order is as load-bearing as the field set, and the compiler cannot see it — the same invisibility that made the Text Projection layer verify field order by mechanicalencode_canonical-vs-projectdiff.- The interim guard must be deleted, not widened. Ratified in P13-S2. A 4b that adds registry resolution beside the name check has preserved the temporary mechanism as policy.
twelve_tet_semitonebecomes a lie once non-12 spaces resolve. It is public, has six callers across three crates, and renaming it is a breaking change inepiphany-core— cheaper now than later.- The spelling pre-pass stays 12-chromatic. Line-of-fifths is
7·lof mod 12in its bones.cmn-24positions land inspelling_unavailable, which the P13-S2 conformance note already records. 4b should not quietly acquire a microtonal spelling algorithm; that is its own ratification. - Built-in catalogs are implementation-provided, not serialized. The 13 pitch spaces and 20 tuning systems are referenced by id and do not go on the wire. Only score-local definitions and overrides would — which is precisely what Ruling C decides.