The accidental / glyph / engraving type surface Chapter 4 puts on ScoreTuningContext
now exists in epiphany-core (new accidental.rs): ScoreAccidentalExtensions,
AccidentalDefinition, GlyphReference (Chapter 4's, recursive -- not layout-ir's
homonym), PitchSpaceModification, AccidentalEngraving with EngravingBoundingBox
and AnchorPoint, AccidentalCombination, SmuflVersion(Requirement), plus three
catalog_id! newtypes. All in memory, no Codec, canonical bytes unmoved -- the
reversible half; 3b freezes them on the wire.
The three ratified corrections land as filed:
- S10 Cents(CanonicalF64), not raw f64 -- reverting it to f64 is 9 compile
errors, the type system is the test.
- S11 AnchorPoint { x, y: SpaceUnit }, core-native, frame pinned in its doc.
- S12 SmuflVersion { major, minor_centi }, built only through from_decimal which
normalizes 1-digit x10 / 2-digit as-is, so derived Ord orders SMuFL's real
history right. layout-ir's SmuflVersion untouched; 3b unifies and moves
GlyphCatalogIdentity.
accidental_extensions and smufl join overrides as in-memory-only fields; the hand
codec's enc is byte-for-byte unchanged (three wire fields), only dec defaults the
new ones. The consumer that keeps this off the NOTEHEAD_ANCHORS path is real:
resolve_accidental (override > addition > base) and the
accidental-modification-compatibility invariant wired into check_invariants.
Glyph/engraving metadata is carried but its deep consumer is the engraver, out of
core -- said honestly, not faked.
Verified independently of the agent. Through Score::canonical_bytes: a non-empty
accidental_extensions + non-default smufl encode byte-identically to all-default
(268 both) and decode back to empty -- all three fields off the wire. SmuflVersion
orders 1.12 < 1.18 < 1.20 < 1.3 < 1.4 (the trap: 1.12 before 1.3). CanonicalF64
rejects NaN/inf. And the compatibility invariant is non-vacuous: weakening the
predicate myself made the edo-31 reject test fail. No Codec, no golden moved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A pitch now resolves to a frequency. The tuning-resolution vocabulary
(TuningSystem, TuningResolution, TuningOverride, TuningScope), a partial built-in
catalog, and the five-scope resolver land in epiphany-core, entirely off the
wire -- the reversible half of the remaining tuning work, exercised and proven
before the schema-major-3 bump freezes anything.
Nine of twenty systems resolve: the six tet-* (EqualTemperament, each paired to
the pitch space of matching chromatic cardinality -- tet-12/cmn-12,
tet-N/edo-N), and the three ji-static-5limit-{C,G,D}, whose twelve ratios are
COMPUTED from the lattice block {3^a 5^b | a in [-1,2], b in [-1,1]} in exact
integer arithmetic, never a pasted table. The other eleven fail closed with a
distinct NotYetSupported (vs UnknownTuningSystem): the ten historical
temperaments await tranche 2b, ji-adaptive-5limit awaits HarmonicContext.
overrides is added to ScoreTuningContext as the one field the scope walk needs,
in memory only. Its struct_codec! -- whose dec constructs a literal of exactly
the named fields -- is replaced by a hand-written Codec that encodes the three
wire fields and defaults overrides on decode, with a matching hand-written
TextValue.
Verified independently of the agent that wrote it. Through the real
Score::canonical_bytes() path: a populated overrides encodes byte-identically to
an empty one (268 bytes both) and decodes back to empty -- the field never
reaches the wire. tet-12 A4=440 resolves C5 to 523.2511306011972 Hz by hand.
ji-static-5limit-C's major third is 386.3137c against tet-12's 400.0000c, the
5/4 just third distinct by the expected 13.69c. The wire-invariant test
mutation-killed by leaking the override count from the codec (the first attempt,
encoding the field itself, was a compile error since TuningOverride has no Codec
-- meaningless as a mutation, redone). No golden or fuzz digest moved.
Also corrected a doc comment on pitch.rs's sounding_equivalent that this tranche
falsified: it said frequency resolution is "not modeled in this crate", which is
now untrue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`project` and `parse` for every value an operation payload can embed.
The codec macros now emit a `TextValue` impl beside the `Codec` impl, from the
same invocation: 116 types whose field order cannot disagree between the binary
form and the text, at zero call-site churn. That is the companion's own rationale
applied to code -- a rule cannot drift from the listing it reads, and two listings
of one struct is the drift P13-I1 already cost us. `struct_codec!` rebuilds through
a struct literal and `cstyle_enum_codec!` matches exhaustively, so a field or
variant added later fails to compile rather than silently vanishing from the text.
The other 44 types have hand-written codecs and so need hand-written projections.
Their field order is verified by a mechanical diff of the identifier sequence in
each `fn enc` against the one in each `project`; all 44 agree. This matters because
a `project`/`parse` pair that agrees with itself on a *wrong* order round-trips
perfectly -- neither the compiler nor any round-trip test can see it. The
neighbouring blind spot, a mistyped constructor symbol, is closed by
`textvalue_names.rs`, which recovers each type's Rust name from its derived `Debug`
and compares it against the symbol actually emitted.
Strictness turned out to need only one of its two layers, and mutation testing is
what established that. Every per-site check is live: the set/map strictly-increasing
walk, `RationalTime`'s lowest-terms compare before construction, the catalog-id NFC
intern-and-compare, and `EventArena`'s ascending-`EventId` walk. Every whole-value
`ensure_canonical` guard was dead -- `Tempo::new`, `ReferencePitch::new`,
`SpellingPrecedence::new` and `EventOrderingDAG::try_new` reject rather than adjust,
so an accepted value re-projects to exactly its input and the guard could never
fire. A probe confirmed `try_new` returns its input map unchanged. Helper and all
four call sites removed: a check that cannot fail invites weakening the real one.
Also moves `catalog_name` out of the grammar *test* and into
`operation_kind_tag_vocabulary!`, where the discriminant and decoder already live.
It was a hand-maintained list parallel to an enum -- the exact shape that has cost
this project four bugs.
Method note recorded in DECISIONS: the work list came from compiler errors, but the
compiler reports only the frontier. `AnchorOffset`, `VoiceSelector`, `PowerOfTwo`,
`OctaveOffset` and `NonZeroU16` were each hidden behind a type that had not compiled
yet, so the list must be iterated to a fixpoint.
Gate green -- clippy 0, 1109 tests, doc 0, conformance 8/8, no golden churn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>