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) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-21 13:41:40 -04:00
parent cf81074ca0
commit b6728da5c6
2 changed files with 29 additions and 27 deletions

View File

@ -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(
<Self as CanonicalEncode>::to_canonical_bytes(self),
)
}
fn parse(
s: &crate::textvalue::Sexp,
) -> Result<Self, crate::textvalue::TextError> {
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 = <Self as CanonicalDecode>::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)
}
}
};
}

View File

@ -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