//! 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 { 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) { out.extend_from_slice(&self.to_be_bytes()); } } impl CanonicalDecode for ReplicaId { #[inline] fn decode_canonical(bytes: &[u8]) -> Result { 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) { out.extend_from_slice(&self.canonical_bytes()); } } impl CanonicalDecode for $name { #[inline] fn decode_canonical(bytes: &[u8]) -> Result { 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) { out.extend_from_slice(&self.canonical_bytes()); } } impl CanonicalDecode for OperationId { #[inline] fn decode_canonical(bytes: &[u8]) -> Result { 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 { 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 { 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) { out.extend_from_slice(&self.canonical_bytes()); } } impl CanonicalByteOrder for TypedObjectId {} impl CanonicalDecode for TypedObjectId { fn decode_canonical(bytes: &[u8]) -> Result { 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 { 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 { 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(&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(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(); } }