From b6728da5c6c5afa7d97f3da6c00efb2219ccbc5e Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 21 Jul 2026 13:41:40 -0400 Subject: [PATCH] Text Projection: every graph id projects, because graph_id! says so `TransactionId` had no `TextValue`, which blocked the operation layer: it is a core type, so epiphany-ops cannot implement a core trait for it. The fix is not the missing line. `bytes_text_value!` was a hand-maintained list of thirty ids sitting parallel to the `graph_id!` invocations that declare them -- the same shape that has cost this project four bugs -- and `TransactionId`'s absence was that latent bug already biting. `graph_id!` now generates the `TextValue` impl beside the id itself: every graph id is a byte-string leaf by definition, so the list that declares them is the list that projects them. `bytes_text_value!` keeps only the leaves that are genuinely not graph ids -- `ContentHash`, `TypedObjectId`, `ReplicaId`, `OperationId` -- shrinking from thirty entries to four. A graph id added tomorrow gets its projection for free and cannot be forgotten. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/epiphany-core/src/ids.rs | 28 +++++++++++++++++++++ crates/epiphany-core/src/textvalue_impls.rs | 28 +-------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/epiphany-core/src/ids.rs b/crates/epiphany-core/src/ids.rs index 892f534..102243a 100644 --- a/crates/epiphany-core/src/ids.rs +++ b/crates/epiphany-core/src/ids.rs @@ -244,6 +244,34 @@ macro_rules! graph_id { } // Canonical order *is* byte order for typed identifiers (Appendix D). impl CanonicalByteOrder for $name {} + + impl crate::textvalue::TextValue for $name { + fn project(&self) -> crate::textvalue::Sexp { + crate::textvalue::Sexp::Bytes( + ::to_canonical_bytes(self), + ) + } + + fn parse( + s: &crate::textvalue::Sexp, + ) -> Result { + let crate::textvalue::Sexp::Bytes(bytes) = s else { + return Err(crate::textvalue::TextError::Expected { + expected: stringify!($name), + found: crate::textvalue_impls::class_of(s), + }); + }; + let value = ::decode_canonical(bytes) + .map_err(|_| crate::textvalue::TextError::NotCanonical(stringify!($name)))?; + if value.to_canonical_bytes() != *bytes { + return Err(crate::textvalue::TextError::NotCanonical(concat!( + stringify!($name), + " is not canonically encoded" + ))); + } + Ok(value) + } + } }; } diff --git a/crates/epiphany-core/src/textvalue_impls.rs b/crates/epiphany-core/src/textvalue_impls.rs index 83cc0b8..0ba9edd 100644 --- a/crates/epiphany-core/src/textvalue_impls.rs +++ b/crates/epiphany-core/src/textvalue_impls.rs @@ -77,34 +77,8 @@ pub(crate) fn class_of(s: &Sexp) -> &'static str { bytes_text_value! { crate::ids::ReplicaId => "ReplicaId", crate::ids::OperationId => "OperationId", - crate::ids::EventId => "EventId", - crate::ids::PitchId => "PitchId", - crate::ids::VoiceId => "VoiceId", - crate::ids::StaffId => "StaffId", - crate::ids::StaffInstanceId => "StaffInstanceId", - crate::ids::StaffGroupId => "StaffGroupId", - crate::ids::RegionId => "RegionId", - crate::ids::InstrumentId => "InstrumentId", - crate::ids::PartDefinitionId => "PartDefinitionId", - crate::ids::MeasureId => "MeasureId", - crate::ids::BarlineAlignmentGroupId => "BarlineAlignmentGroupId", - crate::ids::SlurId => "SlurId", - crate::ids::TieId => "TieId", - crate::ids::BeamId => "BeamId", - crate::ids::SpannerId => "SpannerId", - crate::ids::TupletId => "TupletId", - crate::ids::MarkerId => "MarkerId", - crate::ids::AnalyticalAnnotationId => "AnalyticalAnnotationId", - crate::ids::CommentId => "CommentId", - crate::ids::RepeatStructureId => "RepeatStructureId", - crate::ids::LyricLineId => "LyricLineId", - crate::ids::ChordSymbolId => "ChordSymbolId", - crate::ids::GraphicObjectId => "GraphicObjectId", - crate::ids::GraphicGestureId => "GraphicGestureId", - crate::ids::TimeSignatureId => "TimeSignatureId", - crate::ids::AnalysisLayerId => "AnalysisLayerId", - crate::ids::ViewId => "ViewId", ContentHash => "ContentHash", + crate::ids::TypedObjectId => "TypedObjectId", } /// A `CanonicalF64` is its eight canonical little-endian IEEE 754 bytes, never a