epiphany/crates/epiphany-core
Levi Neuwirth 357e8b9eeb Push 4a design gate: pin the transpose algebra, freeze the old operation
An audit reopened P12-K2, whose Pass-12 pin promised the repair would be "a
payload schema-major landing with the Chapter 4 tuning catalog". Both halves
were wrong, and the operation was more broken than the pin admitted.

Measured through EditorSession, not inferred. On a C4: +12 yields alteration
12 (six double-sharps, not C5); +128 clamps to 127 and still reports Applied;
targets [p, p] transposes twice; a non-Cmn position is silently untouched.
transpose(1000) then transpose(-1000) lands on -128, so the operation is not
invertible. Nothing downstream is at fault -- prepass::accidental_ids renders
alteration 12 faithfully. The defect is entirely in what Transpose means.

The false coupling is why this looked big. Pitch has orthogonal scale_position
and acoustic fields. Transposition adds an interval to a scale position;
tuning decides what frequency a scale position sounds at. Adding a fifth to C4
needs no tuning catalog. The same weld had spread: PitchSpaceMismatch was
"Reserved: requires the Chapter 4 tuning catalog" (it reads a discriminant),
and TranspositionInterval was "ADVISORY until the Chapter 4 tuning catalog
pins interval algebra". Push 4 splits: 4a is the algebra and needs no catalog;
4b is the catalog, which has its own blockers (cmn-24 is in the pitch-space
table but cannot exist while Cmn.alteration is i8 semitones).

Ratified by the user:

- New kind, freeze the old. An operation is history; a corrected reduction
  rule would rewrite every score that used one. Transpose (disc 9) keeps its
  exact semantics, now written as normative replay semantics rather than as
  apologies. TransposeInterval takes disc 30. This is cheap: appending a kind
  at >= 30 is a schema MINOR, and the payload's constituents are all major-0
  layouts, so it stamps major 0. No major 3, no migration.

- Diatonic + chromatic interval, reusing TranspositionInterval -- which
  already existed in graph.rs at major 2 for Instrument.transposition, already
  codec'd, byte-for-byte the required pair. Minting an Interval beside it
  would have been a second normative listing of one type, the drift P13-I1
  just closed. Declared once now, in Chapter 2; Chapter 5 references it.

- Atomic refusal. Non-Cmn, AbsoluteHz, or an out-of-range result refuses the
  whole operation. Never saturate, never partially apply. Tombstoned and
  SYSTEM_DERIVED targets are still skipped: a deleted pitch is not an
  untransposable pitch, it is one the operation has nothing to say about.

targets becomes CanonicalSet<PitchId> at the type level, not a Vec plus a
dedup() someone can forget (PitchId's Ord is its canonical byte order). This
was never a convergence bug -- every replica replaying [p, p] double-
transposes identically -- but a canonicalization one. It is free today because
no operation-payload decoder exists yet; once one lands in Push 5, dedup
normalization would change the meaning of stored operations. Push 4a blocks
Push 5, and that is why.

Spec: req:pitch:transposition (algebra + the three refusals), and four
req:opcat:transpose-* requirements. Operation Catalog 0.7.0 -> 0.8.0; Binary
Format 0.6.0 -> 0.7.0 (disc 30, and a seq-strictly-increasing notation whose
decoder must reject a duplicate rather than normalize it away).

This commit is the design gate: the spec now declares MUSTs the code does not
yet satisfy -- editor-core still authors Transpose, and TransposeInterval does
not exist. The implementation follows in this push.

Also recorded: the two existing transpose tests are false locks. Gutting
graph_transpose_pitch leaves both green -- they call base-free reduce(), where
graph is None and the function never runs, and assert only OperationEffect.
Only editor-core's undo_and_redo_a_transpose, three crates away, catches it.

Gate: clippy 0, 30 targets / 964 passed / 0 failed, docs 0 under -D warnings,
conformance 8/8, all three spec documents build with no undefined references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:25:27 -04:00
..
src Schema major 2 Phase D: the repeat-authoring pair (code tranche) 2026-07-07 20:06:18 -04:00
tests Schema major 2 Phase B: snapshot side + honest stamps (data-model fills) 2026-07-07 16:30:07 -04:00
Cargo.toml A B C D F 2026-06-19 12:42:31 -04:00
DECISIONS.md Push 4a design gate: pin the transpose algebra, freeze the old operation 2026-07-09 15:25:27 -04:00
README.md Phase 2: spelling + decomposition pre-passes (Agent H) with F merge gate 2026-06-23 16:33:39 -04:00

README.md

epiphany-core

The Epiphany score graph: the in-memory representation of all musical content in a score, implementing the normative requirements of Chapters 25 of the core specification (spec/core_spec.pdf). This is Agent B's crate per spec/QUICKSTART.md — the largest scope — building only on Agent A's epiphany-determinism.

The graph is the canonical truth about the music; layout, serialization, and editing operations are downstream projections and consumers of it. — Chapter 5, Design Principles

What's here

Area Items Spec
Identifiers ReplicaId (+ SYSTEM_DERIVED), OperationId, the typed 128-bit family (EventId, PitchId, VoiceId, …), TypedObjectId, IdentityContext, derive_system_id Ch. 5 §"Identifiers"; Ch. 6 §"Operation Identity"
Time RationalTime (inline-or-promoted), MusicalPosition/MusicalDuration (typed algebra), WallClockTime/WallClockDuration, TimeAnchor/AnchorOffset, EventPosition/EventDuration/ConcreteDuration, TimeSignature/BeatGroup, NotatedComponent/NoteValue Ch. 3
Tempo TempoMap, Tempo, TempoSegment, TempoShape with closed-form musical_to_wallclock/wallclock_to_musical over constant/linear/exponential segments (curve deferred → TempoError) Ch. 3 §"Tempo Map"
Pitch Pitch, ScalePosition, IdentifiedPitch, PitchSpelling, the spelling-attachment subsystem, ReferencePitch, spell (single-pitch simplest spelling), all three equivalences (scale_position_equivalent, enharmonic_equivalent, sounding_equivalent) Ch. 2; Ch. 4 registry ids
Pre-passes derive_annotations: the real spelling pre-pass (Temperley line-of-fifths) + notational-decomposition pre-pass (metric greedy-aligned splitting) as canonical derived annotations (not stored), the eligibility TaxonomyReport, and resolve_spelling (authored-override precedence) Ch. 2 §"Spelling Pre-Pass"; Ch. 3 §"Notational Decomposition"
Events the Event taxonomy (7 variants) and the slotmap-backed EventArena Ch. 5 §"The Event Arena"
Graph Canvas, Region, Staff vs StaffInstance, Voice/VoiceOrigin, Measure, BarlineAlignmentGroup, aleatoric EventOrderingDAG (acyclic by construction), the full cross-cutting registry, the full top-level Score Ch. 5
Indexes ScoreIndexes: the four mandatory indexes (event-time, cross-cutting-reference, measure, spelling-attachment) Ch. 5 §"Indexes"
Invariants check_invariants over all 19 enumerated graph invariants, with a typed InvariantViolation witness per check Ch. 5 §"Graph Invariants"
Generators generators::valid_score/valid_score_rich (positive), violating_score (negative, per invariant), shrink (witness minimizer) QUICKSTART, Agent B hand-off

The identity discipline this crate enforces

  1. Replica + counter, big-endian canonical bytes. Every typed identifier is (replica << 64) | counter; its canonical 16-byte form is to_be_bytes() (8-byte replica, 8-byte counter) and the numeric Ord is the Appendix-D lexicographic byte order. Identity is exact, never tolerant.
  2. A reserved system namespace. ReplicaId::SYSTEM_DERIVED is rejected by ReplicaId::generate/from_entropy; system-derived ids (derive_system_id, derive_promoted_voice_id) live only in that namespace, with counters trunc64(BLAKE3(domain_tag || canonical_inputs)) via epiphany-determinism.
  3. Cross-kind confusion is a compile error. Each object kind has its own newtype; TypedObjectId tags them apart with a discriminant that is part of canonical content.

Graph invariants as property tests

The Chapter 5 invariants are property tests in CI, not runtime assertions in release builds (QUICKSTART). check_invariants returns every violation with a small witness. For each invariant generators provides:

  • a positive generator (valid_score / arbitrary_graph_corpus) whose output passes every check, and
  • a negative generator (violating_score) plus a shrinker (shrink) that minimizes a violating graph to a small witness while retaining only the structure the violation needs.

Generation is deterministic (a vendored SplitMix64), so a failing case reproduces from its seed — no platform entropy enters generation (Appendix D §"Randomness").

Implementation decisions

Per QUICKSTART "Decisions you'll need to make" (full rationale in DECISIONS.md):

  • Replica entropy: getrandom (decision 1). ReplicaId::generate re-draws until the value is not the reserved namespace.
  • Event-arena storage: slotmap (decision 2) plus a hash index for the required O(1) EventId lookup and generation-checked stale handles.
  • Sync only (decision 4): no async anywhere.
  • Current stable Rust (decision 5); MSRV pinned at the workspace's 1.77.
  • RationalTime's promoted arm uses num-rational's BigRational, the spec's reference design (Ch. 3 §"Recommended Implementation").
  • unsafe is forbidden crate-wide (#![forbid(unsafe_code)]).

Hand-off criteria (QUICKSTART, Agent B)

  • Every invariant has both a generator and a shrinker (generators::{valid_score, violating_score, shrink}; one per invariant, property-tested), plus targeted tests for the cross-cutting/anchor/tie sub-rules.
  • The arbitrary-graph corpus runs clean (generators::tests::positive_corpus_runs_clean, 500 graphs), and a breadth corpus (valid_score_rich: concurrent metric/proportional/ aleatoric regions, measures, triplet, tie, spanner, marker, chord symbol, decomposition, tombstones) runs clean over 200 seeds.
  • cargo test -p epiphany-core clean (69 unit + 5 integration).
  • cargo clippy --all-targets -- -D warnings clean; cargo doc clean under RUSTDOCFLAGS="-D warnings".

Depth of the invariant checks

The checks are not surface-level. In particular: invariant 3 computes per-clock event intervals and detects both disorder and overlap; invariant 7 resolves region extents to absolute wall-clock coordinates (wall-clock leaves, plus event/region/measure-start anchors — an event anchor is its region origin plus its region-relative position) and only skips pairs that can't be placed without the deferred tempo map; invariant 9 sweeps every reachable anchor (region extents, meter changes, measure starts, clef/key changes, user breaks, spanners, spelling ranges); invariant 10 resolves all graph references — cross-cutting anchor targets, annotation layers, tuplet parents, graphic objects, and event-internal references (indeterminate alternatives, trajectory event-pitches, graphic-event objects, cue sources); invariant 11 covers every id kind, plus tombstone/live collisions, SYSTEM_DERIVED misuse (including the score's own identity context), and arena index/well-formedness integrity (catching post-get_mut corruption); invariant 17 validates explicit and implicit (pitch-id-ascending) tie pairings, per-class adjacency, and the cross-voice position rule; invariant 18 recomputes the deterministic promoted-voice derivation. Enharmonic equivalence is a sounding notion (octave matters: C4 ≠ C5). Empty pitched events are rejected at the arena boundary and re-checked; IdentityContext::try_new rejects the reserved replica and counters use checked_add so a counter is never silently reused.

Known bounded limitations (deferred dependencies)

  • Tempo conversion integrates the piecewise map in closed form for Constant/Linear/Exponential segments (Chapter 3 §"Conversion"); only TempoShape::Curve is deferred to the open numerical algorithm (TempoError::CurveIntegrationUnsupported). Segment boundaries that cannot be placed without the score graph, and malformed segment sequences, return a TempoError, never a wrong answer. The inverse round-trips ordinary rhythms via a documented continued-fraction approximation (DECISIONS P11-7).
  • Region time-overlap (invariant 7) resolves extents to absolute wall-clock, now including musical event positions placed through the region's effective tempo map (its local_tempo_map, else the score map). Extents that still cannot be placed (no tempo defined, or a deferred curve) are skipped rather than rejected. Sound (no false positives), incomplete (DECISIONS P11-4).
  • System-promoted voice derivation (invariant 18) retains the winning and losing operation ids on VoiceOrigin::SystemPromoted; the checker recomputes the exact four-input derivation used by epiphany-ops.
  • The Chapter 4 tuning catalogPitchSpace/TuningSystem/ AccidentalRegistry definitions, the built-in catalog, the hierarchical resolver, and the position→frequency resolution function — is not an Agent B deliverable (the QUICKSTART lists those as referenced-by-id). This crate models the identifiers and the score-level ScoreTuningContext; Pitch::sounding_equivalent takes a caller-supplied frequency resolver. See DECISIONS P11-5.

Scope boundaries

The full Chapter 5 top-level Score shape is modeled (metadata, instruments, staff groups, parts, tuning context, tempo map, analysis layers, views) along with the complete CrossCuttingRegistry (slurs, ties, beams, tuplets, spanners, markers, repeats, analytical annotations, comments, graphic gestures, lyrics, chord symbols). The reference- and identity-bearing fields are modeled in depth; deeper bodies (tuning resolution, tempo-curve integration, part layout, view recipes, glyph/engraving detail) are Chapters 3/4/7 and later companions.

Engraving-display detail (StemConfiguration, ClefChange, KeySignatureChange, articulations, dynamics, line styles, spanner/marker visual kinds) is introduced informally here and fully defined in Chapter 7 — it belongs to Agent E (epiphany-layout-ir), and this crate carries minimal, clearly-marked placeholders for it. Operation envelopes, stamps (HLC), causal contexts, the canonical reduction, tombstone tracking, and conflict records are Chapter 6 / Agent C (epiphany-ops); epiphany-core defines only the OperationId they hang off and the tombstone-aware invariants.

Ambiguities discovered while building are not resolved in code — they are batched as Pass 11 candidates in DECISIONS.md (QUICKSTART, Process notes).