//! Conflict records and the content-derived [`ConflictId`] (Chapter 6 //! §"Conflict Records"). //! //! When an operation cannot apply cleanly, the reduction produces a //! `Conflicted` effect referencing a [`ConflictRecord`] in the score's conflict //! registry. Conflict records are first-class, *canonical* graph objects: //! stable, addressable, visible to users, and resolvable by a later //! `ResolveConflict` operation. //! //! Because conflict records are produced during deterministic reduction rather //! than authored by replicas, their identifiers MUST be **content-derived**: //! two replicas reducing the same operation set produce the same conflicts with //! the same ids ([`derive_conflict_id`]). An ordinal or local counter in the //! preimage is forbidden — conflicts that share kind, causing operations, and //! affected objects are by definition the same conflict (Chapter 6 §6.4.3). //! //! Integrity *anomalies* ([`crate::IntegrityAnomaly`]) are deliberately a //! separate type, not a `ConflictKind`: a conflict is an ordinary //! canonical-state fact (two valid edits collide); an anomaly is a structural //! failure that takes the document out of ordinary canonical operation //! (Chapter 6 / Chapter 5 §"System-Derived Counter Collisions"; Pass 10). use epiphany_core::{OperationId, RegionId, TransactionId, TypedObjectId}; use epiphany_determinism::{ sorted_canonical, CanonicalByteOrder, CanonicalEncode, DomainTag, Preimage, }; use crate::encode::{push_canon, push_seq, push_str, push_tag}; use crate::support::{ConflictKindRegistryId, ResolutionRegistryId}; /// A content-derived conflict identifier: `trunc128(BLAKE3("MUSCCONF" || …))` /// (Chapter 6 §6.4.3). 128-bit, canonical 16-byte big-endian form, ordered /// byte-lexicographically — the order the conflict registry iterates in /// (Appendix D §"Ordered Iteration": "Conflict records: ascending by /// ConflictId"). #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] pub struct ConflictId(pub u128); impl ConflictId { /// The canonical 16-byte big-endian form. #[inline] pub const fn canonical_bytes(self) -> [u8; 16] { self.0.to_be_bytes() } } impl CanonicalEncode for ConflictId { #[inline] fn encode_canonical(&self, out: &mut Vec) { out.extend_from_slice(&self.canonical_bytes()); } } impl CanonicalByteOrder for ConflictId {} /// A path to a specific field, naming what two concurrent operations collided /// on in a [`ConflictKind::StructuralFieldCollision`]. Canonical text /// (Appendix D §"Text and Unicode"). #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] pub struct FieldPath(pub String); impl CanonicalEncode for FieldPath { #[inline] fn encode_canonical(&self, out: &mut Vec) { push_str(out, &self.0); } } /// The kind of conflict, governing the resolution options (Chapter 6 §6.4.1). /// /// The canonical byte form (via [`CanonicalEncode`]) includes the kind /// discriminant and **all** payload, so distinct conflicts have distinct /// preimages by construction (Chapter 6 §6.4.3 requirement). #[derive(Clone, PartialEq, Eq, Debug)] pub enum ConflictKind { /// Two concurrent operations wrote the same non-LWW field. The winner's /// effect is materialized; the loser's is preserved here for inspection. StructuralFieldCollision { winner: OperationId, loser: OperationId, field: FieldPath, }, /// A transaction's members were partially applicable; the whole /// transaction was rejected. TransactionConflict { transaction: TransactionId, failed_members: Vec, }, /// An operation targeted a tombstoned object the kind's rules could not /// repair. TombstonedTarget { target: TypedObjectId, operation: OperationId, }, /// Re-anchoring failed: no deterministic target could be found. ReanchorFailure { original_referent: TypedObjectId, referencing_object: TypedObjectId, }, /// A region time-model migration produced contained events whose /// coordinate kinds are incompatible with the new model. TimeModelMigrationFailure { region: RegionId, incompatible_events: Vec, }, /// A registered extension operation's reduction failed; opaque to the core. ExtensionConflict { kind_id: ConflictKindRegistryId, details: Vec, }, } impl ConflictKind { /// The discriminant byte; part of the canonical preimage. fn discriminant(&self) -> u8 { match self { ConflictKind::StructuralFieldCollision { .. } => 0, ConflictKind::TransactionConflict { .. } => 1, ConflictKind::TombstonedTarget { .. } => 2, ConflictKind::ReanchorFailure { .. } => 3, ConflictKind::TimeModelMigrationFailure { .. } => 4, ConflictKind::ExtensionConflict { .. } => 5, } } /// The canonical byte form of the kind, as required by /// [`derive_conflict_id`] (Chapter 6 §6.4.3). pub fn canonical_bytes(&self) -> Vec { self.to_canonical_bytes() } } impl CanonicalEncode for ConflictKind { fn encode_canonical(&self, out: &mut Vec) { push_tag(out, self.discriminant()); match self { ConflictKind::StructuralFieldCollision { winner, loser, field, } => { push_canon(out, winner); push_canon(out, loser); push_canon(out, field); } ConflictKind::TransactionConflict { transaction, failed_members, } => { push_canon(out, transaction); // Failed members in canonical order so the preimage is stable. push_seq(out, &sorted_canonical(failed_members.clone())); } ConflictKind::TombstonedTarget { target, operation } => { push_canon(out, target); push_canon(out, operation); } ConflictKind::ReanchorFailure { original_referent, referencing_object, } => { push_canon(out, original_referent); push_canon(out, referencing_object); } ConflictKind::TimeModelMigrationFailure { region, incompatible_events, } => { push_canon(out, region); push_seq(out, &sorted_canonical(incompatible_events.clone())); } ConflictKind::ExtensionConflict { kind_id, details } => { push_canon(out, kind_id); crate::encode::push_lp_bytes(out, details); } } } } /// How a conflict was resolved (Chapter 6 §6.4.1). #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum ResolutionAction { /// Accept the losing operation's effect (replacing the winner). AcceptLoser, /// Reapply the winner explicitly (clears the conflict, no semantic change). KeepWinner, /// A user-authored replacement that supersedes both. Override { override_operation: OperationId }, /// Re-anchor to a user-chosen target. Reanchor { new_target: TypedObjectId }, /// Dismiss the conflict without changing the materialized graph: the user /// acknowledges it and accepts the current (winner) state. Selects the /// `Dismissed` resolution state (Pass 11, item 2.5; Chapter 6). Dismiss, /// Custom resolution for a registered conflict kind. Registered(ResolutionRegistryId), } impl ResolutionAction { fn discriminant(&self) -> u8 { match self { ResolutionAction::AcceptLoser => 0, ResolutionAction::KeepWinner => 1, ResolutionAction::Override { .. } => 2, ResolutionAction::Reanchor { .. } => 3, ResolutionAction::Dismiss => 4, ResolutionAction::Registered(_) => 5, } } } impl CanonicalEncode for ResolutionAction { fn encode_canonical(&self, out: &mut Vec) { push_tag(out, self.discriminant()); match self { ResolutionAction::AcceptLoser | ResolutionAction::KeepWinner | ResolutionAction::Dismiss => {} ResolutionAction::Override { override_operation } => { push_canon(out, override_operation) } ResolutionAction::Reanchor { new_target } => push_canon(out, new_target), ResolutionAction::Registered(id) => push_canon(out, id), } } } /// The current resolution state of a conflict (Chapter 6 §6.4.1). Conflicts /// begin `Unresolved`; a later `ResolveConflict` operation transitions them. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum ConflictResolutionState { Unresolved, Resolved { by: OperationId, action: ResolutionAction, }, Dismissed { by: OperationId, }, } impl ConflictResolutionState { fn discriminant(&self) -> u8 { match self { ConflictResolutionState::Unresolved => 0, ConflictResolutionState::Resolved { .. } => 1, ConflictResolutionState::Dismissed { .. } => 2, } } } impl CanonicalEncode for ConflictResolutionState { fn encode_canonical(&self, out: &mut Vec) { push_tag(out, self.discriminant()); match self { ConflictResolutionState::Unresolved => {} ConflictResolutionState::Resolved { by, action } => { push_canon(out, by); push_canon(out, action); } ConflictResolutionState::Dismissed { by } => push_canon(out, by), } } } /// A first-class conflict record (Chapter 6 §6.4.1). #[derive(Clone, PartialEq, Eq, Debug)] pub struct ConflictRecord { /// Content-derived identifier ([`derive_conflict_id`]). pub id: ConflictId, /// The operations that participated. At least two for a true conflict; one /// for an operation that failed precondition checking under reduction. pub caused_by: Vec, /// The kind of conflict. pub kind: ConflictKind, /// Objects affected, for diagnostic and UI navigation. pub affected_objects: Vec, /// Current resolution state. pub resolution_state: ConflictResolutionState, } impl ConflictRecord { /// Builds an unresolved conflict record, deriving its content id from the /// kind, causing operations, and affected objects (Chapter 6 §6.4.3). The /// `caused_by` and `affected_objects` vectors are stored in canonical order /// so the record's own canonical bytes are stable regardless of how the /// reduction assembled them. pub fn new( kind: ConflictKind, caused_by: Vec, affected_objects: Vec, ) -> Self { let id = derive_conflict_id(&kind, &caused_by, &affected_objects); ConflictRecord { id, caused_by: sorted_canonical(caused_by), kind, affected_objects: sorted_canonical(affected_objects), resolution_state: ConflictResolutionState::Unresolved, } } } impl CanonicalEncode for ConflictRecord { fn encode_canonical(&self, out: &mut Vec) { push_canon(out, &self.id); push_seq(out, &self.caused_by); push_canon(out, &self.kind); push_seq(out, &self.affected_objects); push_canon(out, &self.resolution_state); } } /// Derives a [`ConflictId`] from a conflict's content (Chapter 6 §6.4.3). /// /// The preimage is `"MUSCCONF" || kind.canonical_bytes() || sorted_ops || /// sorted_objs`, where causing operations and affected objects are each sorted /// lexicographically by canonical bytes. No ordinal or local counter enters the /// preimage: conflicts sharing kind, causing operations, and affected objects /// are by definition the same conflict. pub fn derive_conflict_id( kind: &ConflictKind, causing_operations: &[OperationId], affected_objects: &[TypedObjectId], ) -> ConflictId { let mut p = Preimage::new(DomainTag::CONFLICT); p.push_bytes(&kind.canonical_bytes()); for op in sorted_canonical(causing_operations.to_vec()) { p.push_bytes(&op.canonical_bytes()); } for obj in sorted_canonical(affected_objects.to_vec()) { p.push_bytes(&obj.canonical_bytes()); } ConflictId(p.finish_trunc128()) } /// The score's conflict registry (Chapter 6 §6.4.2): part of canonical /// materialized state. Records are kept in the normative order — ascending by /// [`ConflictId`] — so the registry's canonical bytes never depend on the order /// in which the reduction discovered the conflicts. #[derive(Clone, PartialEq, Eq, Debug, Default)] pub struct ConflictRegistry { records: Vec, } impl ConflictRegistry { /// An empty registry. #[inline] pub fn new() -> Self { ConflictRegistry::default() } /// Inserts a record, keeping the registry ordered by `ConflictId`. If a /// record with the same content id already exists (the same conflict /// re-derived), the insert is idempotent — content-derived identity means /// re-discovering a conflict is not a second conflict. pub fn insert(&mut self, record: ConflictRecord) { match self.records.binary_search_by(|r| r.id.cmp(&record.id)) { Ok(_) => {} // already present; content-derived identity ⇒ idempotent Err(pos) => self.records.insert(pos, record), } } /// The records, in canonical (ascending-`ConflictId`) order. #[inline] pub fn records(&self) -> &[ConflictRecord] { &self.records } /// A mutable handle to the record with `id`, if present. pub fn get_mut(&mut self, id: ConflictId) -> Option<&mut ConflictRecord> { self.records .binary_search_by(|r| r.id.cmp(&id)) .ok() .map(|pos| &mut self.records[pos]) } /// Whether the registry holds any records. #[inline] pub fn is_empty(&self) -> bool { self.records.is_empty() } } impl CanonicalEncode for ConflictRegistry { fn encode_canonical(&self, out: &mut Vec) { // Already in ConflictId order by construction. push_seq(out, &self.records); } } #[cfg(test)] mod tests { use super::*; use epiphany_core::{EventId, ReplicaId}; fn op(r: u64, c: u64) -> OperationId { OperationId::new(ReplicaId(r), c) } fn obj(n: u128) -> TypedObjectId { TypedObjectId::Event(EventId::from_raw(n)) } #[test] fn resolution_action_discriminants_are_golden() { // RATIFIED by Pass 11 (item 2.5; core_spec // `req:semops:resolution-action-discriminants`): ResolutionAction is // canonically encoded into operation content, so its discriminants are // normative. Dismiss = 4 was inserted ahead of Registered (now 5); lock // the literal values so any future reorder breaks deliberately rather // than silently shifting the wire form. assert_eq!(ResolutionAction::AcceptLoser.discriminant(), 0); assert_eq!(ResolutionAction::KeepWinner.discriminant(), 1); assert_eq!( ResolutionAction::Override { override_operation: op(1, 1) } .discriminant(), 2 ); assert_eq!( ResolutionAction::Reanchor { new_target: obj(7) }.discriminant(), 3 ); assert_eq!(ResolutionAction::Dismiss.discriminant(), 4); assert_eq!( ResolutionAction::Registered(ResolutionRegistryId(0)).discriminant(), 5 ); } #[test] fn conflict_id_is_order_independent_in_its_inputs() { let kind = ConflictKind::TombstonedTarget { target: obj(7), operation: op(1, 1), }; let a = derive_conflict_id(&kind, &[op(1, 1), op(2, 2)], &[obj(7), obj(3)]); let b = derive_conflict_id(&kind, &[op(2, 2), op(1, 1)], &[obj(3), obj(7)]); assert_eq!(a, b, "causing-op / affected-object order must not matter"); } #[test] fn distinct_kinds_yield_distinct_ids() { let k1 = ConflictKind::TombstonedTarget { target: obj(7), operation: op(1, 1), }; let k2 = ConflictKind::ReanchorFailure { original_referent: obj(7), referencing_object: obj(1), }; assert_ne!( derive_conflict_id(&k1, &[op(1, 1)], &[obj(7)]), derive_conflict_id(&k2, &[op(1, 1)], &[obj(7)]), ); } #[test] fn registry_is_sorted_and_idempotent_on_same_content() { let mut reg = ConflictRegistry::new(); let mk = |t: u128| { ConflictRecord::new( ConflictKind::TombstonedTarget { target: obj(t), operation: op(1, 1), }, vec![op(1, 1)], vec![obj(t)], ) }; let a = mk(5); reg.insert(mk(9)); reg.insert(a.clone()); reg.insert(mk(1)); reg.insert(a.clone()); // re-discovering the same conflict let ids: Vec<_> = reg.records().iter().map(|r| r.id).collect(); let mut sorted = ids.clone(); sorted.sort(); assert_eq!(ids, sorted, "registry must be ConflictId-ordered"); // a inserted twice but present once. assert_eq!(reg.records().iter().filter(|r| r.id == a.id).count(), 1); } }