`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>
The nine type bodies fill to the ratified Ch5 shapes and the wire moves
to the Binary Format §Schema Major 2 layouts, review-hardened (high-
effort review; 8 findings, 7 fixed + 1 deferral sharpened).
epiphany-core:
- 19 new leaf types in graph.rs (SlurKind/CurveDirection/
CurvatureOverride/SpanStyle/LineStyle/SpaceUnit/SubBeam/
BeamGeometryOverride/SpannerKind+5 payload types/RepeatKind/Volta/
StaffBracketKind/MetadataEntry/MetadataValue/Timestamp/
SoundConfiguration/TranspositionInterval/UnpitchedMember) with the
ratified discriminants (cstyle_enum_codec! reused; tagged unions
hand-written); nine struct fills appended in wire order.
- The frozen wire forms generalized to a SHARED sub-codec layer
(enc_/dec_*_v1, v0==v1 for every type major 2 changed; vec framing
through enc_/dec_vec_v1) used by the new decode_v1_score/
encode_v1_score AND the rerouted v0 pair — major 2 touched types the
v0 walk had treated as unchanged (metadata, staves, cross_cutting,
staff instances transitively). Strict-canonical guards on every
versioned path; decode_canonical_versioned dispatches {0,1,2} with
composed default-fill migration.
- Invariants extended to the new reference-bearing fields (REVIEW
FIX): Beam.sub_beams events, RepeatKind DaCapo/DalSegno anchors,
Volta spans now covered by CrossCuttingRefsResolve + the anchor
model walk, with negative tests.
- Tests: v1 migration size-anchor (v1 omits exactly the appended
default bytes — the frozen encoder cannot drift), a non-default
round-trip covering every new field and every SpannerKind/RepeatKind
wire arm, fuzzer corpus gains genuine-v1 forms + the major-2 seam
with enforced must-decode-Ok on unmutated frozen forms.
epiphany-ops + epiphany-bundle (Phase C's semantic core, landed here
deliberately — the live codec flip makes CrossCutting/Staff/Metadata
payload bytes v2 immediately; shimming nine transitively-embedded
types the major-1 D1 way was throwaway):
- Minimal-stamping OperationKind::schema_major per the ratified rule
(CrossCutting/CreateStaff/SetMetadata always 2; CreateRegion 2 iff a
carried instance bears Some(staff_lines_override) else 1;
CreateStaffInstance/SetStaffLayout 2 iff Some else 0), unit-locked.
- Bundle op-block accept-set [0,2]; SchemaVersion::V2;
beyond-accept-set tests moved to major 3; testkit V2
stamp-derivation test.
- the_canonical_base_is_byte_identical_across_data_model_majors:
pinned blake3 of a seeded reduction — the companion's SHOULD that
the canonical base never moves across data-model majors.
- The op-payload migrate-on-read deferral restated precisely in
DECISIONS (no consumer byte-reconstructs op payloads today; the
first one must bring per-type frozen payload decoders).
Zero golden churn (fixtures deliberately carry v2 defaults).
Instrument::new consolidates the sweep's default fills. Full gate:
fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace suites,
conformance scale 1 (8/8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
The full-Score snapshot side of the three schema-major-1 data-model fields,
on a struct-level frozen v0 decoder. Snapshot-only: the CreateRegion op
payload stays byte-v0 (D2 owns the op-block migration + read-only).
Data model (epiphany-core):
- PitchRange { lowest, highest: Pitch } in pitch.rs; contains() is frame-guarded
(decides only within a shared PitchSpaceId) and treats a reversed range as
undecidable (None), not "everything out of range".
- Instrument.range: Option<PitchRange>; Region.permits_spanning_slurs: bool.
Codec:
- struct_codec! for PitchRange; Instrument -> {id,name,range}; Region appends
permits_spanning_slurs.
- Replaced the Phase-C byte-splice with a struct-level decode_v0_score: a
hand-written 19-field Score walk using dec_canvas_v0/dec_region_v0/
dec_instruments_v0 for the two changed fields (nested in Vecs) and the current
Codec for the other 17. Removed the now-unused Reader::pos().
- Region.canonical_bytes_v0() (+ enc_region_v0) is the frozen v0 op-payload
surface: CreateRegionOp embeds it so the op-envelope block stays byte-v0.
Advisory preconditions (epiphany-ops/validate.rs):
- PitchOutsideInstrumentRange: pitch-in-range via voice->instance->staff->
instrument (honoring instrument_override); "if any"/indeterminate-frame pass.
- Slur-spanning suppressed only when BOTH endpoint regions permit (AND
semantics; documented pending spec ratification of which region governs).
Tests: the three frozen-decoder fixtures (non-default v1 round-trip; a mirror
v0 encoder synthesizing genuine v0 bytes that migrate default-filling all three
fields, anchored by an independent byte-length check; the nested-Vec multi-
region case); the byte-v0 CreateRegion payload; the frame-aware/reversed-range
contains(); and 5 advisory tests. ~29 construction sites updated. Full gate
green (workspace tests, clippy -D warnings, fmt, rustdoc -D warnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
The first real data-model field of the schema-major-1 bump, and the point
where the dispatch seam flips from the Phase-B identity to a genuine
migration. Zero golden churn: CanvasLayoutDefaults::default() equals the
engraver's existing A4/8mm default, so no resolved geometry moves.
- Data model (core/graph.rs): Canvas gains `layout_defaults:
CanvasLayoutDefaults`, with new core geometry primitives CanvasSize /
CanvasMargins (staff-space CanonicalF64, A4/8mm Default -- core has no
geometry types of its own and must not depend on layout-ir). Exported from
the crate root; ~7 Canvas construction sites updated with
`..Default::default()`.
- Codec (core/codec.rs): struct_codec! for the three new types; Canvas v1
encodes `regions` then `layout_defaults`.
- The frozen migrate-on-read (decode_v0_score): a byte-level splice. v0 Score
bytes are the v1 layout minus Canvas.layout_defaults; Canvas is Score field 2
and its v0 layout was just `regions`. Read the v0 prefix (metadata,
canvas.regions) to find the split, insert the default CanvasLayoutDefaults
encoding, then decode the resulting v1 bytes. Total and default-filling (no
score context), frozen by value. A `pos()` accessor was added to Reader for
the splice.
- Test (v0_score_migrates_by_default_filling_layout_defaults): derives REAL v0
bytes by stripping the field from a v1 encoding, then migrates them back and
checks the original score is reconstructed with the default refilled. A wrong
splice offset corrupts the bytes and fails the decode, so it guards the
frozen v0 assumptions.
Deferred: routing the migrate through a bundle acceleration_snapshots slot (a
Phase-B Finding-3 item). The migrate logic is proven by the core unit test and
the bundle read path by the canonical-base roundtrip; combining them hits
cross-crate friction (constructing v0 bytes needs core-internal Reader, which
testkit can't reach and core-below-bundle can't stage) for marginal coverage.
The forward scenario (v1 reader migrating a v0 bundle's major-0 acceleration
snapshot) needs no new bundle code -- the v0 snapshot passes the exact gate.
863 workspace tests pass; clippy -D warnings, fmt --check, rustdoc -D warnings
all clean; no render/snapshot goldens changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd