941 lines
35 KiB
Rust
941 lines
35 KiB
Rust
//! The typed identifier family (Chapter 5 §"Identifiers").
|
|
//!
|
|
//! Every named object in the score graph carries a typed 128-bit identifier
|
|
//! composed of a 64-bit [`ReplicaId`] and a 64-bit monotonic counter local to
|
|
//! that replica (Chapter 5 §"Identifier Generation"). Each object *kind* has
|
|
//! its own newtype so that cross-kind confusion is a compile-time error
|
|
//! (Chapter 5 §"Design Principles": "Typed identifiers").
|
|
//!
|
|
//! ## Canonical byte form
|
|
//!
|
|
//! Appendix D §"Ordered Iteration" fixes the canonical byte form of every
|
|
//! typed identifier: *16 bytes — 8-byte replica, 8-byte counter, big-endian* —
|
|
//! and the canonical total order is lexicographic ascending on those bytes.
|
|
//! We store each identifier as a single `u128` equal to
|
|
//! `(replica << 64) | counter`; then [`u128::to_be_bytes`] *is* the canonical
|
|
//! 16-byte form, and the derived numeric `Ord` on the `u128` is exactly the
|
|
//! lexicographic byte order. Identity is therefore exact and never tolerant
|
|
//! (Appendix D §"Tolerance Classes").
|
|
//!
|
|
//! ## System-derived identifiers
|
|
//!
|
|
//! [`ReplicaId::SYSTEM_DERIVED`] (`0xffff_ffff_ffff_ffff`) is reserved for
|
|
//! deterministically-derived system identifiers (system-promoted voices,
|
|
//! content-derived ids). User-authored replicas must never use it. The 64-bit
|
|
//! counter of such an identifier is `trunc64(BLAKE3(domain_tag || inputs))`
|
|
//! via [`epiphany_determinism::derive_system_counter`]; [`derive_system_id`]
|
|
//! wraps that into any typed identifier.
|
|
|
|
use epiphany_determinism::{
|
|
derive_system_counter, CanonicalByteOrder, CanonicalDecode, CanonicalEncode, DecodeError,
|
|
SystemDomainTag,
|
|
};
|
|
|
|
/// A replica identifier: the 64-bit high half of every graph identifier
|
|
/// (Chapter 5 §"Identifier Generation"). Generated once at score creation with
|
|
/// at least 64 bits of CSPRNG entropy.
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
pub struct ReplicaId(pub u64);
|
|
|
|
impl ReplicaId {
|
|
/// Reserved replica identifier for deterministically-derived system
|
|
/// identifiers (system-promoted voices, content-derived conflict ids,
|
|
/// future deterministic synthetic identifiers). User-authored replicas
|
|
/// **must not** use this value (Chapter 5 §"System-Derived Identifier
|
|
/// Namespace").
|
|
pub const SYSTEM_DERIVED: ReplicaId = ReplicaId(0xffff_ffff_ffff_ffff);
|
|
|
|
/// Whether this is the reserved [`Self::SYSTEM_DERIVED`] namespace.
|
|
#[inline]
|
|
pub const fn is_system_derived(self) -> bool {
|
|
self.0 == Self::SYSTEM_DERIVED.0
|
|
}
|
|
|
|
/// Wraps raw entropy into a replica identifier, rejecting the reserved
|
|
/// [`Self::SYSTEM_DERIVED`] value (Chapter 5: "Implementations creating a
|
|
/// new score MUST reject this value … and MUST regenerate"). Returns `None`
|
|
/// if the entropy happens to land on the reserved value so the caller
|
|
/// re-draws — exactly what [`Self::generate`] does.
|
|
#[inline]
|
|
pub fn from_entropy(bytes: [u8; 8]) -> Option<Self> {
|
|
let v = ReplicaId(u64::from_le_bytes(bytes));
|
|
if v.is_system_derived() {
|
|
None
|
|
} else {
|
|
Some(v)
|
|
}
|
|
}
|
|
|
|
/// Generates a fresh replica identifier from the platform CSPRNG
|
|
/// (QUICKSTART decision 1: `getrandom`), re-drawing until the value is not
|
|
/// the reserved [`Self::SYSTEM_DERIVED`] namespace. This is the only
|
|
/// sanctioned use of platform randomness in the core (Appendix D
|
|
/// §"Randomness"); the entropy enters canonical state only via the
|
|
/// identifiers it seeds.
|
|
///
|
|
/// # Panics
|
|
/// Panics only if the platform entropy source itself fails, which on a
|
|
/// conforming platform does not happen; identifier minting cannot proceed
|
|
/// without it.
|
|
pub fn generate() -> Self {
|
|
loop {
|
|
let mut bytes = [0u8; 8];
|
|
getrandom::getrandom(&mut bytes).expect("platform CSPRNG unavailable");
|
|
if let Some(id) = Self::from_entropy(bytes) {
|
|
return id;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The canonical 8 big-endian bytes.
|
|
#[inline]
|
|
pub const fn to_be_bytes(self) -> [u8; 8] {
|
|
self.0.to_be_bytes()
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Debug for ReplicaId {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
if self.is_system_derived() {
|
|
f.write_str("ReplicaId(SYSTEM_DERIVED)")
|
|
} else {
|
|
write!(f, "ReplicaId({:016x})", self.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CanonicalEncode for ReplicaId {
|
|
#[inline]
|
|
fn encode_canonical(&self, out: &mut Vec<u8>) {
|
|
out.extend_from_slice(&self.to_be_bytes());
|
|
}
|
|
}
|
|
impl CanonicalDecode for ReplicaId {
|
|
#[inline]
|
|
fn decode_canonical(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
let arr: [u8; 8] = bytes
|
|
.try_into()
|
|
.map_err(|_| DecodeError::UnexpectedLength {
|
|
expected: 8,
|
|
actual: bytes.len(),
|
|
})?;
|
|
Ok(ReplicaId(u64::from_be_bytes(arr)))
|
|
}
|
|
}
|
|
impl CanonicalByteOrder for ReplicaId {}
|
|
|
|
/// Behaviour shared by every typed 128-bit graph identifier. Implemented by
|
|
/// the `graph_id!` macro; consumed by [`IdentityContext::mint`] and
|
|
/// [`derive_system_id`] so minting is generic over the identifier kind.
|
|
pub trait GraphId: Copy + Eq + Ord + core::hash::Hash {
|
|
/// Builds an identifier from its replica and counter parts.
|
|
fn from_parts(replica: ReplicaId, counter: u64) -> Self;
|
|
/// The replica half.
|
|
fn replica(self) -> ReplicaId;
|
|
/// The counter half.
|
|
fn counter(self) -> u64;
|
|
/// The whole identifier as a `u128` (`(replica << 64) | counter`).
|
|
fn as_u128(self) -> u128;
|
|
/// The canonical 16-byte big-endian form (Appendix D §"Ordered Iteration").
|
|
fn canonical_bytes(self) -> [u8; 16];
|
|
}
|
|
|
|
/// Defines a typed 128-bit graph identifier newtype over `u128`, with the
|
|
/// canonical byte form and ordering fixed by Appendix D. The spec writes each
|
|
/// of these as e.g. `pub struct EventId(u128)`; this macro keeps that shape
|
|
/// while sharing the replica/counter logic and the canonical-encoding impls.
|
|
macro_rules! graph_id {
|
|
($(#[$meta:meta])* $name:ident) => {
|
|
$(#[$meta])*
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
pub struct $name(pub u128);
|
|
|
|
impl $name {
|
|
/// Builds the identifier from a replica and a counter. The replica
|
|
/// occupies the high 64 bits so the numeric order matches the
|
|
/// canonical byte order.
|
|
#[inline]
|
|
pub const fn new(replica: ReplicaId, counter: u64) -> Self {
|
|
$name(((replica.0 as u128) << 64) | (counter as u128))
|
|
}
|
|
|
|
/// Wraps a raw `u128` (e.g. when decoding).
|
|
#[inline]
|
|
pub const fn from_raw(raw: u128) -> Self {
|
|
$name(raw)
|
|
}
|
|
|
|
/// The raw `u128`.
|
|
#[inline]
|
|
pub const fn as_u128(self) -> u128 {
|
|
self.0
|
|
}
|
|
|
|
/// The replica half (high 64 bits).
|
|
#[inline]
|
|
pub const fn replica(self) -> ReplicaId {
|
|
ReplicaId((self.0 >> 64) as u64)
|
|
}
|
|
|
|
/// The counter half (low 64 bits).
|
|
#[inline]
|
|
pub const fn counter(self) -> u64 {
|
|
self.0 as u64
|
|
}
|
|
|
|
/// The canonical 16-byte big-endian form: 8-byte replica then
|
|
/// 8-byte counter (Appendix D §"Ordered Iteration").
|
|
#[inline]
|
|
pub const fn canonical_bytes(self) -> [u8; 16] {
|
|
self.0.to_be_bytes()
|
|
}
|
|
}
|
|
|
|
impl GraphId for $name {
|
|
#[inline]
|
|
fn from_parts(replica: ReplicaId, counter: u64) -> Self {
|
|
$name::new(replica, counter)
|
|
}
|
|
#[inline]
|
|
fn replica(self) -> ReplicaId {
|
|
$name::replica(self)
|
|
}
|
|
#[inline]
|
|
fn counter(self) -> u64 {
|
|
$name::counter(self)
|
|
}
|
|
#[inline]
|
|
fn as_u128(self) -> u128 {
|
|
self.0
|
|
}
|
|
#[inline]
|
|
fn canonical_bytes(self) -> [u8; 16] {
|
|
$name::canonical_bytes(self)
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Debug for $name {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
write!(
|
|
f,
|
|
concat!(stringify!($name), "({:016x}:{:016x})"),
|
|
(self.0 >> 64) as u64,
|
|
self.0 as u64
|
|
)
|
|
}
|
|
}
|
|
|
|
impl CanonicalEncode for $name {
|
|
#[inline]
|
|
fn encode_canonical(&self, out: &mut Vec<u8>) {
|
|
out.extend_from_slice(&self.canonical_bytes());
|
|
}
|
|
}
|
|
impl CanonicalDecode for $name {
|
|
#[inline]
|
|
fn decode_canonical(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
let arr: [u8; 16] =
|
|
bytes.try_into().map_err(|_| DecodeError::UnexpectedLength {
|
|
expected: 16,
|
|
actual: bytes.len(),
|
|
})?;
|
|
Ok($name(u128::from_be_bytes(arr)))
|
|
}
|
|
}
|
|
// Canonical order *is* byte order for typed identifiers (Appendix D).
|
|
impl CanonicalByteOrder for $name {}
|
|
};
|
|
}
|
|
|
|
graph_id!(
|
|
/// Identifies a rhythmic event in the [`crate::EventArena`].
|
|
EventId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a pitch embedded in an event (Chapter 2 §"Pitch Identifiers").
|
|
PitchId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a polyphonic voice within a staff instance.
|
|
VoiceId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a global, abstract staff (Chapter 5 §"Staves: Identity Versus
|
|
/// Instance").
|
|
StaffId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a region-local manifestation of a [`StaffId`].
|
|
StaffInstanceId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a staff grouping (grand staff, bracket, choral group).
|
|
StaffGroupId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a region of the canvas.
|
|
RegionId
|
|
);
|
|
graph_id!(
|
|
/// Identifies an abstract instrument definition.
|
|
InstrumentId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a part-extraction view definition.
|
|
PartDefinitionId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a measure (belongs to exactly one staff instance).
|
|
MeasureId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a barline-alignment group.
|
|
BarlineAlignmentGroupId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a slur / phrase mark.
|
|
SlurId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a tie.
|
|
TieId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a beam.
|
|
BeamId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a spanner (hairpin, octave line, pedal, …).
|
|
SpannerId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a tuplet grouping object (Chapter 3 §"Tuplets").
|
|
TupletId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a point marker (rehearsal mark, segno, …).
|
|
MarkerId
|
|
);
|
|
graph_id!(
|
|
/// Identifies an analytical annotation.
|
|
AnalyticalAnnotationId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a review-mode comment thread.
|
|
CommentId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a repeat structure (simple repeat, da capo, volta).
|
|
RepeatStructureId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a lyric line.
|
|
LyricLineId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a chord symbol.
|
|
ChordSymbolId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a graphic object in the canvas's graphic storage.
|
|
GraphicObjectId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a multi-object graphic gesture.
|
|
GraphicGestureId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a time signature object.
|
|
TimeSignatureId
|
|
);
|
|
graph_id!(
|
|
/// Identifies an analysis layer (Chapter 5 §"Analysis Layers and Views").
|
|
AnalysisLayerId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a view definition (Chapter 5 §"Views").
|
|
ViewId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a transaction grouping of operations (Chapter 6).
|
|
TransactionId
|
|
);
|
|
graph_id!(
|
|
/// Identifies a diagnostic integrity anomaly (Chapter 5
|
|
/// §"System-Derived Counter Collisions").
|
|
IntegrityAnomalyId
|
|
);
|
|
graph_id!(
|
|
/// Identifies an extension-introduced object kind, used by
|
|
/// [`TypedObjectId::Registered`].
|
|
ObjectKindRegistryId
|
|
);
|
|
|
|
/// The stable identity of an operation (Chapter 6 §"Operation Identity and
|
|
/// Stamps"): a replica plus an authoring counter. Defined here, with the rest
|
|
/// of the identifier family, because graph types reference it
|
|
/// (notably [`crate::VoiceOrigin::SystemPromoted`]); `epiphany-ops` (Agent C)
|
|
/// builds the operation *semantics* on top of it.
|
|
///
|
|
/// Identity is fixed at authoring time and never changes under reordering,
|
|
/// retransmission, or merging. Ordering is `(replica, counter)`, i.e. the same
|
|
/// lexicographic order as the typed graph identifiers.
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
pub struct OperationId {
|
|
/// Authoring replica.
|
|
pub replica: ReplicaId,
|
|
/// Authoring counter, monotonic within the replica.
|
|
pub counter: u64,
|
|
}
|
|
|
|
impl OperationId {
|
|
/// Builds an operation identifier.
|
|
#[inline]
|
|
pub const fn new(replica: ReplicaId, counter: u64) -> Self {
|
|
OperationId { replica, counter }
|
|
}
|
|
|
|
/// The canonical 16-byte big-endian form: 8-byte replica then 8-byte
|
|
/// counter, matching the typed-identifier convention (Appendix D).
|
|
#[inline]
|
|
pub fn canonical_bytes(self) -> [u8; 16] {
|
|
let mut out = [0u8; 16];
|
|
out[0..8].copy_from_slice(&self.replica.to_be_bytes());
|
|
out[8..16].copy_from_slice(&self.counter.to_be_bytes());
|
|
out
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Debug for OperationId {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
write!(
|
|
f,
|
|
"OperationId({:016x}:{:016x})",
|
|
self.replica.0, self.counter
|
|
)
|
|
}
|
|
}
|
|
|
|
impl CanonicalEncode for OperationId {
|
|
#[inline]
|
|
fn encode_canonical(&self, out: &mut Vec<u8>) {
|
|
out.extend_from_slice(&self.canonical_bytes());
|
|
}
|
|
}
|
|
impl CanonicalDecode for OperationId {
|
|
#[inline]
|
|
fn decode_canonical(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
let arr: [u8; 16] = bytes
|
|
.try_into()
|
|
.map_err(|_| DecodeError::UnexpectedLength {
|
|
expected: 16,
|
|
actual: bytes.len(),
|
|
})?;
|
|
let mut r = [0u8; 8];
|
|
let mut c = [0u8; 8];
|
|
r.copy_from_slice(&arr[0..8]);
|
|
c.copy_from_slice(&arr[8..16]);
|
|
Ok(OperationId::new(
|
|
ReplicaId(u64::from_be_bytes(r)),
|
|
u64::from_be_bytes(c),
|
|
))
|
|
}
|
|
}
|
|
impl CanonicalByteOrder for OperationId {}
|
|
|
|
/// A tagged identifier over every typed identifier kind in the score graph
|
|
/// (Chapter 5 §"Identifiers"). Used wherever an object is referenced
|
|
/// generically: cross-cutting endpoints, conflict `affected_objects`, repair
|
|
/// records, edit barriers.
|
|
///
|
|
/// The variant tag is part of canonical content: distinct variants with the
|
|
/// same underlying `u128` are distinct `TypedObjectId`s. The canonical byte
|
|
/// form is a 16-bit big-endian discriminant followed by the payload's
|
|
/// canonical bytes (Chapter 5 `TypedObjectId::canonical_bytes`).
|
|
///
|
|
/// **Discriminant assignment** (a prototype decision; see `DECISIONS.md`): the
|
|
/// spec fixes the *shape* of the encoding but not the numeric discriminant per
|
|
/// variant. We assign them by declaration order, starting at 0, with
|
|
/// [`TypedObjectId::Registered`] last. Because these values enter canonical
|
|
/// state, the choice is recorded as a Pass 11 candidate for the spec to pin.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
|
pub enum TypedObjectId {
|
|
Event(EventId),
|
|
Pitch(PitchId),
|
|
Voice(VoiceId),
|
|
Staff(StaffId),
|
|
StaffInstance(StaffInstanceId),
|
|
StaffGroup(StaffGroupId),
|
|
Region(RegionId),
|
|
Instrument(InstrumentId),
|
|
PartDefinition(PartDefinitionId),
|
|
Measure(MeasureId),
|
|
BarlineAlignmentGroup(BarlineAlignmentGroupId),
|
|
Slur(SlurId),
|
|
Tie(TieId),
|
|
Beam(BeamId),
|
|
Spanner(SpannerId),
|
|
Marker(MarkerId),
|
|
AnalyticalAnnotation(AnalyticalAnnotationId),
|
|
Comment(CommentId),
|
|
GraphicObject(GraphicObjectId),
|
|
GraphicGesture(GraphicGestureId),
|
|
TimeSignature(TimeSignatureId),
|
|
AnalysisLayer(AnalysisLayerId),
|
|
Tuplet(TupletId),
|
|
RepeatStructure(RepeatStructureId),
|
|
LyricLine(LyricLineId),
|
|
ChordSymbol(ChordSymbolId),
|
|
View(ViewId),
|
|
/// Extension-defined object kind, identified by registry id plus the
|
|
/// extension's own 128-bit identifier.
|
|
Registered(ObjectKindRegistryId, u128),
|
|
}
|
|
|
|
impl TypedObjectId {
|
|
/// The 16-bit discriminant for this variant (see the type docs).
|
|
pub fn discriminant(&self) -> u16 {
|
|
match self {
|
|
TypedObjectId::Event(_) => 0,
|
|
TypedObjectId::Pitch(_) => 1,
|
|
TypedObjectId::Voice(_) => 2,
|
|
TypedObjectId::Staff(_) => 3,
|
|
TypedObjectId::StaffInstance(_) => 4,
|
|
TypedObjectId::StaffGroup(_) => 5,
|
|
TypedObjectId::Region(_) => 6,
|
|
TypedObjectId::Instrument(_) => 7,
|
|
TypedObjectId::PartDefinition(_) => 8,
|
|
TypedObjectId::Measure(_) => 9,
|
|
TypedObjectId::BarlineAlignmentGroup(_) => 10,
|
|
TypedObjectId::Slur(_) => 11,
|
|
TypedObjectId::Tie(_) => 12,
|
|
TypedObjectId::Beam(_) => 13,
|
|
TypedObjectId::Spanner(_) => 14,
|
|
TypedObjectId::Marker(_) => 15,
|
|
TypedObjectId::AnalyticalAnnotation(_) => 16,
|
|
TypedObjectId::Comment(_) => 17,
|
|
TypedObjectId::GraphicObject(_) => 18,
|
|
TypedObjectId::GraphicGesture(_) => 19,
|
|
TypedObjectId::TimeSignature(_) => 20,
|
|
TypedObjectId::AnalysisLayer(_) => 21,
|
|
TypedObjectId::Tuplet(_) => 22,
|
|
TypedObjectId::RepeatStructure(_) => 23,
|
|
TypedObjectId::LyricLine(_) => 24,
|
|
TypedObjectId::ChordSymbol(_) => 25,
|
|
TypedObjectId::View(_) => 26,
|
|
TypedObjectId::Registered(..) => 27,
|
|
}
|
|
}
|
|
|
|
/// The underlying 128-bit payload, ignoring the variant tag. For
|
|
/// [`TypedObjectId::Registered`] this is the extension's own identifier
|
|
/// (the registry id is encoded separately in the canonical bytes).
|
|
fn payload_u128(&self) -> u128 {
|
|
match self {
|
|
TypedObjectId::Event(i) => i.as_u128(),
|
|
TypedObjectId::Pitch(i) => i.as_u128(),
|
|
TypedObjectId::Voice(i) => i.as_u128(),
|
|
TypedObjectId::Staff(i) => i.as_u128(),
|
|
TypedObjectId::StaffInstance(i) => i.as_u128(),
|
|
TypedObjectId::StaffGroup(i) => i.as_u128(),
|
|
TypedObjectId::Region(i) => i.as_u128(),
|
|
TypedObjectId::Instrument(i) => i.as_u128(),
|
|
TypedObjectId::PartDefinition(i) => i.as_u128(),
|
|
TypedObjectId::Measure(i) => i.as_u128(),
|
|
TypedObjectId::BarlineAlignmentGroup(i) => i.as_u128(),
|
|
TypedObjectId::Slur(i) => i.as_u128(),
|
|
TypedObjectId::Tie(i) => i.as_u128(),
|
|
TypedObjectId::Beam(i) => i.as_u128(),
|
|
TypedObjectId::Spanner(i) => i.as_u128(),
|
|
TypedObjectId::Marker(i) => i.as_u128(),
|
|
TypedObjectId::AnalyticalAnnotation(i) => i.as_u128(),
|
|
TypedObjectId::Comment(i) => i.as_u128(),
|
|
TypedObjectId::GraphicObject(i) => i.as_u128(),
|
|
TypedObjectId::GraphicGesture(i) => i.as_u128(),
|
|
TypedObjectId::TimeSignature(i) => i.as_u128(),
|
|
TypedObjectId::AnalysisLayer(i) => i.as_u128(),
|
|
TypedObjectId::Tuplet(i) => i.as_u128(),
|
|
TypedObjectId::RepeatStructure(i) => i.as_u128(),
|
|
TypedObjectId::LyricLine(i) => i.as_u128(),
|
|
TypedObjectId::ChordSymbol(i) => i.as_u128(),
|
|
TypedObjectId::View(i) => i.as_u128(),
|
|
TypedObjectId::Registered(_, raw) => *raw,
|
|
}
|
|
}
|
|
|
|
/// Canonical byte form for hashing, ordering, and equality (Chapter 5
|
|
/// `TypedObjectId::canonical_bytes`): the 16-bit big-endian discriminant
|
|
/// followed by the variant payload's canonical bytes.
|
|
///
|
|
/// For [`TypedObjectId::Registered`] the payload is the registry id's
|
|
/// 16 canonical bytes followed by the extension's own 16 `u128` bytes, so
|
|
/// the form stays fixed-width and unambiguously decodable.
|
|
pub fn canonical_bytes(&self) -> Vec<u8> {
|
|
let mut out = Vec::with_capacity(2 + 32);
|
|
out.extend_from_slice(&self.discriminant().to_be_bytes());
|
|
match self {
|
|
TypedObjectId::Registered(reg, raw) => {
|
|
out.extend_from_slice(®.canonical_bytes());
|
|
out.extend_from_slice(&raw.to_be_bytes());
|
|
}
|
|
other => out.extend_from_slice(&other.payload_u128().to_be_bytes()),
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for TypedObjectId {
|
|
#[inline]
|
|
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
impl Ord for TypedObjectId {
|
|
#[inline]
|
|
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
|
// The canonical byte form is the normative total order (Appendix D);
|
|
// comparing it keeps `Ord` consistent with hashing and equality.
|
|
self.canonical_bytes().cmp(&other.canonical_bytes())
|
|
}
|
|
}
|
|
|
|
impl CanonicalEncode for TypedObjectId {
|
|
#[inline]
|
|
fn encode_canonical(&self, out: &mut Vec<u8>) {
|
|
out.extend_from_slice(&self.canonical_bytes());
|
|
}
|
|
}
|
|
impl CanonicalByteOrder for TypedObjectId {}
|
|
|
|
impl CanonicalDecode for TypedObjectId {
|
|
fn decode_canonical(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
if bytes.len() < 2 {
|
|
return Err(DecodeError::UnexpectedLength {
|
|
expected: 18,
|
|
actual: bytes.len(),
|
|
});
|
|
}
|
|
let disc = u16::from_be_bytes([bytes[0], bytes[1]]);
|
|
let rest = &bytes[2..];
|
|
// Helper: read exactly one 16-byte id payload.
|
|
let one_id = |rest: &[u8]| -> Result<u128, DecodeError> {
|
|
let arr: [u8; 16] = rest.try_into().map_err(|_| DecodeError::UnexpectedLength {
|
|
expected: 16,
|
|
actual: rest.len(),
|
|
})?;
|
|
Ok(u128::from_be_bytes(arr))
|
|
};
|
|
Ok(match disc {
|
|
0 => TypedObjectId::Event(EventId::from_raw(one_id(rest)?)),
|
|
1 => TypedObjectId::Pitch(PitchId::from_raw(one_id(rest)?)),
|
|
2 => TypedObjectId::Voice(VoiceId::from_raw(one_id(rest)?)),
|
|
3 => TypedObjectId::Staff(StaffId::from_raw(one_id(rest)?)),
|
|
4 => TypedObjectId::StaffInstance(StaffInstanceId::from_raw(one_id(rest)?)),
|
|
5 => TypedObjectId::StaffGroup(StaffGroupId::from_raw(one_id(rest)?)),
|
|
6 => TypedObjectId::Region(RegionId::from_raw(one_id(rest)?)),
|
|
7 => TypedObjectId::Instrument(InstrumentId::from_raw(one_id(rest)?)),
|
|
8 => TypedObjectId::PartDefinition(PartDefinitionId::from_raw(one_id(rest)?)),
|
|
9 => TypedObjectId::Measure(MeasureId::from_raw(one_id(rest)?)),
|
|
10 => TypedObjectId::BarlineAlignmentGroup(BarlineAlignmentGroupId::from_raw(one_id(
|
|
rest,
|
|
)?)),
|
|
11 => TypedObjectId::Slur(SlurId::from_raw(one_id(rest)?)),
|
|
12 => TypedObjectId::Tie(TieId::from_raw(one_id(rest)?)),
|
|
13 => TypedObjectId::Beam(BeamId::from_raw(one_id(rest)?)),
|
|
14 => TypedObjectId::Spanner(SpannerId::from_raw(one_id(rest)?)),
|
|
15 => TypedObjectId::Marker(MarkerId::from_raw(one_id(rest)?)),
|
|
16 => {
|
|
TypedObjectId::AnalyticalAnnotation(AnalyticalAnnotationId::from_raw(one_id(rest)?))
|
|
}
|
|
17 => TypedObjectId::Comment(CommentId::from_raw(one_id(rest)?)),
|
|
18 => TypedObjectId::GraphicObject(GraphicObjectId::from_raw(one_id(rest)?)),
|
|
19 => TypedObjectId::GraphicGesture(GraphicGestureId::from_raw(one_id(rest)?)),
|
|
20 => TypedObjectId::TimeSignature(TimeSignatureId::from_raw(one_id(rest)?)),
|
|
21 => TypedObjectId::AnalysisLayer(AnalysisLayerId::from_raw(one_id(rest)?)),
|
|
22 => TypedObjectId::Tuplet(TupletId::from_raw(one_id(rest)?)),
|
|
23 => TypedObjectId::RepeatStructure(RepeatStructureId::from_raw(one_id(rest)?)),
|
|
24 => TypedObjectId::LyricLine(LyricLineId::from_raw(one_id(rest)?)),
|
|
25 => TypedObjectId::ChordSymbol(ChordSymbolId::from_raw(one_id(rest)?)),
|
|
26 => TypedObjectId::View(ViewId::from_raw(one_id(rest)?)),
|
|
27 => {
|
|
let arr: [u8; 32] = rest.try_into().map_err(|_| DecodeError::UnexpectedLength {
|
|
expected: 32,
|
|
actual: rest.len(),
|
|
})?;
|
|
let mut reg = [0u8; 16];
|
|
let mut raw = [0u8; 16];
|
|
reg.copy_from_slice(&arr[0..16]);
|
|
raw.copy_from_slice(&arr[16..32]);
|
|
TypedObjectId::Registered(
|
|
ObjectKindRegistryId::from_raw(u128::from_be_bytes(reg)),
|
|
u128::from_be_bytes(raw),
|
|
)
|
|
}
|
|
_ => return Err(DecodeError::MalformedDomainTag),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The replica identifier plus identifier-generation state of a score
|
|
/// (Chapter 5 `IdentityContext`). A single monotonic counter suffices for all
|
|
/// identifier kinds.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub struct IdentityContext {
|
|
/// This replica's identifier, generated at score creation.
|
|
pub replica_id: ReplicaId,
|
|
/// Monotonic counter for new identifiers; never reused, even after
|
|
/// deletion (Chapter 5 §"Identifier Generation").
|
|
pub next_counter: u64,
|
|
}
|
|
|
|
impl IdentityContext {
|
|
/// Starts a fresh identity context for a replica, with the counter at 0.
|
|
///
|
|
/// The caller is responsible for passing a non-reserved replica; a
|
|
/// `debug_assert` guards against the reserved [`ReplicaId::SYSTEM_DERIVED`]
|
|
/// namespace (Chapter 5: user-authored replicas must not use it), and the
|
|
/// score-level invariant check ([`crate::check_invariants`], invariant 11)
|
|
/// enforces it in every build profile. Use [`IdentityContext::try_new`] for
|
|
/// a checked constructor.
|
|
#[inline]
|
|
pub fn new(replica_id: ReplicaId) -> Self {
|
|
debug_assert!(
|
|
!replica_id.is_system_derived(),
|
|
"user-authored IdentityContext must not use the reserved SYSTEM_DERIVED replica"
|
|
);
|
|
IdentityContext {
|
|
replica_id,
|
|
next_counter: 0,
|
|
}
|
|
}
|
|
|
|
/// Checked constructor: returns `None` for the reserved
|
|
/// [`ReplicaId::SYSTEM_DERIVED`] namespace (Chapter 5 §"System-Derived
|
|
/// Identifier Namespace").
|
|
#[inline]
|
|
pub fn try_new(replica_id: ReplicaId) -> Option<Self> {
|
|
if replica_id.is_system_derived() {
|
|
None
|
|
} else {
|
|
Some(IdentityContext {
|
|
replica_id,
|
|
next_counter: 0,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Starts a fresh identity context with a freshly generated replica id
|
|
/// (QUICKSTART decision 1). [`ReplicaId::generate`] never yields the
|
|
/// reserved namespace.
|
|
pub fn fresh() -> Self {
|
|
Self::new(ReplicaId::generate())
|
|
}
|
|
|
|
/// The next counter value, faulting loudly on the impossible `u64` wrap so
|
|
/// a counter is never reused (Chapter 5 §"Identifier Generation"). Uses
|
|
/// `checked_add` so the guarantee holds in *every* build profile, not only
|
|
/// where `overflow-checks` is on.
|
|
#[inline]
|
|
fn take_counter(&mut self) -> u64 {
|
|
let counter = self.next_counter;
|
|
self.next_counter = self
|
|
.next_counter
|
|
.checked_add(1)
|
|
.expect("identifier counter overflowed u64; counters must never be reused");
|
|
counter
|
|
}
|
|
|
|
/// Mints the next identifier of any [`GraphId`] kind from this replica,
|
|
/// advancing the monotonic counter (never reused).
|
|
#[inline]
|
|
pub fn mint<T: GraphId>(&mut self) -> T {
|
|
let counter = self.take_counter();
|
|
T::from_parts(self.replica_id, counter)
|
|
}
|
|
|
|
/// Mints the next [`OperationId`] from this replica.
|
|
#[inline]
|
|
pub fn mint_operation(&mut self) -> OperationId {
|
|
let counter = self.take_counter();
|
|
OperationId::new(self.replica_id, counter)
|
|
}
|
|
}
|
|
|
|
/// Derives a system identifier of kind `T` in the [`ReplicaId::SYSTEM_DERIVED`]
|
|
/// namespace: its counter is `trunc64(BLAKE3(domain || canonical_inputs))`
|
|
/// (Chapter 5 §"System-Derived Identifiers"). The `domain` is a
|
|
/// [`SystemDomainTag`], so only a `MUSCS…` tag can seed a system identifier —
|
|
/// the precondition is enforced by the type, in every build profile.
|
|
///
|
|
/// Two replicas reducing identical canonical inputs derive byte-identical
|
|
/// system identifiers, which is the determinism the CRDT layer relies on.
|
|
#[inline]
|
|
pub fn derive_system_id<T: GraphId>(domain: SystemDomainTag, canonical_inputs: &[u8]) -> T {
|
|
let counter = derive_system_counter(domain, canonical_inputs);
|
|
T::from_parts(ReplicaId::SYSTEM_DERIVED, counter)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use epiphany_determinism::sorted_canonical;
|
|
|
|
#[test]
|
|
fn canonical_bytes_are_replica_then_counter_big_endian() {
|
|
let id = EventId::new(ReplicaId(0x0102_0304_0506_0708), 0x1112_1314_1516_1718);
|
|
assert_eq!(
|
|
id.canonical_bytes(),
|
|
[
|
|
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // replica, BE
|
|
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // counter, BE
|
|
]
|
|
);
|
|
assert_eq!(id.replica(), ReplicaId(0x0102_0304_0506_0708));
|
|
assert_eq!(id.counter(), 0x1112_1314_1516_1718);
|
|
}
|
|
|
|
#[test]
|
|
fn numeric_ord_matches_canonical_byte_order() {
|
|
// Replica dominates the counter, exactly as big-endian bytes sort.
|
|
let lo = VoiceId::new(ReplicaId(1), u64::MAX);
|
|
let hi = VoiceId::new(ReplicaId(2), 0);
|
|
assert!(lo < hi);
|
|
assert!(lo.canonical_bytes() < hi.canonical_bytes());
|
|
|
|
let mut ids = vec![
|
|
VoiceId::new(ReplicaId(2), 0),
|
|
VoiceId::new(ReplicaId(1), 5),
|
|
VoiceId::new(ReplicaId(1), 0),
|
|
];
|
|
ids.sort();
|
|
let by_bytes = sorted_canonical(ids.clone());
|
|
assert_eq!(ids, by_bytes);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_kind_ids_are_distinct_types_same_bytes() {
|
|
// Same replica/counter, different kinds: identical canonical *id*
|
|
// bytes, but a TypedObjectId tags them apart.
|
|
let r = ReplicaId(7);
|
|
let e = EventId::new(r, 3);
|
|
let v = VoiceId::new(r, 3);
|
|
assert_eq!(e.canonical_bytes(), v.canonical_bytes());
|
|
let te = TypedObjectId::Event(e);
|
|
let tv = TypedObjectId::Voice(v);
|
|
assert_ne!(te, tv);
|
|
assert_ne!(te.canonical_bytes(), tv.canonical_bytes());
|
|
}
|
|
|
|
#[test]
|
|
fn replica_generation_never_yields_reserved() {
|
|
assert!(ReplicaId::from_entropy([0xff; 8]).is_none());
|
|
assert_eq!(ReplicaId::from_entropy([0; 8]), Some(ReplicaId(0)));
|
|
for _ in 0..10_000 {
|
|
assert!(!ReplicaId::generate().is_system_derived());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mint_is_monotonic_and_per_replica() {
|
|
let mut ctx = IdentityContext::new(ReplicaId(42));
|
|
let a: EventId = ctx.mint();
|
|
let b: VoiceId = ctx.mint();
|
|
let c: EventId = ctx.mint();
|
|
// One shared counter across kinds; strictly increasing; replica fixed.
|
|
assert_eq!(a.counter(), 0);
|
|
assert_eq!(b.counter(), 1);
|
|
assert_eq!(c.counter(), 2);
|
|
assert_eq!(a.replica(), ReplicaId(42));
|
|
assert_eq!(b.replica(), ReplicaId(42));
|
|
}
|
|
|
|
#[test]
|
|
fn system_derived_ids_are_deterministic_and_namespaced() {
|
|
let a: VoiceId = derive_system_id(SystemDomainTag::VOICE, b"abc");
|
|
let b: VoiceId = derive_system_id(SystemDomainTag::VOICE, b"abc");
|
|
let c: VoiceId = derive_system_id(SystemDomainTag::VOICE, b"abd");
|
|
assert_eq!(a, b, "identical inputs derive identical ids");
|
|
assert_ne!(a, c, "different inputs derive different ids");
|
|
assert_eq!(a.replica(), ReplicaId::SYSTEM_DERIVED);
|
|
assert_eq!(
|
|
a.counter(),
|
|
derive_system_counter(SystemDomainTag::VOICE, b"abc")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn typed_object_id_round_trips_every_variant() {
|
|
let r = ReplicaId(9);
|
|
let cases = [
|
|
TypedObjectId::Event(EventId::new(r, 1)),
|
|
TypedObjectId::Pitch(PitchId::new(r, 2)),
|
|
TypedObjectId::Voice(VoiceId::new(r, 3)),
|
|
TypedObjectId::TimeSignature(TimeSignatureId::new(r, 20)),
|
|
TypedObjectId::AnalysisLayer(AnalysisLayerId::new(r, 21)),
|
|
TypedObjectId::Tuplet(TupletId::new(r, 22)),
|
|
TypedObjectId::RepeatStructure(RepeatStructureId::new(r, 23)),
|
|
TypedObjectId::LyricLine(LyricLineId::new(r, 24)),
|
|
TypedObjectId::ChordSymbol(ChordSymbolId::new(r, 25)),
|
|
TypedObjectId::View(ViewId::new(r, 26)),
|
|
TypedObjectId::Registered(ObjectKindRegistryId::new(r, 99), 0xdead_beef),
|
|
];
|
|
for c in cases {
|
|
let bytes = c.canonical_bytes();
|
|
assert_eq!(TypedObjectId::decode_canonical(&bytes).unwrap(), c);
|
|
// Re-encode is byte-identical.
|
|
assert_eq!(c.to_canonical_bytes(), bytes);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn typed_object_id_byte_form_is_locked() {
|
|
// Golden: locks the 16-bit big-endian discriminant table and the payload
|
|
// layout. RATIFIED by Pass 11 (item 1.1, P11-1): this is now the spec's
|
|
// golden, normative in core_spec §"Identifiers",
|
|
// Requirement `req:graph:typed-object-id-discriminants` (discriminants
|
|
// 0..=27, Registered=27 as disc(2)||reg(16)||raw_be(16)). A reorder or
|
|
// reassignment breaks this deliberately — these bytes drive
|
|
// ordering/hashing/equality for every object.
|
|
let r = ReplicaId(9);
|
|
// Event = discriminant 0, then the 16-byte big-endian id payload.
|
|
let event = TypedObjectId::Event(EventId::new(r, 1)).canonical_bytes();
|
|
const GOLDEN_EVENT: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 1];
|
|
assert_eq!(event, GOLDEN_EVENT);
|
|
// Registered = discriminant 27, then registry id (16) + raw extension (16).
|
|
let reg = TypedObjectId::Registered(ObjectKindRegistryId::new(r, 99), 0xdead_beef)
|
|
.canonical_bytes();
|
|
const GOLDEN_REGISTERED: &[u8] = &[
|
|
0, 27, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 222, 173, 190, 239,
|
|
];
|
|
assert_eq!(reg, GOLDEN_REGISTERED);
|
|
}
|
|
|
|
#[test]
|
|
fn operation_id_orders_by_replica_then_counter() {
|
|
let a = OperationId::new(ReplicaId(1), 9);
|
|
let b = OperationId::new(ReplicaId(2), 0);
|
|
assert!(a < b);
|
|
assert_eq!(
|
|
OperationId::decode_canonical(&a.to_canonical_bytes()).unwrap(),
|
|
a
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn try_new_rejects_the_reserved_replica() {
|
|
assert!(IdentityContext::try_new(ReplicaId::SYSTEM_DERIVED).is_none());
|
|
assert!(IdentityContext::try_new(ReplicaId(1)).is_some());
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "counter overflowed")]
|
|
fn counter_overflow_faults_loudly_in_every_profile() {
|
|
// `checked_add` guarantees this regardless of the build's overflow-checks
|
|
// setting, so a counter is never silently reused.
|
|
let mut ctx = IdentityContext::new(ReplicaId(1));
|
|
ctx.next_counter = u64::MAX;
|
|
let _: EventId = ctx.mint();
|
|
}
|
|
}
|