epiphany/crates/epiphany-core/src/ids.rs

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(&reg.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();
}
}