//! The canonical reduction (Chapter 6 §"The Canonical Reduction").
//!
//! The materialized score state is the deterministic reduction of the operation
//! set. This module is the determinism heart of the architecture:
//!
//! * [`canonical_reduction_order`] is the **single function** that orders
//!   operations (Chapter 6 §6.3.3). The order is causal-first, then by the HLC
//!   tuple `(physical, logical, replica, counter)`. A deterministic topological
//!   pass enforces causal precedence even for an accepted remote envelope whose
//!   HLC contradicts its causal context; HLC orders only ready operations.
//! * [`reduce_operation_set`] walks that order and produces a
//!   [`MaterializedState`] whose [`canonical_bytes`](MaterializedState::canonical_bytes)
//!   are **byte-identical across any permutation of the input** (Appendix D
//!   §"Canonical score determinism"; v0 acceptance criteria 1 and 5).
//!
//! The reduction handles, deterministically: equivocated-slot and
//! anomalous-segment exclusion, the missing-causal-predecessor rule (dependents
//! held pending), atomic transactions with the descriptor-precedence rule,
//! tombstones with re-anchoring, conflict generation with content-derived ids,
//! LWW-advisory fields, and forward (compensating) undo.
//!
//! ## Prototype scope
//!
//! The per-kind reduction implements the representative operations of §6.10
//! against the canonical bookkeeping Chapter 6 owns. [`OperationSet::reduce_onto`]
//! additionally seeds that state from and materializes it into Agent B's
//! [`Score`]. Rich values absent from the provisional operation payloads remain
//! deferred to the Operation Catalog (§6.11); see `DECISIONS.md` for the exact
//! boundary.

use std::collections::{BTreeMap, BTreeSet};

use epiphany_core::{
    derive_promoted_voice_id, AnchorOffset, CanonicalValue, Event, EventDuration, EventId,
    EventPosition, MetricGrid, MusicalDuration, MusicalPosition, OperationId, Pitch, PitchId,
    PitchSpelling, RegionEdge, RegionId, RegionTimeModel, Score, SpellingAttachment,
    SpellingDirective, SpellingScope, SpellingSource, StaffInstance, StaffInstanceId, TimeAnchor,
    TransactionId, TypedObjectId, Voice, VoiceId, VoiceOrigin,
};
use epiphany_determinism::CanonicalEncode;

use crate::anomaly::{detect_replica_anomalies, IntegrityAnomaly, IntegrityAnomalyKind};
use crate::conflict::{
    ConflictKind, ConflictRecord, ConflictRegistry, FieldPath, ResolutionAction,
};
use crate::effect::{
    NoOpReason, OperationEffect, PreconditionFailureReason, ReanchorReason, RepairKind,
    RepairRecord, TupletCompensationKind,
};
use crate::encode::{push_canon, push_len, push_lp_bytes, push_u8_bool};
use crate::envelope::OperationEnvelope;
use crate::opset::OperationSet;
use crate::payload::{
    CreateCrossCuttingOp, CreateRegionOp, CreateStaffInstanceOp, CreateVoiceOp, CrossCuttingValue,
    DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp,
    DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp,
    ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload,
    RespellPitchOp, SetMetadataOp, SetMetricGridOp, SetUserPageBreakOp, TransposeOp,
    TupletCompensation,
};
use crate::undo::{UndoPolicy, UndoTransactionPayload};

/// Orders operation envelopes into the canonical reduction order (Chapter 6
/// §6.3.3): causal-first, then by the HLC tuple `(physical, logical, replica,
/// counter)`. Returns the envelopes in that order.
///
/// This is the single ordering function the determinism property tests against.
/// It performs deterministic Kahn topological ordering over causal-context
/// coverage, choosing the smallest HLC reduction tuple among ready operations.
/// A malformed causal cycle has no valid topological order; the smallest HLC
/// tuple deterministically breaks the cycle so every replica still converges.
pub fn canonical_reduction_order<'a>(
    envelopes: &[&'a OperationEnvelope],
) -> Vec<&'a OperationEnvelope> {
    let len = envelopes.len();
    let mut indegree = vec![0usize; len];
    let mut successors = vec![Vec::<usize>::new(); len];
    for predecessor in 0..len {
        for successor in 0..len {
            if predecessor != successor
                && envelopes[successor]
                    .causal_context
                    .covers(envelopes[predecessor].id)
            {
                successors[predecessor].push(successor);
                indegree[successor] += 1;
            }
        }
    }

    let mut emitted = vec![false; len];
    let mut ordered = Vec::with_capacity(len);
    for _ in 0..len {
        let ready = (0..len)
            .filter(|&index| !emitted[index] && indegree[index] == 0)
            .min_by_key(|&index| envelopes[index].stamp.reduction_tuple());
        let next = ready.unwrap_or_else(|| {
            // A cycle is malformed, but selecting by the canonical tie-breaker
            // keeps reduction deterministic and unlocks its outgoing edges.
            (0..len)
                .filter(|&index| !emitted[index])
                .min_by_key(|&index| envelopes[index].stamp.reduction_tuple())
                .expect("an un-emitted operation remains")
        });
        emitted[next] = true;
        ordered.push(envelopes[next]);
        for &successor in &successors[next] {
            indegree[successor] = indegree[successor].saturating_sub(1);
        }
    }
    ordered
}

/// The state of an object's identifier in the materialized graph (Chapter 6
/// §"Object Existence and Tombstones"). `Unknown` is not represented: an
/// identifier the reduction has never seen simply has no entry.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ObjectState {
    /// Resolves to a current object.
    Live,
    /// Resolves to a deletion record carrying the deleting and minting
    /// operations. Tombstones are never resurrected except by an explicit
    /// inverse-of-delete (Chapter 6 §6.3.4).
    Tombstoned {
        deleted_by: OperationId,
        minted_by: OperationId,
    },
}

impl ObjectState {
    fn discriminant(&self) -> u8 {
        match self {
            ObjectState::Live => 0,
            ObjectState::Tombstoned { .. } => 1,
        }
    }
}

impl CanonicalEncode for ObjectState {
    fn encode_canonical(&self, out: &mut Vec<u8>) {
        out.push(self.discriminant());
        if let ObjectState::Tombstoned {
            deleted_by,
            minted_by,
        } = self
        {
            push_canon(out, deleted_by);
            push_canon(out, minted_by);
        }
    }
}

/// Why an operation is held pending rather than reduced (Chapter 6 §6.5, §6.6).
/// A pending operation is retained in the operation set but produces no
/// canonical effect until its blocking cause is resolved.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum PendingReason {
    /// A causal predecessor is absent from the operation set entirely.
    MissingCausalPredecessor { missing: OperationId },
    /// A causal predecessor's slot is equivocated.
    DependsOnEquivocated { on: OperationId },
    /// A causal predecessor was excluded by an anomalous-replica segment.
    DependsOnExcluded { on: OperationId },
    /// A causal predecessor is itself held pending.
    DependsOnPending { on: OperationId },
}

impl PendingReason {
    fn discriminant(&self) -> u8 {
        match self {
            PendingReason::MissingCausalPredecessor { .. } => 0,
            PendingReason::DependsOnEquivocated { .. } => 1,
            PendingReason::DependsOnExcluded { .. } => 2,
            PendingReason::DependsOnPending { .. } => 3,
        }
    }
    fn blocker(&self) -> OperationId {
        match self {
            PendingReason::MissingCausalPredecessor { missing } => *missing,
            PendingReason::DependsOnEquivocated { on }
            | PendingReason::DependsOnExcluded { on }
            | PendingReason::DependsOnPending { on } => *on,
        }
    }
}

impl CanonicalEncode for PendingReason {
    fn encode_canonical(&self, out: &mut Vec<u8>) {
        out.push(self.discriminant());
        push_canon(out, &self.blocker());
    }
}

/// The canonical materialized state produced by the reduction (Chapter 6 §6.3).
/// Every field is in a normative total order, so [`MaterializedState::canonical_bytes`]
/// is byte-identical across any permutation of the input operation set.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct MaterializedState {
    /// The effect log: one [`OperationEffect`] per reduced operation, in
    /// canonical reduction order (Chapter 6 §6.3.2). Part of canonical state.
    pub effects: Vec<(OperationId, OperationEffect)>,
    /// The conflict registry, ordered by `ConflictId`.
    pub conflicts: ConflictRegistry,
    /// The integrity-anomaly register, ordered by `IntegrityAnomalyId`.
    pub anomalies: Vec<IntegrityAnomaly>,
    /// Object existence (live/tombstoned), keyed by `TypedObjectId`.
    pub objects: BTreeMap<TypedObjectId, ObjectState>,
    /// Current resolved spelling per pitch (the `RespellPitch` field).
    pub spellings: BTreeMap<PitchId, PitchSpelling>,
    /// User system-break preferences (LWW advisory), keyed by region+anchor.
    pub breaks: BTreeMap<(RegionId, MusicalPosition), bool>,
    /// User page-break preferences (LWW advisory), keyed by region+anchor (the
    /// page-break sibling of [`MaterializedState::breaks`], M2d).
    pub page_breaks: BTreeMap<(RegionId, MusicalPosition), bool>,
    /// Operations held pending, ordered by `OperationId`.
    pub pending: Vec<(OperationId, PendingReason)>,
}

/// The result of reducing an operation set onto a canonical base score.
///
/// `state` remains the byte-canonical Chapter 6 reduction product. `score` is
/// the corresponding Agent B graph materialization used by editing, invariant
/// checking, indexing, and layout; it is derived state, never the source of
/// truth.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct GraphMaterialization {
    pub state: MaterializedState,
    pub score: Score,
}

impl MaterializedState {
    /// The canonical byte serialization of the materialized state. Two
    /// reductions of the same operation set — in any order — produce identical
    /// bytes (the determinism property; v0 acceptance criteria 1 and 5).
    pub fn canonical_bytes(&self) -> Vec<u8> {
        let mut out = Vec::new();
        // Effects, in reduction order.
        push_len(&mut out, self.effects.len());
        for (id, eff) in &self.effects {
            push_canon(&mut out, id);
            let mut scratch = Vec::new();
            eff.encode_canonical(&mut scratch);
            crate::encode::push_lp_bytes(&mut out, &scratch);
        }
        // Conflict registry (ConflictId order).
        self.conflicts.encode_canonical(&mut out);
        // Anomaly register (IntegrityAnomalyId order).
        push_len(&mut out, self.anomalies.len());
        for a in &self.anomalies {
            let mut scratch = Vec::new();
            a.encode_canonical(&mut scratch);
            crate::encode::push_lp_bytes(&mut out, &scratch);
        }
        // Objects (TypedObjectId order).
        push_len(&mut out, self.objects.len());
        for (id, state) in &self.objects {
            push_canon(&mut out, id);
            state.encode_canonical(&mut out);
        }
        // Spellings (PitchId order). The value is the full PitchSpelling (v1),
        // encoded behind a u32 length prefix via its canonical value bytes.
        push_len(&mut out, self.spellings.len());
        for (pitch, spelling) in &self.spellings {
            push_canon(&mut out, pitch);
            push_lp_bytes(&mut out, &spelling.canonical_bytes());
        }
        // Breaks (region+anchor order).
        push_len(&mut out, self.breaks.len());
        for ((region, anchor), present) in &self.breaks {
            push_canon(&mut out, region);
            push_canon(&mut out, anchor);
            push_u8_bool(&mut out, *present);
        }
        // Page breaks (region+anchor order) — sibling of breaks (M2d).
        push_len(&mut out, self.page_breaks.len());
        for ((region, anchor), present) in &self.page_breaks {
            push_canon(&mut out, region);
            push_canon(&mut out, anchor);
            push_u8_bool(&mut out, *present);
        }
        // Pending (OperationId order).
        push_len(&mut out, self.pending.len());
        for (id, reason) in &self.pending {
            push_canon(&mut out, id);
            reason.encode_canonical(&mut out);
        }
        out
    }

    /// Decodes the exact inverse of [`MaterializedState::canonical_bytes`].
    ///
    /// The decoder validates all nested tags, lengths, primitive values,
    /// canonical ordering, and trailing-byte discipline. Structurally valid but
    /// non-canonical bytes are rejected rather than silently normalized.
    pub fn decode_canonical(bytes: &[u8]) -> Result<Self, crate::MaterializedDecodeError> {
        crate::decode::decode_materialized_state(bytes)
    }

    /// Whether the materialized state is free of conflicts and anomalies and
    /// has no pending operations — a clean reduction.
    pub fn is_clean(&self) -> bool {
        self.conflicts.is_empty() && self.anomalies.is_empty() && self.pending.is_empty()
    }
}

/// Reduces an [`OperationSet`] to its canonical [`MaterializedState`].
pub fn reduce_operation_set(op_set: &OperationSet) -> MaterializedState {
    Reducer::new(op_set).run().0
}

/// Reduces an [`OperationSet`] onto a canonical base [`Score`].
pub fn reduce_operation_set_onto(op_set: &OperationSet, base: &Score) -> GraphMaterialization {
    let (state, score) = Reducer::new_onto(op_set, base).run();
    GraphMaterialization {
        state,
        score: score.expect("graph-aware reduction always retains its base score"),
    }
}

/// The working state of one reduction pass.
struct Reducer<'a> {
    op_set: &'a OperationSet,
    // Canonical results.
    objects: BTreeMap<TypedObjectId, ObjectState>,
    spellings: BTreeMap<PitchId, PitchSpelling>,
    breaks: BTreeMap<(RegionId, MusicalPosition), bool>,
    page_breaks: BTreeMap<(RegionId, MusicalPosition), bool>,
    conflicts: ConflictRegistry,
    effects: Vec<(OperationId, OperationEffect)>,
    anomalies: BTreeMap<epiphany_core::IntegrityAnomalyId, IntegrityAnomaly>,
    // Transient indices.
    minted_by: BTreeMap<TypedObjectId, OperationId>,
    event_pitches: BTreeMap<EventId, Vec<PitchId>>,
    voice_occupancy: BTreeMap<VoiceId, Vec<(MusicalPosition, MusicalDuration, EventId)>>,
    last_respell: BTreeMap<PitchId, OperationId>,
    // LWW working state for the Group-1 field-overwrite ops: the last modifier
    // and the value it wrote, used to detect concurrent *differing* modifications
    // (the resolved value itself lives in the graph, not in MaterializedState).
    last_event_modify: BTreeMap<EventId, (OperationId, Event)>,
    last_pitch_modify: BTreeMap<PitchId, (OperationId, Pitch)>,
    // LWW working state for ModifyCrossCutting (Group 2), mirroring the leaf-field
    // modify maps above: last modifier + value it wrote, keyed by structure id.
    last_cross_cutting_modify: BTreeMap<TypedObjectId, (OperationId, CrossCuttingValue)>,
    // LWW working state for SetMetricGrid (Group 4, M2d): the last writer and the
    // grid it wrote, used to detect concurrent *differing* grids (the resolved
    // value lives in the graph). SetMetadata is advisory LWW — no working state,
    // no conflict — so the latest write in canonical order silently wins.
    last_metric_grid: BTreeMap<RegionId, (OperationId, Option<MetricGrid>)>,
    structures: BTreeMap<TypedObjectId, Vec<TypedObjectId>>,
    // Live child sets for the structural-container empty-only delete (Group 3):
    // a region's live staff instances, and a staff instance's live voices. (A
    // voice's live events are read from `voice_occupancy`.)
    region_instances: BTreeMap<RegionId, BTreeSet<StaffInstanceId>>,
    instance_voices: BTreeMap<StaffInstanceId, BTreeSet<VoiceId>>,
    // Regions whose content carries a staff-based slot (staff-based or hybrid),
    // and so can hold a metric grid or user break. FreeGraphic regions cannot.
    // Tracking this lets SetMetricGrid / SetUserPageBreak / SetUserSystemBreak
    // reach the same precondition verdict for any region *represented in reducer
    // state* (those an op stream creates/deletes) whether or not a graph is
    // present. A base-only region exists in reducer state solely after
    // `seed_from_graph` (reduce_onto), so a base-free reduce() that never sees it
    // can still diverge on a base-region target — the corpus targets only
    // op-created regions, where the two agree.
    staff_based_regions: BTreeSet<RegionId>,
    migrated_regions: BTreeSet<RegionId>,
    region_migrator: BTreeMap<RegionId, OperationId>,
    descriptors: BTreeMap<TransactionId, OperationId>,
    // Losing insert -> (promoted voice, winning insert).
    promotion: BTreeMap<OperationId, (VoiceId, OperationId)>,
    tx_minted: BTreeMap<TransactionId, Vec<TypedObjectId>>,
    current_tx: Option<TransactionId>,
    graph: Option<Score>,
}

/// A snapshot of the working state, for atomic transaction rollback.
struct WorkingSnapshot {
    objects: BTreeMap<TypedObjectId, ObjectState>,
    spellings: BTreeMap<PitchId, PitchSpelling>,
    breaks: BTreeMap<(RegionId, MusicalPosition), bool>,
    page_breaks: BTreeMap<(RegionId, MusicalPosition), bool>,
    conflicts: ConflictRegistry,
    minted_by: BTreeMap<TypedObjectId, OperationId>,
    event_pitches: BTreeMap<EventId, Vec<PitchId>>,
    voice_occupancy: BTreeMap<VoiceId, Vec<(MusicalPosition, MusicalDuration, EventId)>>,
    last_respell: BTreeMap<PitchId, OperationId>,
    last_event_modify: BTreeMap<EventId, (OperationId, Event)>,
    last_pitch_modify: BTreeMap<PitchId, (OperationId, Pitch)>,
    last_cross_cutting_modify: BTreeMap<TypedObjectId, (OperationId, CrossCuttingValue)>,
    last_metric_grid: BTreeMap<RegionId, (OperationId, Option<MetricGrid>)>,
    structures: BTreeMap<TypedObjectId, Vec<TypedObjectId>>,
    region_instances: BTreeMap<RegionId, BTreeSet<StaffInstanceId>>,
    instance_voices: BTreeMap<StaffInstanceId, BTreeSet<VoiceId>>,
    staff_based_regions: BTreeSet<RegionId>,
    migrated_regions: BTreeSet<RegionId>,
    region_migrator: BTreeMap<RegionId, OperationId>,
    descriptors: BTreeMap<TransactionId, OperationId>,
    tx_minted: BTreeMap<TransactionId, Vec<TypedObjectId>>,
    graph: Option<Score>,
}

/// The precondition no-op a structural create or delete returns when a container
/// is non-empty where the operation requires it empty (a create carrying children,
/// or a delete of a container with live children).
fn container_not_empty() -> OperationEffect {
    OperationEffect::NoOp {
        reason: NoOpReason::PreconditionFailedUnderReduction {
            reason: PreconditionFailureReason::ContainerNotEmpty,
        },
    }
}

/// Apply a user break to a region's break list under the canonical LWW key — the
/// anchor's resolved musical position. Any existing anchor resolving to that same
/// position is dropped first, so two anchors at one position never both persist,
/// then the new anchor is pushed iff the break is present. The graph break list
/// then matches the resolved-position-keyed ledger map.
fn apply_break_lww(breaks: &mut Vec<TimeAnchor>, anchor: &TimeAnchor, present: bool) {
    let resolved = crate::payload::resolved_anchor_position(anchor);
    breaks.retain(|existing| crate::payload::resolved_anchor_position(existing) != resolved);
    if present {
        breaks.push(anchor.clone());
    }
}

fn intervals_overlap(
    a_position: &MusicalPosition,
    a_duration: &MusicalDuration,
    b_position: &MusicalPosition,
    b_duration: &MusicalDuration,
) -> bool {
    if !a_duration.is_positive() || !b_duration.is_positive() {
        return false;
    }
    let a_end = a_position.clone() + a_duration.clone();
    let b_end = b_position.clone() + b_duration.clone();
    a_position < &b_end && b_position < &a_end
}

fn insert_intervals_overlap(a: &InsertEventOp, b: &InsertEventOp) -> bool {
    intervals_overlap(
        &a.musical_position(),
        &a.musical_duration(),
        &b.musical_position(),
        &b.musical_duration(),
    )
}

fn graph_voice_location(score: &Score, voice: VoiceId) -> Option<(usize, usize, usize)> {
    for (region_index, region) in score.canvas.regions.iter().enumerate() {
        for (instance_index, instance) in region.staff_instances().iter().enumerate() {
            if let Some(voice_index) = instance
                .voices
                .iter()
                .position(|candidate| candidate.id == voice)
            {
                return Some((region_index, instance_index, voice_index));
            }
        }
    }
    None
}

/// The verdict on a [`ModifyEvent`](OperationKind::ModifyEvent)'s placement: whether
/// it moves the target event's metric span, and if so whether the move keeps
/// invariant 3 (`VoiceEventsSortedNonOverlap`). A non-metric or same-placement modify
/// is [`Unchanged`](PlacementVerdict::Unchanged) — handled by the existing field-edit
/// path.
enum PlacementVerdict {
    /// Same placement, or a non-metric event: nothing to materialize or refuse.
    Unchanged,
    /// A metric move that would overlap a sibling, or carries a non-positive span.
    Refused,
    /// A valid metric move to `position`/`duration` within `voice`.
    Moved {
        voice: VoiceId,
        position: MusicalPosition,
        duration: MusicalDuration,
    },
}

/// The graph value inserted by a value-typed InsertEvent: the carried [`Event`]
/// itself, with its voice rebound to the (possibly system-promoted) target
/// voice. The Operation Catalog (v1) carries the real event, so this is no
/// longer a placeholder reconstruction.
fn graph_event_from_insert(op: &InsertEventOp, target_voice: VoiceId) -> Event {
    let mut event = op.event.clone();
    event.set_voice(target_voice);
    event
}

impl<'a> Reducer<'a> {
    fn new(op_set: &'a OperationSet) -> Self {
        Reducer {
            op_set,
            objects: BTreeMap::new(),
            spellings: BTreeMap::new(),
            breaks: BTreeMap::new(),
            page_breaks: BTreeMap::new(),
            conflicts: ConflictRegistry::new(),
            effects: Vec::new(),
            anomalies: BTreeMap::new(),
            minted_by: BTreeMap::new(),
            event_pitches: BTreeMap::new(),
            voice_occupancy: BTreeMap::new(),
            last_respell: BTreeMap::new(),
            last_event_modify: BTreeMap::new(),
            last_pitch_modify: BTreeMap::new(),
            last_cross_cutting_modify: BTreeMap::new(),
            last_metric_grid: BTreeMap::new(),
            structures: BTreeMap::new(),
            region_instances: BTreeMap::new(),
            instance_voices: BTreeMap::new(),
            staff_based_regions: BTreeSet::new(),
            migrated_regions: BTreeSet::new(),
            region_migrator: BTreeMap::new(),
            descriptors: BTreeMap::new(),
            promotion: BTreeMap::new(),
            tx_minted: BTreeMap::new(),
            current_tx: None,
            graph: None,
        }
    }

    fn new_onto(op_set: &'a OperationSet, base: &Score) -> Self {
        let mut reducer = Self::new(op_set);
        reducer.graph = Some(base.clone());
        reducer.seed_from_graph();
        reducer
    }

    /// Seeds reduction indices from the canonical base graph. Base objects are
    /// live but have no operation minter; if a later operation tombstones one,
    /// that deleting operation is used as the provenance fallback already
    /// defined by the bookkeeping reducer.
    fn seed_from_graph(&mut self) {
        let Some(score) = self.graph.as_ref() else {
            return;
        };

        for instrument in &score.instruments {
            self.objects
                .insert(TypedObjectId::Instrument(instrument.id), ObjectState::Live);
        }
        for staff in &score.staves {
            self.objects
                .insert(TypedObjectId::Staff(staff.id), ObjectState::Live);
        }
        for group in &score.staff_groups {
            self.objects
                .insert(TypedObjectId::StaffGroup(group.id), ObjectState::Live);
        }
        for part in &score.parts {
            self.objects
                .insert(TypedObjectId::PartDefinition(part.id), ObjectState::Live);
        }
        for signature in &score.time_signatures {
            self.objects.insert(
                TypedObjectId::TimeSignature(signature.id),
                ObjectState::Live,
            );
        }
        for layer in &score.analysis_layers {
            self.objects
                .insert(TypedObjectId::AnalysisLayer(layer.id), ObjectState::Live);
        }
        for view in &score.views {
            self.objects
                .insert(TypedObjectId::View(view.id), ObjectState::Live);
        }

        for region in &score.canvas.regions {
            self.objects
                .insert(TypedObjectId::Region(region.id), ObjectState::Live);
            if region.content.staff_based().is_some() {
                self.staff_based_regions.insert(region.id);
            }
            let instance_set = self.region_instances.entry(region.id).or_default();
            for instance in region.staff_instances() {
                instance_set.insert(instance.id);
            }
            for instance in region.staff_instances() {
                self.objects
                    .insert(TypedObjectId::StaffInstance(instance.id), ObjectState::Live);
                let voice_set = self.instance_voices.entry(instance.id).or_default();
                for voice in &instance.voices {
                    voice_set.insert(voice.id);
                }
                for measure in &instance.measures {
                    self.objects
                        .insert(TypedObjectId::Measure(measure.id), ObjectState::Live);
                }
                for voice in &instance.voices {
                    self.objects
                        .insert(TypedObjectId::Voice(voice.id), ObjectState::Live);
                }
            }
        }

        for event in score.events.iter_canonical() {
            let event_id = event.id();
            self.objects
                .insert(TypedObjectId::Event(event_id), ObjectState::Live);
            let mut pitch_ids = Vec::new();
            let mut pitches = Vec::new();
            event.collect_identified_pitches(&mut pitches);
            for pitch in pitches {
                pitch_ids.push(pitch.id);
                self.objects
                    .insert(TypedObjectId::Pitch(pitch.id), ObjectState::Live);
            }
            self.event_pitches.insert(event_id, pitch_ids);

            if let (EventPosition::Musical(position), EventDuration::Musical(duration)) =
                (event.position(), event.duration())
            {
                self.voice_occupancy
                    .entry(event.voice())
                    .or_default()
                    .push((position.clone(), duration.clone(), event_id));
            }
        }

        for slur in &score.cross_cutting.slurs {
            let id = TypedObjectId::Slur(slur.id);
            self.objects.insert(id, ObjectState::Live);
            self.structures.insert(
                id,
                vec![
                    TypedObjectId::Event(slur.start_event),
                    TypedObjectId::Event(slur.end_event),
                ],
            );
        }
        for tie in &score.cross_cutting.ties {
            let id = TypedObjectId::Tie(tie.id);
            self.objects.insert(id, ObjectState::Live);
            self.structures.insert(
                id,
                vec![
                    TypedObjectId::Event(tie.start_event),
                    TypedObjectId::Event(tie.end_event),
                ],
            );
        }
        for beam in &score.cross_cutting.beams {
            let id = TypedObjectId::Beam(beam.id);
            self.objects.insert(id, ObjectState::Live);
            self.structures.insert(
                id,
                beam.events
                    .iter()
                    .copied()
                    .map(TypedObjectId::Event)
                    .collect(),
            );
        }
        for tuplet in &score.cross_cutting.tuplets {
            let id = TypedObjectId::Tuplet(tuplet.id);
            self.objects.insert(id, ObjectState::Live);
            self.structures.insert(
                id,
                tuplet
                    .members
                    .iter()
                    .copied()
                    .map(TypedObjectId::Event)
                    .collect(),
            );
        }
        for spanner in &score.cross_cutting.spanners {
            let id = TypedObjectId::Spanner(spanner.id);
            self.objects.insert(id, ObjectState::Live);
            // Record the spanner's event-anchored endpoints so a later event
            // tombstone re-anchors it through the same rule table as a created
            // spanner (keeping the graph and ledger consistent on delete).
            self.structures.insert(
                id,
                [&spanner.start, &spanner.end]
                    .into_iter()
                    .filter_map(|anchor| match anchor {
                        TimeAnchor::Event { id, .. } => Some(TypedObjectId::Event(*id)),
                        _ => None,
                    })
                    .collect(),
            );
        }
        for marker in &score.cross_cutting.markers {
            self.objects
                .insert(TypedObjectId::Marker(marker.id), ObjectState::Live);
        }
        for annotation in &score.cross_cutting.analytical {
            self.objects.insert(
                TypedObjectId::AnalyticalAnnotation(annotation.id),
                ObjectState::Live,
            );
        }
        for comment in &score.cross_cutting.comments {
            self.objects
                .insert(TypedObjectId::Comment(comment.id), ObjectState::Live);
        }
        for gesture in &score.cross_cutting.graphic_gestures {
            self.objects
                .insert(TypedObjectId::GraphicGesture(gesture.id), ObjectState::Live);
        }
        for repeat in &score.cross_cutting.repeats {
            self.objects
                .insert(TypedObjectId::RepeatStructure(repeat.id), ObjectState::Live);
        }
        for lyric in &score.cross_cutting.lyrics {
            self.objects
                .insert(TypedObjectId::LyricLine(lyric.id), ObjectState::Live);
        }
        for chord in &score.cross_cutting.chord_symbols {
            self.objects
                .insert(TypedObjectId::ChordSymbol(chord.id), ObjectState::Live);
        }
    }

    fn run(mut self) -> (MaterializedState, Option<Score>) {
        let singles = self.op_set.single_envelopes();
        let equivocated: BTreeSet<OperationId> =
            self.op_set.equivocated_ids().into_iter().collect();

        // 1. HLC monotonicity: exclude anomalous segments.
        let segments = detect_replica_anomalies(&singles);
        let mut excluded: BTreeSet<OperationId> = BTreeSet::new();
        for seg in &segments {
            excluded.extend(seg.excluded.iter().copied());
            self.record_anomaly(IntegrityAnomalyKind::ReplicaStreamQuarantined {
                replica: seg.replica,
                first_bad_counter: seg.first_bad_counter,
            });
        }
        for id in &equivocated {
            self.record_anomaly(IntegrityAnomalyKind::OperationSlotEquivocated {
                operation_id: *id,
            });
        }

        // 2. Reducible candidates = Single slots minus excluded.
        let reducible: Vec<&OperationEnvelope> = singles
            .iter()
            .copied()
            .filter(|e| !excluded.contains(&e.id))
            .collect();
        let reducible_ids: BTreeSet<OperationId> = reducible.iter().map(|e| e.id).collect();
        let declared_transactions: BTreeSet<TransactionId> = singles
            .iter()
            .filter_map(|env| match &env.payload {
                OperationPayload::Primitive(OperationKind::DeclareTransaction(descriptor)) => {
                    Some(descriptor.id)
                }
                _ => None,
            })
            .collect();

        // 3. Missing-causal-predecessor rule → pending set (with reasons).
        let pending = compute_pending(
            &reducible,
            &reducible_ids,
            &equivocated,
            &excluded,
            &declared_transactions,
        );
        let active: Vec<&OperationEnvelope> = reducible
            .iter()
            .copied()
            .filter(|e| !pending.contains_key(&e.id))
            .collect();

        // 4. Voice-promotion pre-pass (order-independent assignment).
        self.compute_promotions(&active);

        // 5. Walk active ops in canonical reduction order; group transactions.
        let order = canonical_reduction_order(&active);
        let tx_members = transaction_members(&active);
        let mut processed: BTreeSet<OperationId> = BTreeSet::new();
        for env in &order {
            if processed.contains(&env.id) {
                continue;
            }
            if let Some(tx) = member_transaction(env) {
                let members = tx_members.get(&tx).cloned().unwrap_or_default();
                self.reduce_transaction_block(tx, &members);
                processed.extend(members.iter().map(|m| m.id));
            } else {
                let effect = self.apply(env);
                self.effects.push((env.id, effect));
                processed.insert(env.id);
            }
        }

        let mut pending_vec: Vec<(OperationId, PendingReason)> = pending.into_iter().collect();
        pending_vec.sort_by_key(|(id, _)| *id);

        let graph = self.graph.take();
        let state = MaterializedState {
            effects: self.effects,
            conflicts: self.conflicts,
            anomalies: self.anomalies.into_values().collect(),
            objects: self.objects,
            spellings: self.spellings,
            breaks: self.breaks,
            page_breaks: self.page_breaks,
            pending: pending_vec,
        };
        (state, graph)
    }

    fn record_anomaly(&mut self, kind: IntegrityAnomalyKind) {
        let a = IntegrityAnomaly::new(kind);
        self.anomalies.entry(a.id).or_insert(a);
    }

    fn env_of(&self, id: OperationId) -> Option<&'a OperationEnvelope> {
        self.op_set.slot(id).and_then(|s| s.single())
    }

    // --- Voice promotion pre-pass (Chapter 6 §6.10 InsertEvent). ------------

    fn compute_promotions(&mut self, active: &[&OperationEnvelope]) {
        // Bucket inserts by target voice. Bucketing by `op.voice` alone realizes
        // the spec's `(staff_instance, original_voice)` key: a VoiceId is
        // globally unique and (Invariant 5) belongs to exactly one staff
        // instance, so the voice id alone determines the pair. Promotion applies
        // only to concurrent operations whose half-open duration intervals overlap.
        let mut buckets: BTreeMap<VoiceId, Vec<&OperationEnvelope>> = BTreeMap::new();
        for env in active {
            if let OperationPayload::Primitive(OperationKind::InsertEvent(op)) = &env.payload {
                if self.graph_insert_precondition(op).is_err() {
                    continue;
                }
                buckets.entry(op.voice()).or_default().push(env);
            }
        }
        for (_, mut bucket) in buckets {
            if bucket.len() < 2 {
                continue;
            }
            // Retain non-overlapping operations in the original voice in
            // OperationId order. A concurrent collision with an already-kept
            // insert receives its own deterministic promoted voice.
            bucket.sort_by_key(|e| e.id);
            let mut original_voice = Vec::<&OperationEnvelope>::new();
            for env in bucket {
                let OperationPayload::Primitive(OperationKind::InsertEvent(op)) = &env.payload
                else {
                    continue;
                };
                let collision = original_voice.iter().copied().find(|kept| {
                    let OperationPayload::Primitive(OperationKind::InsertEvent(kept_op)) =
                        &kept.payload
                    else {
                        return false;
                    };
                    self.concurrent(env.id, kept.id) && insert_intervals_overlap(op, kept_op)
                });
                if let Some(winner) = collision {
                    let promoted =
                        derive_promoted_voice_id(op.staff_instance, op.voice(), winner.id, env.id);
                    self.promotion.insert(env.id, (promoted, winner.id));
                } else {
                    original_voice.push(env);
                }
            }
        }
    }

    fn graph_insert_precondition(
        &self,
        op: &InsertEventOp,
    ) -> Result<(usize, usize, usize), PreconditionFailureReason> {
        let Some(score) = self.graph.as_ref() else {
            return Ok((0, 0, 0));
        };
        let location = graph_voice_location(score, op.voice())
            .ok_or(PreconditionFailureReason::VoiceMissing)?;
        let (region_index, instance_index, _) = location;
        let region = &score.canvas.regions[region_index];
        let instance = &region.staff_instances()[instance_index];
        if instance.id != op.staff_instance {
            return Err(PreconditionFailureReason::VoiceMissing);
        }
        if !matches!(region.time_model, epiphany_core::RegionTimeModel::Metric(_)) {
            return Err(PreconditionFailureReason::WrongRegionTimeModel);
        }
        let event_id = op.event_id();
        if score.events.contains(event_id)
            || score.tombstoned_events.contains(&event_id)
            || op.pitch_ids().iter().any(|pitch| {
                self.objects.contains_key(&TypedObjectId::Pitch(*pitch))
                    || score.tombstoned_pitches.contains(pitch)
            })
        {
            return Err(PreconditionFailureReason::TargetTombstoned);
        }
        Ok(location)
    }

    fn materialize_graph_insert(
        &mut self,
        env: &OperationEnvelope,
        op: &InsertEventOp,
        location: (usize, usize, usize),
        target_voice: VoiceId,
        promotion: Option<(VoiceId, OperationId)>,
    ) -> Result<(), PreconditionFailureReason> {
        let Some(score) = self.graph.as_mut() else {
            return Ok(());
        };
        let (region_index, instance_index, voice_index) = location;
        let event = graph_event_from_insert(op, target_voice);
        score
            .events
            .insert(event)
            .map_err(|_| PreconditionFailureReason::EventDurationInvalid)?;

        if let Some((promoted, winner)) = promotion {
            let instance = score.canvas.regions[region_index]
                .content
                .staff_instances_mut()
                .expect("the precondition found a staff-based instance")
                .get_mut(instance_index)
                .expect("the precondition found this instance");
            instance.voices.push(Voice {
                id: promoted,
                events: vec![op.event_id()],
                default_stem_direction: None,
                is_primary: false,
                origin: VoiceOrigin::SystemPromoted {
                    winning_operation: winner,
                    losing_operation: env.id,
                    original_voice: op.voice(),
                },
            });
        } else {
            let mut ordered = score.canvas.regions[region_index].staff_instances()[instance_index]
                .voices[voice_index]
                .events
                .clone();
            ordered.push(op.event_id());
            ordered.sort_by(|a, b| {
                let a_position = score.events.get(*a).map(Event::position);
                let b_position = score.events.get(*b).map(Event::position);
                match (a_position, b_position) {
                    (
                        Some(EventPosition::Musical(a_position)),
                        Some(EventPosition::Musical(b_position)),
                    ) => a_position.cmp(b_position).then_with(|| a.cmp(b)),
                    _ => a.cmp(b),
                }
            });
            score.canvas.regions[region_index]
                .content
                .staff_instances_mut()
                .expect("the precondition found a staff-based instance")[instance_index]
                .voices[voice_index]
                .events = ordered;
        }
        Ok(())
    }

    fn graph_delete_precondition(
        &self,
        op: &DeleteEventOp,
    ) -> Result<(), PreconditionFailureReason> {
        let Some(score) = self.graph.as_ref() else {
            return Ok(());
        };
        let event = score
            .events
            .get(op.event)
            .ok_or(PreconditionFailureReason::TargetMissing)?;
        let containing_tuplets: Vec<_> = score
            .cross_cutting
            .tuplets
            .iter()
            .filter(|tuplet| tuplet.members.contains(&op.event))
            .map(|tuplet| tuplet.id)
            .collect();
        match &op.tuplet_compensation {
            TupletCompensation::NotInTuplet if !containing_tuplets.is_empty() => {
                Err(PreconditionFailureReason::TupletCompensationInvalid)
            }
            TupletCompensation::NotInTuplet => Ok(()),
            TupletCompensation::ReplaceWithRest { rest } => {
                if score.events.contains(rest.id)
                    || score.tombstoned_events.contains(&rest.id)
                    || event.duration() != &rest.duration
                {
                    Err(PreconditionFailureReason::TupletCompensationInvalid)
                } else {
                    Ok(())
                }
            }
            // The prototype payload carries only ids, not the rewritten tuplet
            // values required to preserve invariant 16. Graph-aware reduction
            // refuses to fabricate those values.
            TupletCompensation::RewriteTuplets { .. } => {
                Err(PreconditionFailureReason::TupletCompensationInvalid)
            }
            TupletCompensation::CascadeDeleteTuplets { tuplets } => {
                let listed: BTreeSet<_> = tuplets.iter().copied().collect();
                let containing: BTreeSet<_> = containing_tuplets.into_iter().collect();
                if listed == containing && !listed.is_empty() {
                    Ok(())
                } else {
                    Err(PreconditionFailureReason::TupletCompensationInvalid)
                }
            }
        }
    }

    fn materialize_graph_delete(&mut self, op: &DeleteEventOp) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        let Some(event) = score.events.remove(op.event) else {
            return;
        };
        let voice_id = event.voice();
        let location = graph_voice_location(score, voice_id);
        let region_id = location.map(|(region, _, _)| score.canvas.regions[region].id);
        let removed_event_index = location.and_then(|(region, instance, voice)| {
            score.canvas.regions[region].staff_instances()[instance].voices[voice]
                .events
                .iter()
                .position(|event| *event == op.event)
        });
        if let Some((region_index, instance_index, voice_index)) = location {
            score.canvas.regions[region_index]
                .content
                .staff_instances_mut()
                .expect("an event voice belongs to staff-based content")[instance_index]
                .voices[voice_index]
                .events
                .retain(|id| *id != op.event);
        }

        let mut identified = Vec::new();
        event.collect_identified_pitches(&mut identified);
        let deleted_pitches: Vec<PitchId> = identified.iter().map(|pitch| pitch.id).collect();
        score.tombstoned_events.insert(op.event);
        score
            .tombstoned_pitches
            .extend(deleted_pitches.iter().copied());

        match &op.tuplet_compensation {
            TupletCompensation::ReplaceWithRest { rest } => {
                let new_rest = rest.id;
                for tuplet in &mut score.cross_cutting.tuplets {
                    for member in &mut tuplet.members {
                        if *member == op.event {
                            *member = new_rest;
                        }
                    }
                }
                // The value-typed payload (v1) carries the replacement Rest; it
                // is placed at the deleted event's voice and position.
                let mut replacement = rest.clone();
                replacement.voice = voice_id;
                replacement.position = event.position().clone();
                score
                    .events
                    .insert(Event::Rest(replacement))
                    .expect("replacement-rest preconditions were checked");
                if let Some((region_index, instance_index, voice_index)) =
                    graph_voice_location(score, voice_id)
                {
                    let voice = &mut score.canvas.regions[region_index]
                        .content
                        .staff_instances_mut()
                        .expect("an event voice belongs to staff-based content")[instance_index]
                        .voices[voice_index];
                    if !voice.events.contains(&new_rest) {
                        let index = removed_event_index
                            .unwrap_or(voice.events.len())
                            .min(voice.events.len());
                        voice.events.insert(index, new_rest);
                    }
                }
            }
            TupletCompensation::CascadeDeleteTuplets { tuplets } => {
                let removed: BTreeSet<_> = tuplets.iter().copied().collect();
                score
                    .cross_cutting
                    .tuplets
                    .retain(|tuplet| !removed.contains(&tuplet.id));
            }
            TupletCompensation::NotInTuplet | TupletCompensation::RewriteTuplets { .. } => {}
        }

        // Keep the materialized graph reference-clean. The detailed repair
        // records remain in Chapter 6 state; these graph updates realize the
        // representative event-anchored structures Agent B models.
        score
            .cross_cutting
            .ties
            .retain(|tie| tie.start_event != op.event && tie.end_event != op.event);
        score.cross_cutting.beams.retain_mut(|beam| {
            beam.events.retain(|event| *event != op.event);
            beam.events.len() >= 2
        });
        // Slurs and spanners follow the bookkeeping re-anchoring rule: an
        // endpoint-deleted structure re-anchors to its surviving endpoint while
        // one survives, and cascade-deletes only when none does (Chapter 6 §6.5;
        // matching `reanchor_for_tombstone`, so the graph and the ledger agree on
        // the structure's existence). A re-anchored two-endpoint structure
        // collapses onto the survivor — degenerate but reference-clean (both
        // endpoints stay live); proximity-aware re-anchoring is deferred (P11-C5).
        let slurs = std::mem::take(&mut score.cross_cutting.slurs);
        let kept_slurs: Vec<_> = slurs
            .into_iter()
            .filter_map(|mut slur| {
                let start_hit = slur.start_event == op.event;
                let end_hit = slur.end_event == op.event;
                if !start_hit && !end_hit {
                    return Some(slur);
                }
                let survivor = if start_hit {
                    slur.end_event
                } else {
                    slur.start_event
                };
                if survivor != op.event && score.events.contains(survivor) {
                    slur.start_event = if start_hit {
                        survivor
                    } else {
                        slur.start_event
                    };
                    slur.end_event = if end_hit { survivor } else { slur.end_event };
                    Some(slur)
                } else {
                    None
                }
            })
            .collect();
        score.cross_cutting.slurs = kept_slurs;

        let spanners = std::mem::take(&mut score.cross_cutting.spanners);
        let kept_spanners: Vec<_> = spanners
            .into_iter()
            .filter_map(|mut spanner| {
                let start_hit =
                    matches!(spanner.start, TimeAnchor::Event { id, .. } if id == op.event);
                let end_hit = matches!(spanner.end, TimeAnchor::Event { id, .. } if id == op.event);
                if !start_hit && !end_hit {
                    return Some(spanner);
                }
                // The survivor is the *other* anchor's event, if it is an event
                // anchor on a live event; otherwise the spanner cascade-deletes.
                let other = if start_hit {
                    &spanner.end
                } else {
                    &spanner.start
                };
                let survivor = match other {
                    TimeAnchor::Event { id, .. }
                        if *id != op.event && score.events.contains(*id) =>
                    {
                        Some(*id)
                    }
                    _ => None,
                };
                let to = survivor?;
                if start_hit {
                    if let TimeAnchor::Event { id, .. } = &mut spanner.start {
                        *id = to;
                    }
                }
                if end_hit {
                    if let TimeAnchor::Event { id, .. } = &mut spanner.end {
                        *id = to;
                    }
                }
                Some(spanner)
            })
            .collect();
        score.cross_cutting.spanners = kept_spanners;

        score.cross_cutting.lyrics.retain_mut(|line| {
            line.events.retain(|event| *event != op.event);
            !line.events.is_empty()
        });
        if let Some(region) = region_id {
            let fallback = TimeAnchor::Region {
                id: region,
                edge: RegionEdge::Start,
                offset: AnchorOffset::Zero,
            };
            for marker in &mut score.cross_cutting.markers {
                if matches!(marker.anchor, TimeAnchor::Event { id, .. } if id == op.event) {
                    marker.anchor = fallback.clone();
                }
            }
        }
    }

    fn materialize_graph_tombstones(&mut self, targets: &[TypedObjectId]) {
        let events: Vec<EventId> = targets
            .iter()
            .filter_map(|target| match target {
                TypedObjectId::Event(event) => Some(*event),
                _ => None,
            })
            .collect();
        for event in events {
            for placements in self.voice_occupancy.values_mut() {
                placements.retain(|(_, _, stored_event)| *stored_event != event);
            }
            self.voice_occupancy
                .retain(|_, placements| !placements.is_empty());
            self.materialize_graph_delete(&DeleteEventOp {
                event,
                tuplet_compensation: TupletCompensation::NotInTuplet,
            });
        }

        let Some(score) = self.graph.as_mut() else {
            return;
        };
        for target in targets {
            match target {
                TypedObjectId::Pitch(pitch) => {
                    score.tombstoned_pitches.insert(*pitch);
                }
                TypedObjectId::Voice(voice) => {
                    for region in &mut score.canvas.regions {
                        if let Some(instances) = region.content.staff_instances_mut() {
                            for instance in instances {
                                instance.voices.retain(|candidate| {
                                    candidate.id != *voice || !candidate.events.is_empty()
                                });
                            }
                        }
                    }
                }
                TypedObjectId::Slur(id) => {
                    score.cross_cutting.slurs.retain(|value| value.id != *id);
                }
                TypedObjectId::Tie(id) => {
                    score.cross_cutting.ties.retain(|value| value.id != *id);
                }
                TypedObjectId::Beam(id) => {
                    score.cross_cutting.beams.retain(|value| value.id != *id);
                }
                _ => {}
            }
        }
    }

    fn materialize_graph_cross_cutting(
        &mut self,
        op: &CreateCrossCuttingOp,
    ) -> Result<(), PreconditionFailureReason> {
        let Some(score) = self.graph.as_mut() else {
            return Ok(());
        };
        // The value-typed payload (v1) carries the real structure with its rich
        // fields, so materialization clones it directly rather than rebuilding a
        // default from the reference-level projection.
        match &op.structure {
            CrossCuttingValue::Slur(slur) => score.cross_cutting.slurs.push(slur.clone()),
            CrossCuttingValue::Beam(beam) => {
                if beam.events.len() < 2 {
                    return Err(PreconditionFailureReason::TargetMissing);
                }
                score.cross_cutting.beams.push(beam.clone());
            }
            CrossCuttingValue::Tie(tie) => score.cross_cutting.ties.push(tie.clone()),
            CrossCuttingValue::Spanner(spanner) => {
                score.cross_cutting.spanners.push(spanner.clone())
            }
        }
        Ok(())
    }

    // --- Dispatch. ----------------------------------------------------------

    fn apply(&mut self, env: &OperationEnvelope) -> OperationEffect {
        match &env.payload {
            OperationPayload::Primitive(kind) => match kind {
                OperationKind::InsertEvent(op) => self.insert_event(env, op),
                OperationKind::DeleteEvent(op) => self.delete_event(env, op),
                OperationKind::RespellPitch(op) => self.respell_pitch(env, op),
                OperationKind::CreateCrossCutting(op) => self.create_cross_cutting(env, op),
                OperationKind::ChangeRegionTimeModel(op) => self.change_region_time_model(env, op),
                OperationKind::SetUserSystemBreak(op) => self.set_user_system_break(op),
                OperationKind::DeclareTransaction(desc) => {
                    self.descriptors.insert(desc.id, env.id);
                    OperationEffect::Applied
                }
                // Extension-defined primitive: opaque to the core; recorded as
                // applied (the extension realizes its own effect).
                OperationKind::Registered(_, _) => OperationEffect::Applied,
                OperationKind::ModifyEvent(op) => self.modify_event(env, op),
                OperationKind::Transpose(op) => self.transpose(env, op),
                OperationKind::InsertIdentifiedPitch(op) => self.insert_identified_pitch(env, op),
                OperationKind::DeleteIdentifiedPitch(op) => self.delete_identified_pitch(env, op),
                OperationKind::ModifyIdentifiedPitch(op) => self.modify_identified_pitch(env, op),
                OperationKind::DeleteCrossCutting(op) => self.delete_cross_cutting(env, op),
                OperationKind::ModifyCrossCutting(op) => self.modify_cross_cutting(env, op),
                OperationKind::CreateRegion(op) => self.create_region(env, op),
                OperationKind::DeleteRegion(op) => self.delete_region(env, op),
                OperationKind::CreateStaffInstance(op) => self.create_staff_instance(env, op),
                OperationKind::DeleteStaffInstance(op) => self.delete_staff_instance(env, op),
                OperationKind::CreateVoice(op) => self.create_voice(env, op),
                OperationKind::DeleteVoice(op) => self.delete_voice(env, op),
                OperationKind::SetMetadata(op) => self.set_metadata(op),
                OperationKind::SetMetricGrid(op) => self.set_metric_grid(env, op),
                OperationKind::SetUserPageBreak(op) => self.set_user_page_break(op),
            },
            OperationPayload::ResolveConflict(op) => self.resolve_conflict(env, op),
            OperationPayload::UndoTransaction(op) => self.undo_transaction(env, op),
        }
    }

    // --- Per-kind reduction. ------------------------------------------------

    fn set_user_system_break(
        &mut self,
        op: &crate::payload::SetUserSystemBreakOp,
    ) -> OperationEffect {
        if let Some(effect) = self.layout_region_slot(op.region) {
            return effect;
        }
        if let Some(score) = self.graph.as_mut() {
            if let Some(region) = score.canvas.regions.iter_mut().find(|r| r.id == op.region) {
                if let Some(content) = region.content.staff_based_mut() {
                    apply_break_lww(&mut content.user_system_breaks, &op.anchor, op.present);
                }
            }
        }

        // The LWW bucketing key is the anchor's resolved musical position.
        self.breaks
            .insert((op.region, op.resolved_position()), op.present);
        OperationEffect::Applied
    }

    // --- Group 4 (M2d): score settings (LWW field-overwrite). --------------
    //
    // SetMetadata is an *advisory* last-writer-wins field (operation_catalog
    // §set-user-system-break "LWW advisory"): the latest write in canonical order
    // silently wins, with no conflict — a clean concurrent metadata edit keeps the
    // state clean. SetMetricGrid is a structural field-overwrite: the resolved grid
    // lives in the graph and a concurrent differing grid records a
    // StructuralFieldCollision. SetUserPageBreak mirrors SetUserSystemBreak: a
    // canonical LWW advisory (page_breaks).
    //
    // SetMetricGrid / SetUserPageBreak target a region's staff-based slot, so they
    // share `layout_region_slot`: the region must be live *and* staff-based (a
    // FreeGraphic region has neither a metric grid nor a break list). That verdict
    // reads only the base-free indices, so reduce() and reduce_onto() agree on it.

    /// `Some(NoOp)` when `region` cannot carry a metric grid or user break — it is
    /// missing, tombstoned, or FreeGraphic; `None` when it has a staff-based slot.
    fn layout_region_slot(&self, region: RegionId) -> Option<OperationEffect> {
        let live = matches!(
            self.objects.get(&TypedObjectId::Region(region)),
            Some(ObjectState::Live)
        );
        if live && self.staff_based_regions.contains(&region) {
            None
        } else {
            Some(OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            })
        }
    }

    fn set_metadata(&mut self, op: &SetMetadataOp) -> OperationEffect {
        // Advisory LWW: no conflict, no idempotence short-circuit. The resolved
        // value is the last write in canonical order, held in the graph.
        if let Some(score) = self.graph.as_mut() {
            score.metadata = op.metadata.clone();
        }
        OperationEffect::Applied
    }

    fn set_metric_grid(
        &mut self,
        env: &OperationEnvelope,
        op: &SetMetricGridOp,
    ) -> OperationEffect {
        if let Some(effect) = self.layout_region_slot(op.region) {
            return effect;
        }
        // A non-empty grid names a time signature per meter change; the graph
        // invariant (epiphany-core invariants.rs) rejects a grid that references an
        // undeclared signature, so reject it here rather than install an
        // invariant-violating grid. Time signatures are seeded from the base, so
        // this verdict is identical with or without a graph.
        if let Some(grid) = &op.grid {
            for change in &grid.meter_sequence {
                if !matches!(
                    self.objects
                        .get(&TypedObjectId::TimeSignature(change.time_signature)),
                    Some(ObjectState::Live)
                ) {
                    return OperationEffect::NoOp {
                        reason: NoOpReason::PreconditionFailedUnderReduction {
                            reason: PreconditionFailureReason::TargetMissing,
                        },
                    };
                }
            }
        }
        let prev = self
            .last_metric_grid
            .get(&op.region)
            .map(|(o, g)| (*o, g.clone()));
        let effect = match prev {
            Some((prev_op, prev_grid)) if self.concurrent(env.id, prev_op) => {
                if prev_grid == op.grid {
                    return OperationEffect::NoOp {
                        reason: NoOpReason::AlreadyApplied,
                    };
                }
                let conflict = ConflictRecord::new(
                    ConflictKind::StructuralFieldCollision {
                        winner: env.id,
                        loser: prev_op,
                        field: FieldPath("metric_grid".to_string()),
                    },
                    vec![env.id, prev_op],
                    vec![TypedObjectId::Region(op.region)],
                );
                let cid = conflict.id;
                self.conflicts.insert(conflict);
                OperationEffect::Conflicted { conflict: cid }
            }
            _ => OperationEffect::Applied,
        };
        self.last_metric_grid
            .insert(op.region, (env.id, op.grid.clone()));
        self.graph_set_metric_grid(op.region, &op.grid);
        effect
    }

    fn graph_set_metric_grid(&mut self, region: RegionId, grid: &Option<MetricGrid>) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        if let Some(region) = score.canvas.regions.iter_mut().find(|r| r.id == region) {
            if let Some(content) = region.content.staff_based_mut() {
                content.default_metric_grid = grid.clone();
            }
        }
    }

    fn set_user_page_break(&mut self, op: &SetUserPageBreakOp) -> OperationEffect {
        if let Some(effect) = self.layout_region_slot(op.region) {
            return effect;
        }
        if let Some(score) = self.graph.as_mut() {
            if let Some(region) = score.canvas.regions.iter_mut().find(|r| r.id == op.region) {
                if let Some(content) = region.content.staff_based_mut() {
                    apply_break_lww(&mut content.user_page_breaks, &op.anchor, op.present);
                }
            }
        }
        self.page_breaks
            .insert((op.region, op.resolved_position()), op.present);
        OperationEffect::Applied
    }

    fn insert_event(&mut self, env: &OperationEnvelope, op: &InsertEventOp) -> OperationEffect {
        // The reduction keys are read from the carried event value (v1).
        let event_id = op.event_id();
        let orig_voice = op.voice();
        let op_position = op.musical_position();
        let op_duration = op.musical_duration();
        if !op_duration.is_positive() {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::EventDurationInvalid,
                },
            };
        }
        let ev_obj = TypedObjectId::Event(event_id);
        match self.objects.get(&ev_obj) {
            Some(ObjectState::Live) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::AlreadyApplied,
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::TargetTombstoned,
                }
            }
            None => {}
        }
        let graph_location = match self.graph_insert_precondition(op) {
            Ok(location) => location,
            Err(reason) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction { reason },
                }
            }
        };
        let voice_obj = TypedObjectId::Voice(orig_voice);
        match self.objects.get(&voice_obj) {
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::VoiceMissing,
                    },
                }
            }
            Some(ObjectState::Live) => {}
            None => {
                // Implicit voice creation on first use (prototype convention).
                self.objects.insert(voice_obj, ObjectState::Live);
                self.minted_by.insert(voice_obj, env.id);
                // Track it under its staff instance for the container empty-check.
                self.instance_voices
                    .entry(op.staff_instance)
                    .or_default()
                    .insert(orig_voice);
            }
        }

        let promotion = self.promotion.get(&env.id).copied();
        let target_voice = promotion.map(|(voice, _)| voice).unwrap_or(orig_voice);
        if self
            .voice_occupancy
            .get(&target_voice)
            .is_some_and(|events| {
                events.iter().any(|(position, duration, _)| {
                    intervals_overlap(position, duration, &op_position, &op_duration)
                })
            })
        {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::EventDurationInvalid,
                },
            };
        }

        if let Err(reason) =
            self.materialize_graph_insert(env, op, graph_location, target_voice, promotion)
        {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction { reason },
            };
        }

        let mut repairs = Vec::new();
        if let Some((promoted, _)) = promotion {
            let pv = TypedObjectId::Voice(promoted);
            self.objects.entry(pv).or_insert(ObjectState::Live);
            self.minted_by.entry(pv).or_insert(env.id);
            self.note_minted(env, pv);
            repairs.push(RepairRecord {
                kind: RepairKind::VoicePromoted {
                    from: orig_voice,
                    to: promoted,
                },
                target: pv,
            });
        }

        self.objects.insert(ev_obj, ObjectState::Live);
        self.minted_by.insert(ev_obj, env.id);
        self.note_minted(env, ev_obj);
        let mut pitches = Vec::new();
        for p in op.pitch_ids() {
            let p_obj = TypedObjectId::Pitch(p);
            self.objects.insert(p_obj, ObjectState::Live);
            self.minted_by.insert(p_obj, env.id);
            self.note_minted(env, p_obj);
            pitches.push(p);
        }
        self.event_pitches.insert(event_id, pitches);
        self.voice_occupancy.entry(target_voice).or_default().push((
            op_position,
            op_duration,
            event_id,
        ));

        if repairs.is_empty() {
            OperationEffect::Applied
        } else {
            OperationEffect::AppliedWithRepair { repairs }
        }
    }

    fn delete_event(&mut self, env: &OperationEnvelope, op: &DeleteEventOp) -> OperationEffect {
        let ev_obj = TypedObjectId::Event(op.event);
        match self.objects.get(&ev_obj) {
            None => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                // Concurrent same-target deletes are idempotent.
                return OperationEffect::NoOp {
                    reason: NoOpReason::AlreadyApplied,
                };
            }
            Some(ObjectState::Live) => {}
        }
        if let Err(reason) = self.graph_delete_precondition(op) {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction { reason },
            };
        }

        let deleted_placement = self.voice_occupancy.iter().find_map(|(voice, events)| {
            events
                .iter()
                .find(|(_, _, event)| *event == op.event)
                .map(|(position, duration, _)| (*voice, position.clone(), duration.clone()))
        });
        self.materialize_graph_delete(op);

        let minter = self.minted_by.get(&ev_obj).copied().unwrap_or(env.id);
        self.objects.insert(
            ev_obj,
            ObjectState::Tombstoned {
                deleted_by: env.id,
                minted_by: minter,
            },
        );
        for events in self.voice_occupancy.values_mut() {
            events.retain(|(_, _, event)| *event != op.event);
        }
        self.voice_occupancy.retain(|_, events| !events.is_empty());
        let mut repairs = Vec::new();

        // Tombstone contained pitches.
        if let Some(pitches) = self.event_pitches.get(&op.event).cloned() {
            for p in pitches {
                let p_obj = TypedObjectId::Pitch(p);
                let pm = self.minted_by.get(&p_obj).copied().unwrap_or(env.id);
                self.objects.insert(
                    p_obj,
                    ObjectState::Tombstoned {
                        deleted_by: env.id,
                        minted_by: pm,
                    },
                );
            }
        }

        // Tuplet compensation.
        match &op.tuplet_compensation {
            TupletCompensation::NotInTuplet => {}
            TupletCompensation::ReplaceWithRest { rest } => {
                let new_rest = rest.id;
                let rest_duration = match &rest.duration {
                    EventDuration::Musical(d) => d.clone(),
                    EventDuration::WallClock(_) | EventDuration::Indeterminate(_) => {
                        MusicalDuration::zero()
                    }
                };
                let rest_obj = TypedObjectId::Event(new_rest);
                self.objects.insert(rest_obj, ObjectState::Live);
                self.minted_by.insert(rest_obj, env.id);
                self.note_minted(env, rest_obj);
                self.event_pitches.insert(new_rest, Vec::new());
                if let Some((voice, position, _)) = &deleted_placement {
                    self.voice_occupancy.entry(*voice).or_default().push((
                        position.clone(),
                        rest_duration,
                        new_rest,
                    ));
                }
                repairs.push(RepairRecord {
                    kind: RepairKind::TupletCompensated {
                        compensation_kind: TupletCompensationKind::ReplaceWithRest,
                    },
                    target: rest_obj,
                });
            }
            TupletCompensation::RewriteTuplets { tuplets } => {
                if let Some(first) = tuplets.first() {
                    repairs.push(RepairRecord {
                        kind: RepairKind::TupletCompensated {
                            compensation_kind: TupletCompensationKind::RewriteTuplets,
                        },
                        target: TypedObjectId::Tuplet(*first),
                    });
                }
            }
            TupletCompensation::CascadeDeleteTuplets { tuplets } => {
                for t in tuplets {
                    let t_obj = TypedObjectId::Tuplet(*t);
                    let tm = self.minted_by.get(&t_obj).copied().unwrap_or(env.id);
                    self.objects.insert(
                        t_obj,
                        ObjectState::Tombstoned {
                            deleted_by: env.id,
                            minted_by: tm,
                        },
                    );
                    repairs.push(RepairRecord {
                        kind: RepairKind::TupletCompensated {
                            compensation_kind: TupletCompensationKind::CascadeDeleteTuplets,
                        },
                        target: t_obj,
                    });
                }
            }
        }

        // Re-anchor cross-cutting structures referencing the tombstoned event.
        self.reanchor_for_tombstone(env, ev_obj, &mut repairs);

        if repairs.is_empty() {
            OperationEffect::Applied
        } else {
            OperationEffect::AppliedWithRepair { repairs }
        }
    }

    fn respell_pitch(&mut self, env: &OperationEnvelope, op: &RespellPitchOp) -> OperationEffect {
        let p_obj = TypedObjectId::Pitch(op.pitch);
        match self.objects.get(&p_obj) {
            None => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::TargetTombstoned,
                }
            }
            Some(ObjectState::Live) => {}
        }

        match self.last_respell.get(&op.pitch).copied() {
            None => {
                self.materialize_respell(env, op);
                OperationEffect::Applied
            }
            Some(prev_op) => {
                let prev_spelling = self.spellings.get(&op.pitch).cloned();
                let concurrent = self.concurrent(env.id, prev_op);
                if concurrent {
                    if prev_spelling.as_ref() == Some(&op.spelling) {
                        // Identical concurrent respelling: idempotent.
                        OperationEffect::NoOp {
                            reason: NoOpReason::AlreadyApplied,
                        }
                    } else {
                        // Differing concurrent respelling: this op is later in
                        // canonical order, so it wins and materializes; the
                        // earlier op is recorded as the loser. (Prototype
                        // convention: the winner carries the Conflicted effect;
                        // see DECISIONS.md.)
                        self.materialize_respell(env, op);
                        let conflict = ConflictRecord::new(
                            ConflictKind::StructuralFieldCollision {
                                winner: env.id,
                                loser: prev_op,
                                field: FieldPath("spelling".to_string()),
                            },
                            vec![env.id, prev_op],
                            vec![p_obj],
                        );
                        let cid = conflict.id;
                        self.conflicts.insert(conflict);
                        OperationEffect::Conflicted { conflict: cid }
                    }
                } else {
                    // Causally-ordered re-respell: intentional overwrite.
                    self.materialize_respell(env, op);
                    OperationEffect::Applied
                }
            }
        }
    }

    /// Records a winning respell: the canonical bookkeeping spelling, the LWW
    /// marker, and — for graph-aware reduction — an explicit user-chosen
    /// [`SpellingAttachment`] so the spelling/decomposition pre-passes (Agent H's
    /// `derive_annotations`) resolve the override with manual-override precedence.
    /// Without the graph attachment a reduced `RespellPitch` would be visible only
    /// in `MaterializedState.spellings` and lost before annotation derivation.
    fn materialize_respell(&mut self, env: &OperationEnvelope, op: &RespellPitchOp) {
        self.spellings.insert(op.pitch, op.spelling.clone());
        self.last_respell.insert(op.pitch, env.id);
        self.graph_respell_pitch(op.pitch, &op.spelling);
    }

    fn graph_respell_pitch(&mut self, pitch: PitchId, spelling: &PitchSpelling) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        // Upsert the user-chosen explicit override (one per pitch), keeping the
        // attachment list's canonical order stable for the resolver's tie-break.
        if let Some(existing) = score.spelling_attachments.iter_mut().find(|a| {
            a.layer.is_none()
                && matches!(a.source, SpellingSource::UserChosen)
                && matches!(&a.scope, SpellingScope::Pitch(p) if *p == pitch)
                && matches!(a.directive, SpellingDirective::Explicit(_))
        }) {
            existing.directive = SpellingDirective::Explicit(spelling.clone());
        } else {
            score.spelling_attachments.push(SpellingAttachment {
                scope: SpellingScope::Pitch(pitch),
                directive: SpellingDirective::Explicit(spelling.clone()),
                source: SpellingSource::UserChosen,
                priority: 0,
                layer: None,
            });
        }
    }

    fn create_cross_cutting(
        &mut self,
        env: &OperationEnvelope,
        op: &CreateCrossCuttingOp,
    ) -> OperationEffect {
        let sid = op.structure.id();
        let endpoints = op.structure.endpoints();
        match self.objects.get(&sid) {
            Some(ObjectState::Live) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::AlreadyApplied,
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::TargetTombstoned,
                }
            }
            None => {}
        }
        // Endpoints must exist (live).
        for e in &endpoints {
            if !matches!(self.objects.get(e), Some(ObjectState::Live)) {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                };
            }
        }
        if let Err(reason) = self.materialize_graph_cross_cutting(op) {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction { reason },
            };
        }
        self.objects.insert(sid, ObjectState::Live);
        self.minted_by.insert(sid, env.id);
        self.note_minted(env, sid);
        self.structures.insert(sid, endpoints);
        OperationEffect::Applied
    }

    fn delete_cross_cutting(
        &mut self,
        env: &OperationEnvelope,
        op: &DeleteCrossCuttingOp,
    ) -> OperationEffect {
        let sid = op.structure;
        // DeleteCrossCutting names a cross-cutting structure only; refuse to
        // tombstone any other object kind through this path.
        if !matches!(
            sid,
            TypedObjectId::Tie(_)
                | TypedObjectId::Slur(_)
                | TypedObjectId::Beam(_)
                | TypedObjectId::Spanner(_)
        ) {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            };
        }
        let minted_by = match self.objects.get(&sid) {
            None => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                }
            }
            // Concurrent same-target deletes are idempotent (delete-wins).
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::AlreadyApplied,
                }
            }
            Some(ObjectState::Live) => self.minted_by.get(&sid).copied().unwrap_or(env.id),
        };
        self.objects.insert(
            sid,
            ObjectState::Tombstoned {
                deleted_by: env.id,
                minted_by,
            },
        );
        // Drop the transient endpoint/LWW indices so a later event tombstone's
        // re-anchoring pass never re-processes the deleted structure.
        self.structures.remove(&sid);
        self.last_cross_cutting_modify.remove(&sid);
        self.graph_delete_cross_cutting(sid);
        OperationEffect::Applied
    }

    fn modify_cross_cutting(
        &mut self,
        env: &OperationEnvelope,
        op: &ModifyCrossCuttingOp,
    ) -> OperationEffect {
        let sid = op.id();
        match self.objects.get(&sid) {
            None => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::TargetTombstoned,
                }
            }
            Some(ObjectState::Live) => {}
        }
        // A beam must keep at least two events (mirrors CreateCrossCutting); the
        // new endpoints must all be live.
        if let CrossCuttingValue::Beam(beam) = &op.structure {
            if beam.events.len() < 2 {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                };
            }
        }
        let endpoints = op.structure.endpoints();
        for e in &endpoints {
            if !matches!(self.objects.get(e), Some(ObjectState::Live)) {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                };
            }
        }
        // LWW field-overwrite, mirroring modify_event: the resolved value lives in
        // the graph; MaterializedState records only the effect and, on a
        // concurrent differing write, a StructuralFieldCollision.
        let prev = self
            .last_cross_cutting_modify
            .get(&sid)
            .map(|(o, v)| (*o, v.clone()));
        let effect = match prev {
            Some((prev_op, prev_value)) if self.concurrent(env.id, prev_op) => {
                if prev_value == op.structure {
                    return OperationEffect::NoOp {
                        reason: NoOpReason::AlreadyApplied,
                    };
                }
                let conflict = ConflictRecord::new(
                    ConflictKind::StructuralFieldCollision {
                        winner: env.id,
                        loser: prev_op,
                        field: FieldPath("cross_cutting".to_string()),
                    },
                    vec![env.id, prev_op],
                    vec![sid],
                );
                let cid = conflict.id;
                self.conflicts.insert(conflict);
                OperationEffect::Conflicted { conflict: cid }
            }
            _ => OperationEffect::Applied,
        };
        self.last_cross_cutting_modify
            .insert(sid, (env.id, op.structure.clone()));
        self.structures.insert(sid, endpoints);
        self.graph_modify_cross_cutting(&op.structure);
        effect
    }

    fn graph_delete_cross_cutting(&mut self, sid: TypedObjectId) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        match sid {
            TypedObjectId::Slur(id) => score.cross_cutting.slurs.retain(|v| v.id != id),
            TypedObjectId::Tie(id) => score.cross_cutting.ties.retain(|v| v.id != id),
            TypedObjectId::Beam(id) => score.cross_cutting.beams.retain(|v| v.id != id),
            TypedObjectId::Spanner(id) => score.cross_cutting.spanners.retain(|v| v.id != id),
            _ => {}
        }
    }

    fn graph_modify_cross_cutting(&mut self, value: &CrossCuttingValue) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        // Replace the structure in place by id (identity is preserved across a
        // modify). A structure that is Live in bookkeeping but absent from the
        // graph (e.g. one a DeleteEvent re-anchor removed from the graph while
        // bookkeeping kept it Live) is left as-is; the modify is still recorded.
        match value {
            CrossCuttingValue::Slur(slur) => {
                if let Some(existing) = score
                    .cross_cutting
                    .slurs
                    .iter_mut()
                    .find(|v| v.id == slur.id)
                {
                    *existing = slur.clone();
                }
            }
            CrossCuttingValue::Tie(tie) => {
                if let Some(existing) = score.cross_cutting.ties.iter_mut().find(|v| v.id == tie.id)
                {
                    *existing = tie.clone();
                }
            }
            CrossCuttingValue::Beam(beam) => {
                if let Some(existing) = score
                    .cross_cutting
                    .beams
                    .iter_mut()
                    .find(|v| v.id == beam.id)
                {
                    *existing = beam.clone();
                }
            }
            CrossCuttingValue::Spanner(spanner) => {
                if let Some(existing) = score
                    .cross_cutting
                    .spanners
                    .iter_mut()
                    .find(|v| v.id == spanner.id)
                {
                    *existing = spanner.clone();
                }
            }
        }
    }

    // --- Group 3 (M2c): structural container CRUD. -------------------------
    //
    // Creates mint an empty container (set-union creation; the authoring contract
    // is that the carried value is empty, so only its own object is minted).
    // Deletes are empty-only delete-wins: a precondition NoOp unless the
    // container has no live children. Child liveness is read from
    // `region_instances` / `instance_voices` (and `voice_occupancy` for a voice's
    // events), so the ledger projection and the graph agree on the result.

    fn mint_container(&mut self, env: &OperationEnvelope, obj: TypedObjectId) {
        self.objects.insert(obj, ObjectState::Live);
        self.minted_by.insert(obj, env.id);
        self.note_minted(env, obj);
    }

    /// `Some(effect)` when `obj` cannot be freshly minted (already live or
    /// tombstoned); `None` when it is fresh and the create may proceed.
    fn mint_precondition(&self, obj: TypedObjectId) -> Option<OperationEffect> {
        match self.objects.get(&obj) {
            Some(ObjectState::Live) => Some(OperationEffect::NoOp {
                reason: NoOpReason::AlreadyApplied,
            }),
            Some(ObjectState::Tombstoned { .. }) => Some(OperationEffect::NoOp {
                reason: NoOpReason::TargetTombstoned,
            }),
            None => None,
        }
    }

    fn create_region(&mut self, env: &OperationEnvelope, op: &CreateRegionOp) -> OperationEffect {
        let robj = TypedObjectId::Region(op.region_id());
        if let Some(effect) = self.mint_precondition(robj) {
            return effect;
        }
        // A create mints an *empty* container: its child objects are minted (or
        // base-seeded) separately. A carried value bearing any typed child object
        // — a staff instance, a barline-alignment group, or a graphic object, each
        // a distinct TypedObjectId the reducer does not mint here — would import an
        // unminted object into the graph, so it is rejected (Catalog §Structural
        // Containers).
        if !op.region.content.staff_instances().is_empty()
            || !op.region.content.barline_alignment_groups().is_empty()
            || !op.region.content.graphic_objects().is_empty()
        {
            return container_not_empty();
        }
        self.graph_create_region(&op.region);
        self.mint_container(env, robj);
        self.region_instances.entry(op.region_id()).or_default();
        if op.region.content.staff_based().is_some() {
            self.staff_based_regions.insert(op.region_id());
        }
        OperationEffect::Applied
    }

    fn create_staff_instance(
        &mut self,
        env: &OperationEnvelope,
        op: &CreateStaffInstanceOp,
    ) -> OperationEffect {
        if !matches!(
            self.objects.get(&TypedObjectId::Region(op.region)),
            Some(ObjectState::Live)
        ) {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            };
        }
        let iobj = TypedObjectId::StaffInstance(op.instance_id());
        if let Some(effect) = self.mint_precondition(iobj) {
            return effect;
        }
        // Reject a carried staff instance bearing any typed child object — a voice
        // or a measure (the two object collections it can hold).
        if !op.instance.voices.is_empty() || !op.instance.measures.is_empty() {
            return container_not_empty();
        }
        self.graph_create_staff_instance(op.region, &op.instance);
        self.mint_container(env, iobj);
        self.region_instances
            .entry(op.region)
            .or_default()
            .insert(op.instance_id());
        self.instance_voices.entry(op.instance_id()).or_default();
        OperationEffect::Applied
    }

    fn create_voice(&mut self, env: &OperationEnvelope, op: &CreateVoiceOp) -> OperationEffect {
        if !matches!(
            self.objects
                .get(&TypedObjectId::StaffInstance(op.staff_instance)),
            Some(ObjectState::Live)
        ) {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            };
        }
        let vobj = TypedObjectId::Voice(op.voice_id());
        if let Some(effect) = self.mint_precondition(vobj) {
            return effect;
        }
        if !op.voice.events.is_empty() {
            return container_not_empty();
        }
        self.graph_create_voice(op.staff_instance, &op.voice);
        self.mint_container(env, vobj);
        self.instance_voices
            .entry(op.staff_instance)
            .or_default()
            .insert(op.voice_id());
        OperationEffect::Applied
    }

    /// `Some(effect)` when `obj` cannot be deleted (missing, idempotent
    /// re-delete, or non-empty); `None` with the resolved minter when the
    /// empty-only delete may proceed.
    fn delete_precondition(
        &self,
        obj: TypedObjectId,
        env: &OperationEnvelope,
        has_live_children: bool,
    ) -> Result<OperationId, OperationEffect> {
        match self.objects.get(&obj) {
            None => Err(OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            }),
            Some(ObjectState::Tombstoned { .. }) => Err(OperationEffect::NoOp {
                reason: NoOpReason::AlreadyApplied,
            }),
            Some(ObjectState::Live) if has_live_children => Err(OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::ContainerNotEmpty,
                },
            }),
            Some(ObjectState::Live) => Ok(self.minted_by.get(&obj).copied().unwrap_or(env.id)),
        }
    }

    fn delete_region(&mut self, env: &OperationEnvelope, op: &DeleteRegionOp) -> OperationEffect {
        let robj = TypedObjectId::Region(op.region);
        let has_instances = self
            .region_instances
            .get(&op.region)
            .is_some_and(|s| !s.is_empty());
        let minted_by = match self.delete_precondition(robj, env, has_instances) {
            Ok(minter) => minter,
            Err(effect) => return effect,
        };
        self.objects.insert(
            robj,
            ObjectState::Tombstoned {
                deleted_by: env.id,
                minted_by,
            },
        );
        self.region_instances.remove(&op.region);
        self.staff_based_regions.remove(&op.region);
        self.graph_delete_region(op.region);
        OperationEffect::Applied
    }

    fn delete_staff_instance(
        &mut self,
        env: &OperationEnvelope,
        op: &DeleteStaffInstanceOp,
    ) -> OperationEffect {
        let iobj = TypedObjectId::StaffInstance(op.staff_instance);
        let has_voices = self
            .instance_voices
            .get(&op.staff_instance)
            .is_some_and(|s| !s.is_empty());
        let minted_by = match self.delete_precondition(iobj, env, has_voices) {
            Ok(minter) => minter,
            Err(effect) => return effect,
        };
        self.objects.insert(
            iobj,
            ObjectState::Tombstoned {
                deleted_by: env.id,
                minted_by,
            },
        );
        self.instance_voices.remove(&op.staff_instance);
        for set in self.region_instances.values_mut() {
            set.remove(&op.staff_instance);
        }
        self.graph_delete_staff_instance(op.staff_instance);
        OperationEffect::Applied
    }

    fn delete_voice(&mut self, env: &OperationEnvelope, op: &DeleteVoiceOp) -> OperationEffect {
        let vobj = TypedObjectId::Voice(op.voice);
        let has_events = self
            .voice_occupancy
            .get(&op.voice)
            .is_some_and(|e| !e.is_empty());
        let minted_by = match self.delete_precondition(vobj, env, has_events) {
            Ok(minter) => minter,
            Err(effect) => return effect,
        };
        self.objects.insert(
            vobj,
            ObjectState::Tombstoned {
                deleted_by: env.id,
                minted_by,
            },
        );
        self.voice_occupancy.remove(&op.voice);
        for set in self.instance_voices.values_mut() {
            set.remove(&op.voice);
        }
        self.graph_delete_voice(op.voice);
        OperationEffect::Applied
    }

    // --- Group 3 graph mutations (reduce_onto only; no-op when graph is None). --

    fn graph_create_region(&mut self, region: &epiphany_core::Region) {
        if let Some(score) = self.graph.as_mut() {
            score.canvas.regions.push(region.clone());
        }
    }

    fn graph_create_staff_instance(&mut self, region: RegionId, instance: &StaffInstance) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        if let Some(region) = score.canvas.regions.iter_mut().find(|r| r.id == region) {
            let staff = instance.staff;
            if let Some(instances) = region.content.staff_instances_mut() {
                instances.push(instance.clone());
            }
            // Keep the region's staff extent listing exactly its manifested
            // staves (Chapter 5 RegionExtents).
            if !region.staff_extent.staves.contains(&staff) {
                region.staff_extent.staves.push(staff);
            }
        }
    }

    fn graph_create_voice(&mut self, staff_instance: StaffInstanceId, voice: &Voice) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        for region in &mut score.canvas.regions {
            if let Some(instances) = region.content.staff_instances_mut() {
                if let Some(instance) = instances.iter_mut().find(|i| i.id == staff_instance) {
                    instance.voices.push(voice.clone());
                    return;
                }
            }
        }
    }

    fn graph_delete_region(&mut self, region: RegionId) {
        if let Some(score) = self.graph.as_mut() {
            score.canvas.regions.retain(|r| r.id != region);
        }
    }

    fn graph_delete_staff_instance(&mut self, staff_instance: StaffInstanceId) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        for region in &mut score.canvas.regions {
            let Some(instances) = region.content.staff_instances_mut() else {
                continue;
            };
            let Some(removed_staff) = instances
                .iter()
                .find(|i| i.id == staff_instance)
                .map(|i| i.staff)
            else {
                continue;
            };
            instances.retain(|i| i.id != staff_instance);
            // Drop the staff from the extent if no remaining instance manifests
            // it (Chapter 5 RegionExtents).
            let still_used = instances.iter().any(|i| i.staff == removed_staff);
            if !still_used {
                region.staff_extent.staves.retain(|s| *s != removed_staff);
            }
        }
    }

    fn graph_delete_voice(&mut self, voice: VoiceId) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        for region in &mut score.canvas.regions {
            if let Some(instances) = region.content.staff_instances_mut() {
                for instance in instances {
                    instance.voices.retain(|v| v.id != voice);
                }
            }
        }
    }

    fn change_region_time_model(
        &mut self,
        env: &OperationEnvelope,
        op: &crate::payload::ChangeRegionTimeModelOp,
    ) -> OperationEffect {
        if let Some(winner) = self.region_migrator.get(&op.region).copied() {
            // Concurrent same-target migrations conflict. A causally-later
            // migration is an intentional second structural change and is
            // evaluated against the graph produced by the first.
            if !self.concurrent(env.id, winner) {
                self.migrated_regions.insert(op.region);
            } else {
                let conflict = ConflictRecord::new(
                    ConflictKind::StructuralFieldCollision {
                        winner,
                        loser: env.id,
                        field: FieldPath("time_model".to_string()),
                    },
                    vec![env.id, winner],
                    vec![TypedObjectId::Region(op.region)],
                );
                let cid = conflict.id;
                self.conflicts.insert(conflict);
                return OperationEffect::Conflicted { conflict: cid };
            }
        }
        let mut incompatible_events: BTreeSet<EventId> =
            op.declared_incompatible.iter().copied().collect();
        let mut graph_region_index = None;
        if let Some(score) = self.graph.as_ref() {
            let Some(region_index) = score
                .canvas
                .regions
                .iter()
                .position(|region| region.id == op.region)
            else {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                };
            };
            graph_region_index = Some(region_index);
            let region = &score.canvas.regions[region_index];
            let event_ids: Vec<EventId> = region
                .staff_instances()
                .iter()
                .flat_map(|instance| &instance.voices)
                .flat_map(|voice| voice.events.iter().copied())
                .collect();

            for event_id in &event_ids {
                let Some(event) = score.events.get(*event_id) else {
                    incompatible_events.insert(*event_id);
                    continue;
                };
                let compatible = match &op.new_time_model {
                    RegionTimeModel::Metric(_) => matches!(
                        (event.position(), event.duration()),
                        (EventPosition::Musical(_), EventDuration::Musical(_))
                    ),
                    RegionTimeModel::Proportional(_) => matches!(
                        (event.position(), event.duration()),
                        (EventPosition::WallClock(_), EventDuration::WallClock(_))
                    ),
                    RegionTimeModel::Aleatoric(_) => true,
                };
                if !compatible {
                    incompatible_events.insert(*event_id);
                }
            }

            if let crate::payload::PositionRemapping::Reassign(remapping) = &op.remapping {
                let mapped: BTreeSet<EventId> = remapping.iter().map(|(event, _)| *event).collect();
                incompatible_events.extend(
                    event_ids
                        .iter()
                        .filter(|event| !mapped.contains(event))
                        .copied(),
                );
                if matches!(op.new_time_model, RegionTimeModel::Proportional(_)) {
                    // Reassign carries musical positions in the current
                    // prototype schema, so it cannot satisfy a proportional
                    // region's wall-clock coordinate discipline.
                    incompatible_events.extend(event_ids);
                }
            }
        }

        if !incompatible_events.is_empty() {
            let incompatible: Vec<TypedObjectId> = incompatible_events
                .into_iter()
                .map(TypedObjectId::Event)
                .collect();
            let mut affected = vec![TypedObjectId::Region(op.region)];
            affected.extend(incompatible.iter().copied());
            let conflict = ConflictRecord::new(
                ConflictKind::TimeModelMigrationFailure {
                    region: op.region,
                    incompatible_events: incompatible,
                },
                vec![env.id],
                affected,
            );
            let cid = conflict.id;
            self.conflicts.insert(conflict);
            return OperationEffect::Conflicted { conflict: cid };
        }

        if let Some(region_index) = graph_region_index {
            let score = self
                .graph
                .as_mut()
                .expect("a graph region index implies graph-aware reduction");
            if let crate::payload::PositionRemapping::Reassign(remapping) = &op.remapping {
                for (event, position) in remapping {
                    if let Some(value) = score.events.get_mut(*event) {
                        value.set_position(EventPosition::Musical(position.clone()));
                    }
                    for placements in self.voice_occupancy.values_mut() {
                        if let Some((stored_position, _, _)) = placements
                            .iter_mut()
                            .find(|(_, _, stored_event)| stored_event == event)
                        {
                            *stored_position = position.clone();
                        }
                    }
                }
            }
            // The value-typed payload (v1) carries the real target model, so the
            // region adopts it directly rather than rebuilding a default from a
            // discriminator tag.
            let region = &mut score.canvas.regions[region_index];
            region.time_model = op.new_time_model.clone();
        }
        self.migrated_regions.insert(op.region);
        self.region_migrator.insert(op.region, env.id);
        OperationEffect::Applied
    }

    fn resolve_conflict(
        &mut self,
        env: &OperationEnvelope,
        op: &crate::payload::ResolveConflictPayload,
    ) -> OperationEffect {
        use crate::conflict::ConflictResolutionState as RS;
        let existing_state = self
            .conflicts
            .get_mut(op.target)
            .map(|r| r.resolution_state);
        match existing_state {
            None => OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            },
            Some(RS::Unresolved) => {
                if let Some(rec) = self.conflicts.get_mut(op.target) {
                    // ResolutionAction::Dismiss selects the Dismissed state;
                    // every other action selects Resolved with that action
                    // applied (Pass 11, item 2.5).
                    rec.resolution_state = match op.action {
                        ResolutionAction::Dismiss => RS::Dismissed { by: env.id },
                        action => RS::Resolved { by: env.id, action },
                    };
                }
                OperationEffect::Applied
            }
            Some(RS::Resolved { action, .. }) => {
                if action == op.action {
                    OperationEffect::NoOp {
                        reason: NoOpReason::AlreadyApplied,
                    }
                } else {
                    // Differing concurrent resolution → meta-conflict.
                    let conflict = ConflictRecord::new(
                        ConflictKind::StructuralFieldCollision {
                            winner: env.id,
                            loser: env.id,
                            field: FieldPath("conflict_resolution".to_string()),
                        },
                        vec![env.id],
                        vec![],
                    );
                    let cid = conflict.id;
                    self.conflicts.insert(conflict);
                    OperationEffect::Conflicted { conflict: cid }
                }
            }
            Some(RS::Dismissed { .. }) => OperationEffect::NoOp {
                reason: NoOpReason::AlreadyApplied,
            },
        }
    }

    fn undo_transaction(
        &mut self,
        env: &OperationEnvelope,
        op: &UndoTransactionPayload,
    ) -> OperationEffect {
        let targets = self.tx_minted.get(&op.target).cloned().unwrap_or_default();
        if targets.is_empty() {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            };
        }
        let all_live = targets
            .iter()
            .all(|t| matches!(self.objects.get(t), Some(ObjectState::Live)));
        match op.policy {
            UndoPolicy::StrictInverse | UndoPolicy::Cascade => {
                if all_live {
                    let mut repairs = Vec::new();
                    for t in &targets {
                        let minter = self.minted_by.get(t).copied().unwrap_or(env.id);
                        self.objects.insert(
                            *t,
                            ObjectState::Tombstoned {
                                deleted_by: env.id,
                                minted_by: minter,
                            },
                        );
                        repairs.push(RepairRecord {
                            kind: RepairKind::CascadeDeleted,
                            target: *t,
                        });
                    }
                    self.materialize_graph_tombstones(&targets);
                    OperationEffect::AppliedWithRepair { repairs }
                } else {
                    // A target was already tombstoned/modified: strict undo conflicts.
                    let stuck = targets
                        .iter()
                        .find(|t| !matches!(self.objects.get(t), Some(ObjectState::Live)))
                        .copied()
                        .unwrap_or(targets[0]);
                    let conflict = ConflictRecord::new(
                        ConflictKind::TombstonedTarget {
                            target: stuck,
                            operation: env.id,
                        },
                        vec![env.id],
                        vec![stuck],
                    );
                    let cid = conflict.id;
                    self.conflicts.insert(conflict);
                    OperationEffect::Conflicted { conflict: cid }
                }
            }
            UndoPolicy::BestEffort => {
                let mut repairs = Vec::new();
                let mut tombstoned = Vec::new();
                for t in &targets {
                    if matches!(self.objects.get(t), Some(ObjectState::Live)) {
                        let minter = self.minted_by.get(t).copied().unwrap_or(env.id);
                        self.objects.insert(
                            *t,
                            ObjectState::Tombstoned {
                                deleted_by: env.id,
                                minted_by: minter,
                            },
                        );
                        repairs.push(RepairRecord {
                            kind: RepairKind::CascadeDeleted,
                            target: *t,
                        });
                        tombstoned.push(*t);
                    }
                }
                self.materialize_graph_tombstones(&tombstoned);
                OperationEffect::AppliedWithRepair { repairs }
            }
        }
    }

    // --- Group 1 (M2): event & pitch leaf-field ops. ------------------------
    //
    // The modify/transpose ops follow respell's field-overwrite discipline but
    // do NOT store the resolved value in MaterializedState (an Event/Pitch is a
    // graph object, not a bookkeeping-owned annotation like a spelling): their
    // canonical footprint is the effect-log entry plus, on a concurrent differing
    // write, a StructuralFieldCollision. The resolved value is materialized in the
    // graph by reduce_onto. The LWW key/diff uses `last_*_modify` working state.

    fn modify_event(&mut self, env: &OperationEnvelope, op: &ModifyEventOp) -> OperationEffect {
        let event_id = op.event_id();
        let ev_obj = TypedObjectId::Event(event_id);
        match self.objects.get(&ev_obj) {
            None => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::TargetTombstoned,
                }
            }
            Some(ObjectState::Live) => {}
        }
        // A `ModifyEvent` that moves a metric event's span (a trim or move) is now
        // materialized, but it must keep invariant 3 (`VoiceEventsSortedNonOverlap`):
        // refuse a move onto another live event in the voice, or one with a
        // non-positive span, rather than skip it silently (which would log a clean op
        // that never took effect). The verdict reads `voice_occupancy`, the
        // graph-independent index, so `reduce()` and `reduce_onto()` agree on it.
        let placement = self.metric_placement_verdict(&op.event);
        if matches!(placement, PlacementVerdict::Refused) {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::EventDurationInvalid,
                },
            };
        }
        let prev = self
            .last_event_modify
            .get(&event_id)
            .map(|(o, e)| (*o, e.clone()));
        let effect = match prev {
            Some((prev_op, prev_event)) if self.concurrent(env.id, prev_op) => {
                if prev_event == op.event {
                    return OperationEffect::NoOp {
                        reason: NoOpReason::AlreadyApplied,
                    };
                }
                // Later in canonical order wins and materializes; the earlier op
                // is the loser. (Winner carries the Conflicted tag; see DECISIONS.)
                let conflict = ConflictRecord::new(
                    ConflictKind::StructuralFieldCollision {
                        winner: env.id,
                        loser: prev_op,
                        field: FieldPath("event".to_string()),
                    },
                    vec![env.id, prev_op],
                    vec![ev_obj],
                );
                let cid = conflict.id;
                self.conflicts.insert(conflict);
                OperationEffect::Conflicted { conflict: cid }
            }
            // First modify, or a causally-ordered intentional overwrite.
            _ => OperationEffect::Applied,
        };
        self.last_event_modify
            .insert(event_id, (env.id, op.event.clone()));
        // Materialize a move only when it is a sanctioned metric move (`Moved`) *and*
        // the replacement is well-formed — so the graph and the occupancy index move
        // together. A malformed (empty) pitched replacement is not materialized in the
        // graph (`graph_replace_event` skips it), so it must not move occupancy either.
        let materialize_move = matches!(placement, PlacementVerdict::Moved { .. })
            && !matches!(&op.event, Event::Pitched(pe) if !pe.is_well_formed());
        self.graph_replace_event(&op.event, materialize_move);
        // Keep the voice-occupancy index in step with a materialized move, so a later
        // insert sees the freed/changed span (the same index its overlap check reads).
        if materialize_move {
            if let PlacementVerdict::Moved {
                voice,
                position,
                duration,
            } = placement
            {
                if let Some(events) = self.voice_occupancy.get_mut(&voice) {
                    for slot in events.iter_mut().filter(|slot| slot.2 == event_id) {
                        slot.0 = position.clone();
                        slot.1 = duration.clone();
                    }
                }
            }
        }
        effect
    }

    /// The verdict on a [`ModifyEvent`](OperationKind::ModifyEvent)'s placement: does
    /// it move the event's metric span, and if so does the move keep invariant 3
    /// (`VoiceEventsSortedNonOverlap`)? Read from `voice_occupancy` — the canonical,
    /// graph-independent placement index — so the verdict is identical with or without
    /// a base graph.
    fn metric_placement_verdict(&self, new_event: &Event) -> PlacementVerdict {
        let event_id = new_event.id();
        let Some((voice, current_position, current_duration)) =
            self.voice_occupancy.iter().find_map(|(voice, events)| {
                events
                    .iter()
                    .find(|(_, _, event)| *event == event_id)
                    .map(|(position, duration, _)| (*voice, position.clone(), duration.clone()))
            })
        else {
            // The event has no metric occupancy entry (untracked or non-metric):
            // nothing to materialize or refuse here.
            return PlacementVerdict::Unchanged;
        };
        let (EventPosition::Musical(new_position), EventDuration::Musical(new_duration)) =
            (new_event.position(), new_event.duration())
        else {
            // A non-metric placement is left deferred, neither moved nor refused.
            return PlacementVerdict::Unchanged;
        };
        if *new_position == current_position && *new_duration == current_duration {
            return PlacementVerdict::Unchanged;
        }
        if !new_duration.is_positive() {
            return PlacementVerdict::Refused;
        }
        let overlaps = self.voice_occupancy.get(&voice).is_some_and(|events| {
            events.iter().any(|(position, duration, event)| {
                *event != event_id
                    && intervals_overlap(new_position, new_duration, position, duration)
            })
        });
        if overlaps {
            PlacementVerdict::Refused
        } else {
            PlacementVerdict::Moved {
                voice,
                position: new_position.clone(),
                duration: new_duration.clone(),
            }
        }
    }

    /// Re-sorts `voice`'s graph event list by ascending position (id-tiebroken), the
    /// same order an insert maintains — run after a materialized placement change so
    /// the voice stays sorted (invariant 3). A no-op when the graph is absent.
    fn resort_voice(&mut self, voice: VoiceId) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        let Some((region_index, instance_index, voice_index)) = graph_voice_location(score, voice)
        else {
            return;
        };
        let mut ordered = score.canvas.regions[region_index].staff_instances()[instance_index]
            .voices[voice_index]
            .events
            .clone();
        ordered.sort_by(|a, b| {
            let a_position = score.events.get(*a).map(Event::position);
            let b_position = score.events.get(*b).map(Event::position);
            match (a_position, b_position) {
                (
                    Some(EventPosition::Musical(a_position)),
                    Some(EventPosition::Musical(b_position)),
                ) => a_position.cmp(b_position).then_with(|| a.cmp(b)),
                _ => a.cmp(b),
            }
        });
        score.canvas.regions[region_index]
            .content
            .staff_instances_mut()
            .expect("the voice was located in a staff-based instance")[instance_index]
            .voices[voice_index]
            .events = ordered;
    }

    fn transpose(&mut self, _env: &OperationEnvelope, op: &TransposeOp) -> OperationEffect {
        // Precondition: every target pitch is live. Transpose is order-dependent
        // (transpositions do not commute); its canonical footprint is the
        // effect-log entry. The transposed values are materialized in the graph.
        for pitch in &op.targets {
            match self.objects.get(&TypedObjectId::Pitch(*pitch)) {
                Some(ObjectState::Live) => {}
                Some(ObjectState::Tombstoned { .. }) => {
                    return OperationEffect::NoOp {
                        reason: NoOpReason::TargetTombstoned,
                    }
                }
                None => {
                    return OperationEffect::NoOp {
                        reason: NoOpReason::PreconditionFailedUnderReduction {
                            reason: PreconditionFailureReason::TargetMissing,
                        },
                    }
                }
            }
        }
        for pitch in &op.targets {
            self.graph_transpose_pitch(*pitch, op.chromatic_steps);
        }
        OperationEffect::Applied
    }

    fn insert_identified_pitch(
        &mut self,
        env: &OperationEnvelope,
        op: &InsertIdentifiedPitchOp,
    ) -> OperationEffect {
        let pitch_id = op.pitch_id();
        let p_obj = TypedObjectId::Pitch(pitch_id);
        // The target event must be live; the pitch id must be fresh.
        if !matches!(
            self.objects.get(&TypedObjectId::Event(op.event)),
            Some(ObjectState::Live)
        ) {
            return OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction {
                    reason: PreconditionFailureReason::TargetMissing,
                },
            };
        }
        match self.objects.get(&p_obj) {
            Some(ObjectState::Live) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::AlreadyApplied,
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::TargetTombstoned,
                }
            }
            None => {}
        }
        self.objects.insert(p_obj, ObjectState::Live);
        self.minted_by.insert(p_obj, env.id);
        self.note_minted(env, p_obj);
        self.event_pitches
            .entry(op.event)
            .or_default()
            .push(pitch_id);
        self.graph_insert_pitch(op.event, &op.pitch);
        OperationEffect::Applied
    }

    fn delete_identified_pitch(
        &mut self,
        env: &OperationEnvelope,
        op: &DeleteIdentifiedPitchOp,
    ) -> OperationEffect {
        let p_obj = TypedObjectId::Pitch(op.pitch);
        let minted_by = match self.objects.get(&p_obj) {
            None => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::AlreadyApplied,
                }
            }
            Some(ObjectState::Live) => self.minted_by.get(&p_obj).copied().unwrap_or(env.id),
        };
        self.objects.insert(
            p_obj,
            ObjectState::Tombstoned {
                deleted_by: env.id,
                minted_by,
            },
        );
        for pitches in self.event_pitches.values_mut() {
            pitches.retain(|p| *p != op.pitch);
        }
        self.graph_delete_pitch(op.pitch);
        OperationEffect::Applied
    }

    fn modify_identified_pitch(
        &mut self,
        env: &OperationEnvelope,
        op: &ModifyIdentifiedPitchOp,
    ) -> OperationEffect {
        let p_obj = TypedObjectId::Pitch(op.pitch);
        match self.objects.get(&p_obj) {
            None => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::TargetMissing,
                    },
                }
            }
            Some(ObjectState::Tombstoned { .. }) => {
                return OperationEffect::NoOp {
                    reason: NoOpReason::TargetTombstoned,
                }
            }
            Some(ObjectState::Live) => {}
        }
        let prev = self
            .last_pitch_modify
            .get(&op.pitch)
            .map(|(o, v)| (*o, v.clone()));
        let effect = match prev {
            Some((prev_op, prev_value)) if self.concurrent(env.id, prev_op) => {
                if prev_value == op.value {
                    return OperationEffect::NoOp {
                        reason: NoOpReason::AlreadyApplied,
                    };
                }
                let conflict = ConflictRecord::new(
                    ConflictKind::StructuralFieldCollision {
                        winner: env.id,
                        loser: prev_op,
                        field: FieldPath("pitch".to_string()),
                    },
                    vec![env.id, prev_op],
                    vec![p_obj],
                );
                let cid = conflict.id;
                self.conflicts.insert(conflict);
                OperationEffect::Conflicted { conflict: cid }
            }
            _ => OperationEffect::Applied,
        };
        self.last_pitch_modify
            .insert(op.pitch, (env.id, op.value.clone()));
        self.graph_modify_pitch(op.pitch, &op.value);
        effect
    }

    // --- Group 1 graph mutations (reduce_onto only; no-op when graph is None). --

    /// Applies a `ModifyEvent`'s value to the graph. `materialize_move` is the caller's
    /// sanction (from [`Self::metric_placement_verdict`]) that a *placement* change is a
    /// valid metric move and should be applied + the voice re-sorted; when it is false a
    /// placement change is deferred (a non-metric move, a no-occupancy event, or a
    /// refused move), leaving only same-placement field edits to apply. Keeping
    /// materialization gated on this single sanction is what holds the graph and the
    /// `voice_occupancy` index in agreement.
    fn graph_replace_event(&mut self, new_event: &Event, materialize_move: bool) {
        let placement_changed;
        let voice;
        {
            let Some(score) = self.graph.as_mut() else {
                return;
            };
            // A ModifyEvent carrying a malformed (empty) pitched event must not
            // corrupt the arena: `get_mut` bypasses `insert`'s well-formedness guard,
            // so an empty chord would only be caught later by `check_invariants`.
            // Skip the graph replace in that case (bookkeeping still records it).
            if let Event::Pitched(pe) = new_event {
                if !pe.is_well_formed() {
                    return;
                }
            }
            let Some(existing) = score.events.get_mut(new_event.id()) else {
                return;
            };
            placement_changed = new_event.position() != existing.position()
                || new_event.duration() != existing.duration();
            // A placement change is materialized (and the voice re-sorted below) only
            // when the caller sanctioned it as a valid metric move; otherwise it is
            // deferred, leaving the LWW bookkeeping to record the modify. Same-placement
            // field edits always apply, preserving the original voice membership.
            if placement_changed && !materialize_move {
                return;
            }
            voice = existing.voice();
            let mut replacement = new_event.clone();
            replacement.set_voice(voice);
            *existing = replacement;
        }
        if placement_changed {
            self.resort_voice(voice);
        }
    }

    fn graph_event_of_pitch(score: &Score, pitch: PitchId) -> Option<EventId> {
        score.events.iter().find_map(|event| {
            let mut ips = Vec::new();
            event.collect_identified_pitches(&mut ips);
            ips.iter().any(|ip| ip.id == pitch).then(|| event.id())
        })
    }

    fn graph_insert_pitch(&mut self, event: EventId, pitch: &epiphany_core::IdentifiedPitch) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        let Some(slot) = score.events.get_mut(event) else {
            return;
        };
        if let Event::Pitched(pe) = slot {
            if !pe.pitches.iter().any(|ip| ip.id == pitch.id) {
                pe.pitches.push(pitch.clone());
            }
            return;
        }
        // Adding a pitch to a rest turns the rest into a note — the dual of a
        // last-pitch delete (below). Without this, the bookkeeping mints the
        // pitch live while the graph silently drops it (a non-pitched slot has
        // no pitch list), so the two would diverge.
        if let Event::Rest(rest) = slot {
            let replacement = epiphany_core::PitchedEvent {
                id: rest.id,
                voice: rest.voice,
                position: rest.position.clone(),
                duration: rest.duration.clone(),
                pitches: vec![pitch.clone()],
                articulations: Vec::new(),
                dynamic: None,
                ornaments: Vec::new(),
                stem: epiphany_core::StemConfiguration,
                grace: None,
            };
            *slot = Event::Pitched(replacement);
        }
    }

    fn graph_delete_pitch(&mut self, pitch: PitchId) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        // Drop any user-chosen respell override for the deleted pitch (the dual
        // of `graph_respell_pitch`), so no spelling attachment is left targeting a
        // pitch that is no longer present (Chapter 5 SpellingScopeResolves). The
        // pitch is not added to `tombstoned_pitches` here: unlike a whole-event
        // delete the event survives, and a later ModifyEvent may legitimately
        // reintroduce the id — tombstoning it would make it both live and
        // tombstoned (invariant 11).
        score.spelling_attachments.retain(|a| {
            !(a.layer.is_none()
                && matches!(a.source, SpellingSource::UserChosen)
                && matches!(&a.scope, SpellingScope::Pitch(p) if *p == pitch)
                && matches!(a.directive, SpellingDirective::Explicit(_)))
        });
        let Some(event) = Self::graph_event_of_pitch(score, pitch) else {
            return;
        };
        let Some(slot) = score.events.get_mut(event) else {
            return;
        };
        if let Event::Pitched(pe) = slot {
            if pe.pitches.iter().filter(|ip| ip.id != pitch).count() == 0 {
                // Removing the last pitch would leave an empty (invalid) pitched
                // event; Chapter 5 forbids that ("use Rest for the no-pitch
                // case"), so the note degrades to a rest of the same placement
                // and duration. Keeps `get_mut` from materializing a malformed
                // chord that `check_invariants` would later reject.
                let rest = epiphany_core::Rest {
                    id: pe.id,
                    voice: pe.voice,
                    position: pe.position.clone(),
                    duration: pe.duration.clone(),
                    vertical_position: None,
                    visible: true,
                };
                *slot = Event::Rest(rest);
            } else {
                pe.pitches.retain(|ip| ip.id != pitch);
            }
        }
    }

    fn graph_modify_pitch(&mut self, pitch: PitchId, value: &Pitch) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        let Some(event) = Self::graph_event_of_pitch(score, pitch) else {
            return;
        };
        if let Some(Event::Pitched(pe)) = score.events.get_mut(event) {
            if let Some(ip) = pe.pitches.iter_mut().find(|ip| ip.id == pitch) {
                ip.pitch = value.clone();
            }
        }
    }

    fn graph_transpose_pitch(&mut self, pitch: PitchId, chromatic_steps: i32) {
        let Some(score) = self.graph.as_mut() else {
            return;
        };
        let Some(event) = Self::graph_event_of_pitch(score, pitch) else {
            return;
        };
        if let Some(Event::Pitched(pe)) = score.events.get_mut(event) {
            if let Some(ip) = pe.pitches.iter_mut().find(|ip| ip.id == pitch) {
                // Minimal interval: shift the CMN alteration, saturating at the
                // `i8` bound (a lossy stand-in — an extreme transpose clamps
                // rather than renormalizing nominal/octave). Full interval
                // algebra (Chapter 4 tuning) is deferred — P12-K2.
                if let epiphany_core::PitchSpacePosition::Cmn { alteration, .. } =
                    &mut ip.pitch.scale_position.position
                {
                    let shifted = (*alteration as i32).saturating_add(chromatic_steps);
                    *alteration = shifted.clamp(i8::MIN as i32, i8::MAX as i32) as i8;
                }
            }
        }
    }

    // --- Re-anchoring (Chapter 6 §6.5 rule table, representative subset). ----

    fn reanchor_for_tombstone(
        &mut self,
        env: &OperationEnvelope,
        tombstoned: TypedObjectId,
        repairs: &mut Vec<RepairRecord>,
    ) {
        // Find structures referencing the tombstoned object.
        let referencing: Vec<TypedObjectId> = self
            .structures
            .iter()
            .filter(|(_, endpoints)| endpoints.contains(&tombstoned))
            .map(|(sid, _)| *sid)
            .collect();
        for sid in referencing {
            match sid {
                TypedObjectId::Tie(_) => {
                    // A tie's existence requires both endpoints: cascade-delete.
                    self.cascade_structure(env, sid, repairs);
                }
                TypedObjectId::Comment(_) | TypedObjectId::AnalyticalAnnotation(_) => {
                    // User content is never silently deleted: orphan.
                    repairs.push(RepairRecord {
                        kind: RepairKind::Orphaned,
                        target: sid,
                    });
                }
                TypedObjectId::Beam(_) => {
                    let survivors = self.surviving_endpoints(sid, tombstoned);
                    if survivors < 2 {
                        self.cascade_structure(env, sid, repairs);
                    } else {
                        repairs.push(RepairRecord {
                            kind: RepairKind::SpannerTruncated {
                                removed_members: vec![tombstoned],
                            },
                            target: sid,
                        });
                    }
                }
                TypedObjectId::Slur(_) | TypedObjectId::Spanner(_) => {
                    let survivors = self.surviving_endpoints(sid, tombstoned);
                    if survivors < 1 {
                        self.cascade_structure(env, sid, repairs);
                    } else if let Some(to) = self.nearest_survivor(sid, tombstoned) {
                        repairs.push(RepairRecord {
                            kind: RepairKind::Reanchored {
                                from: tombstoned,
                                to,
                                reason: ReanchorReason::SameVoiceNearer,
                            },
                            target: sid,
                        });
                    } else {
                        self.cascade_structure(env, sid, repairs);
                    }
                }
                _ => {
                    repairs.push(RepairRecord {
                        kind: RepairKind::AttachmentTombstoned,
                        target: sid,
                    });
                }
            }
        }
    }

    fn cascade_structure(
        &mut self,
        env: &OperationEnvelope,
        sid: TypedObjectId,
        repairs: &mut Vec<RepairRecord>,
    ) {
        let minter = self.minted_by.get(&sid).copied().unwrap_or(env.id);
        self.objects.insert(
            sid,
            ObjectState::Tombstoned {
                deleted_by: env.id,
                minted_by: minter,
            },
        );
        repairs.push(RepairRecord {
            kind: RepairKind::CascadeDeleted,
            target: sid,
        });
    }

    fn surviving_endpoints(&self, sid: TypedObjectId, just_tombstoned: TypedObjectId) -> usize {
        self.structures
            .get(&sid)
            .map(|eps| {
                eps.iter()
                    .filter(|e| {
                        **e != just_tombstoned
                            && matches!(self.objects.get(*e), Some(ObjectState::Live))
                    })
                    .count()
            })
            .unwrap_or(0)
    }

    fn nearest_survivor(
        &self,
        sid: TypedObjectId,
        just_tombstoned: TypedObjectId,
    ) -> Option<TypedObjectId> {
        // Deterministic "nearest" stand-in: the lexicographically-smallest
        // surviving endpoint (the spec's full proximity ordering needs resolved
        // positions; see DECISIONS.md).
        self.structures.get(&sid).and_then(|eps| {
            eps.iter()
                .filter(|e| {
                    **e != just_tombstoned
                        && matches!(self.objects.get(*e), Some(ObjectState::Live))
                })
                .min()
                .copied()
        })
    }

    // --- Transactions (Chapter 6 §6.6). -------------------------------------

    fn reduce_transaction_block(&mut self, tx: TransactionId, members: &[&'a OperationEnvelope]) {
        let ordered = canonical_reduction_order(members);
        // Descriptor-precedence rule: the DeclareTransaction must be present and
        // causally precede every member.
        let desc = self.descriptors.get(&tx).copied();
        let well_formed = desc.is_some()
            && ordered
                .iter()
                .all(|m| m.causal_context.covers(desc.expect("checked is_some")));
        if !well_formed {
            let member_ids: Vec<OperationId> = ordered.iter().map(|m| m.id).collect();
            let conflict = ConflictRecord::new(
                ConflictKind::TransactionConflict {
                    transaction: tx,
                    failed_members: member_ids.clone(),
                },
                member_ids,
                vec![],
            );
            self.conflicts.insert(conflict);
            for m in &ordered {
                self.effects.push((
                    m.id,
                    OperationEffect::NoOp {
                        reason: NoOpReason::TransactionConflict,
                    },
                ));
            }
            return;
        }

        // Atomic: apply members against a snapshot; if any fails, roll back.
        let snapshot = self.snapshot();
        self.current_tx = Some(tx);
        let mut member_effects: Vec<(OperationId, OperationEffect)> = Vec::new();
        let mut failed_members: Vec<OperationId> = Vec::new();
        for m in &ordered {
            let eff = self.apply(m);
            if is_member_failure(&eff) {
                failed_members.push(m.id);
            }
            member_effects.push((m.id, eff));
        }
        self.current_tx = None;

        if failed_members.is_empty() {
            for (id, eff) in member_effects {
                self.effects.push((id, eff));
            }
        } else {
            self.restore(snapshot);
            let member_ids: Vec<OperationId> = ordered.iter().map(|m| m.id).collect();
            let conflict = ConflictRecord::new(
                ConflictKind::TransactionConflict {
                    transaction: tx,
                    failed_members,
                },
                member_ids,
                vec![],
            );
            self.conflicts.insert(conflict);
            for m in &ordered {
                self.effects.push((
                    m.id,
                    OperationEffect::NoOp {
                        reason: NoOpReason::TransactionConflict,
                    },
                ));
            }
        }
    }

    /// Records an object as minted by the operation's transaction (if any), so
    /// undo can compensate it later.
    fn note_minted(&mut self, _env: &OperationEnvelope, obj: TypedObjectId) {
        if let Some(tx) = self.current_tx {
            self.tx_minted.entry(tx).or_default().push(obj);
        }
    }

    fn concurrent(&self, a: OperationId, b: OperationId) -> bool {
        let a_ctx = self
            .env_of(a)
            .map(|e| e.causal_context.covers(b))
            .unwrap_or(false);
        let b_ctx = self
            .env_of(b)
            .map(|e| e.causal_context.covers(a))
            .unwrap_or(false);
        !a_ctx && !b_ctx
    }

    fn snapshot(&self) -> WorkingSnapshot {
        WorkingSnapshot {
            objects: self.objects.clone(),
            spellings: self.spellings.clone(),
            breaks: self.breaks.clone(),
            page_breaks: self.page_breaks.clone(),
            conflicts: self.conflicts.clone(),
            minted_by: self.minted_by.clone(),
            event_pitches: self.event_pitches.clone(),
            voice_occupancy: self.voice_occupancy.clone(),
            last_respell: self.last_respell.clone(),
            last_event_modify: self.last_event_modify.clone(),
            last_pitch_modify: self.last_pitch_modify.clone(),
            last_cross_cutting_modify: self.last_cross_cutting_modify.clone(),
            last_metric_grid: self.last_metric_grid.clone(),
            structures: self.structures.clone(),
            region_instances: self.region_instances.clone(),
            instance_voices: self.instance_voices.clone(),
            staff_based_regions: self.staff_based_regions.clone(),
            migrated_regions: self.migrated_regions.clone(),
            region_migrator: self.region_migrator.clone(),
            descriptors: self.descriptors.clone(),
            tx_minted: self.tx_minted.clone(),
            graph: self.graph.clone(),
        }
    }

    fn restore(&mut self, s: WorkingSnapshot) {
        self.objects = s.objects;
        self.spellings = s.spellings;
        self.breaks = s.breaks;
        self.page_breaks = s.page_breaks;
        self.conflicts = s.conflicts;
        self.minted_by = s.minted_by;
        self.event_pitches = s.event_pitches;
        self.voice_occupancy = s.voice_occupancy;
        self.last_respell = s.last_respell;
        self.last_event_modify = s.last_event_modify;
        self.last_pitch_modify = s.last_pitch_modify;
        self.last_cross_cutting_modify = s.last_cross_cutting_modify;
        self.last_metric_grid = s.last_metric_grid;
        self.structures = s.structures;
        self.region_instances = s.region_instances;
        self.instance_voices = s.instance_voices;
        self.staff_based_regions = s.staff_based_regions;
        self.migrated_regions = s.migrated_regions;
        self.region_migrator = s.region_migrator;
        self.descriptors = s.descriptors;
        self.tx_minted = s.tx_minted;
        self.graph = s.graph;
    }
}

/// Whether a transaction member's effect counts as an invariant-precondition
/// failure that conflicts the whole transaction (Chapter 6 §6.6).
fn is_member_failure(effect: &OperationEffect) -> bool {
    matches!(
        effect,
        OperationEffect::Conflicted { .. }
            | OperationEffect::NoOp {
                reason: NoOpReason::PreconditionFailedUnderReduction { .. }
            }
            | OperationEffect::NoOp {
                reason: NoOpReason::TargetTombstoned
            }
    )
}

/// The transaction an envelope is a *member* of: its `transaction` field, unless
/// it is the `DeclareTransaction` descriptor itself (which is not a member of
/// the transaction it declares).
fn member_transaction(env: &OperationEnvelope) -> Option<TransactionId> {
    let tx = env.transaction?;
    if let OperationPayload::Primitive(OperationKind::DeclareTransaction(desc)) = &env.payload {
        if desc.id == tx {
            return None;
        }
    }
    Some(tx)
}

/// Groups active operations into transaction blocks by membership.
fn transaction_members<'a>(
    active: &[&'a OperationEnvelope],
) -> BTreeMap<TransactionId, Vec<&'a OperationEnvelope>> {
    let mut map: BTreeMap<TransactionId, Vec<&'a OperationEnvelope>> = BTreeMap::new();
    for env in active {
        if let Some(tx) = member_transaction(env) {
            map.entry(tx).or_default().push(env);
        }
    }
    map
}

/// Computes which reducible operations are held pending under the
/// missing-causal-predecessor rule (Chapter 6 §6.5, §6.6), and why.
///
/// An operation is directly blocked if a causal predecessor is absent,
/// equivocated, or excluded; the block then propagates transitively to anything
/// that causally depends on a blocked operation. Ties between multiple blocking
/// causes are broken by smallest blocker `OperationId`, so the reason is
/// deterministic.
fn compute_pending(
    reducible: &[&OperationEnvelope],
    reducible_ids: &BTreeSet<OperationId>,
    equivocated: &BTreeSet<OperationId>,
    excluded: &BTreeSet<OperationId>,
    declared_transactions: &BTreeSet<TransactionId>,
) -> BTreeMap<OperationId, PendingReason> {
    let mut blocked: BTreeMap<OperationId, PendingReason> = BTreeMap::new();
    let known_ids: BTreeSet<OperationId> = reducible_ids
        .iter()
        .chain(equivocated)
        .chain(excluded)
        .copied()
        .collect();
    // Direct causes: a hole in an asserted contiguous vector range, a dot
    // referencing a non-reducible id, or coverage of a known bad id.
    for env in reducible {
        // An absent transaction descriptor has a more specific normative
        // outcome: TransactionConflict. Let transaction reduction report it
        // instead of masking it as an ordinary missing predecessor.
        if member_transaction(env).is_some_and(|tx| !declared_transactions.contains(&tx)) {
            continue;
        }
        let mut causes: Vec<(OperationId, PendingReason)> = Vec::new();
        if let Some(missing) = first_missing_vector_predecessor(env, &known_ids) {
            causes.push((missing, PendingReason::MissingCausalPredecessor { missing }));
        }
        for d in env.causal_context.dots() {
            if !reducible_ids.contains(&d) {
                let reason = if equivocated.contains(&d) {
                    PendingReason::DependsOnEquivocated { on: d }
                } else if excluded.contains(&d) {
                    PendingReason::DependsOnExcluded { on: d }
                } else {
                    PendingReason::MissingCausalPredecessor { missing: d }
                };
                causes.push((d, reason));
            }
        }
        for e in equivocated {
            if env.causal_context.covers(*e) {
                causes.push((*e, PendingReason::DependsOnEquivocated { on: *e }));
            }
        }
        for x in excluded {
            if env.causal_context.covers(*x) {
                causes.push((*x, PendingReason::DependsOnExcluded { on: *x }));
            }
        }
        if let Some((_, reason)) = causes.into_iter().min_by_key(|(id, _)| *id) {
            blocked.insert(env.id, reason);
        }
    }

    // Transitive propagation: an op covering a blocked op is itself blocked.
    loop {
        let mut changed = false;
        for env in reducible {
            if blocked.contains_key(&env.id) {
                continue;
            }
            // Smallest blocked id this op covers.
            let cover = blocked
                .keys()
                .copied()
                .find(|b| env.causal_context.covers(*b));
            if let Some(on) = cover {
                blocked.insert(env.id, PendingReason::DependsOnPending { on });
                changed = true;
            }
        }
        if !changed {
            break;
        }
    }

    blocked
}

/// Finds the smallest absent id asserted by a causal context's contiguous
/// vector ranges without expanding those ranges counter by counter.
fn first_missing_vector_predecessor(
    env: &OperationEnvelope,
    known_ids: &BTreeSet<OperationId>,
) -> Option<OperationId> {
    let mut first_missing = None;

    for (&replica, &high) in &env.causal_context.vector {
        let mut expected = 0_u64;
        let mut complete = false;

        for id in known_ids.range(OperationId::new(replica, 0)..=OperationId::new(replica, high)) {
            if id.counter > expected {
                break;
            }
            if id.counter == expected {
                if expected == high {
                    complete = true;
                    break;
                }
                expected += 1;
            }
        }

        if !complete {
            let candidate = OperationId::new(replica, expected);
            if first_missing.map_or(true, |current| candidate < current) {
                first_missing = Some(candidate);
            }
        }
    }

    first_missing
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::causal::CausalContext;
    use crate::stamp::{HybridLogicalClock, OperationStamp};
    use crate::support::AuthorId;
    use epiphany_core::{RationalTime, ReplicaId, StaffInstanceId, WallClockTime};

    fn pos(n: i64) -> MusicalPosition {
        MusicalPosition(RationalTime::from_int(n as i32))
    }

    fn insert(
        replica: u64,
        counter: u64,
        physical: i64,
        voice: u64,
        event: u64,
        pos_units: i64,
    ) -> OperationEnvelope {
        let id = OperationId::new(ReplicaId(replica), counter);
        OperationEnvelope {
            id,
            author: AuthorId(0),
            stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical), 0), id),
            causal_context: CausalContext::new(),
            transaction: None,
            payload: OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp {
                // Voice and staff instance live in a shared (author-independent)
                // namespace so two authors can target the same voice.
                staff_instance: StaffInstanceId::new(ReplicaId(9), 0),
                event: crate::valuegen::insert_event_value(
                    EventId::new(ReplicaId(replica), event),
                    VoiceId::new(ReplicaId(9), voice),
                    pos(pos_units),
                    epiphany_core::MusicalDuration::whole(),
                    &[],
                ),
            })),
        }
    }

    #[test]
    fn reduction_is_permutation_invariant() {
        let envs = vec![
            insert(1, 0, 10, 1, 100, 0),
            insert(1, 1, 20, 1, 101, 1),
            insert(2, 0, 15, 2, 200, 0),
        ];
        let mut forward = OperationSet::new();
        forward.accept_all(envs.clone());
        let mut backward = OperationSet::new();
        let mut rev = envs.clone();
        rev.reverse();
        backward.accept_all(rev);
        assert_eq!(
            forward.reduce().canonical_bytes(),
            backward.reduce().canonical_bytes()
        );
    }

    #[test]
    fn concurrent_same_position_insert_promotes_the_greater_id() {
        // Two concurrent inserts, same voice and position, different events.
        let a = insert(1, 0, 10, 7, 100, 5);
        let b = insert(2, 0, 10, 7, 200, 5);
        let mut set = OperationSet::new();
        set.accept_all(vec![a.clone(), b.clone()]);
        let state = set.reduce();
        // The greater OperationId (replica 2) is promoted to a system voice.
        let promoted = state
            .effects
            .iter()
            .find(|(id, _)| *id == b.id)
            .map(|(_, e)| e);
        assert!(matches!(
            promoted,
            Some(OperationEffect::AppliedWithRepair { .. })
        ));
        let kept = state
            .effects
            .iter()
            .find(|(id, _)| *id == a.id)
            .map(|(_, e)| e);
        assert_eq!(kept, Some(&OperationEffect::Applied));
    }

    #[test]
    fn resolve_conflict_with_dismiss_reaches_dismissed_state() {
        // Pass 11, item 2.5: an authored ResolveConflict whose action is
        // `Dismiss` must reach the `Dismissed` resolution state (previously
        // `Dismissed` was representable but unreachable by any authored op).
        use crate::conflict::{ConflictResolutionState, ResolutionAction};
        use crate::payload::{ResolveConflictPayload, RespellPitchOp};
        use epiphany_core::PitchId;

        let pitch = PitchId::new(ReplicaId(9), 500);

        // An InsertEvent carrying `pitch` makes the pitch Live.
        let mut insert_env = insert(1, 0, 10, 1, 100, 0);
        if let OperationPayload::Primitive(OperationKind::InsertEvent(ref mut op)) =
            insert_env.payload
        {
            op.event = crate::valuegen::insert_event_value(
                op.event_id(),
                op.voice(),
                op.musical_position(),
                op.musical_duration(),
                &[pitch],
            );
        }

        // Two concurrent, differing respellings of `pitch`, both causally after
        // the insert (so the pitch is Live) but concurrent with each other (so
        // they collide into a StructuralFieldCollision conflict).
        let respell = |replica: u64, counter: u64, physical: i64, byte: u8| {
            let id = OperationId::new(ReplicaId(replica), counter);
            OperationEnvelope {
                id,
                author: AuthorId(0),
                stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical), 0), id),
                causal_context: CausalContext::new().with_seen(ReplicaId(1), 0),
                transaction: None,
                payload: OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
                    pitch,
                    spelling: crate::valuegen::spelling(byte),
                })),
            }
        };
        let respell_a = respell(2, 0, 20, 0xAA);
        let respell_b = respell(3, 0, 21, 0xBB);

        // Phase 1: reduce to discover the content-derived conflict id.
        let mut set = OperationSet::new();
        set.accept_all(vec![
            insert_env.clone(),
            respell_a.clone(),
            respell_b.clone(),
        ]);
        let state = set.reduce();
        assert_eq!(
            state.conflicts.records().len(),
            1,
            "expected exactly one field-collision conflict"
        );
        let cid = state.conflicts.records()[0].id;
        assert_eq!(
            state.conflicts.records()[0].resolution_state,
            ConflictResolutionState::Unresolved
        );

        // Phase 2: author ResolveConflict { Dismiss } against that conflict,
        // causally after the colliding respells so it reduces against the
        // already-created conflict record.
        let resolve_id = OperationId::new(ReplicaId(4), 0);
        let resolve_env = OperationEnvelope {
            id: resolve_id,
            author: AuthorId(0),
            stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(30), 0), resolve_id),
            causal_context: CausalContext::new()
                .with_dot(respell_a.id)
                .with_dot(respell_b.id),
            transaction: None,
            payload: OperationPayload::ResolveConflict(ResolveConflictPayload {
                target: cid,
                action: ResolutionAction::Dismiss,
            }),
        };

        let mut set2 = OperationSet::new();
        set2.accept_all(vec![insert_env, respell_a, respell_b, resolve_env]);
        let state2 = set2.reduce();
        let rec = state2
            .conflicts
            .records()
            .iter()
            .find(|r| r.id == cid)
            .expect("conflict still present after resolution");
        assert_eq!(
            rec.resolution_state,
            ConflictResolutionState::Dismissed { by: resolve_id },
            "Dismiss action must select the Dismissed state"
        );
    }

    // --- Group 1 (M2) behavior. --------------------------------------------

    fn prim_env(
        replica: u64,
        counter: u64,
        physical: i64,
        ctx: CausalContext,
        kind: OperationKind,
    ) -> OperationEnvelope {
        let id = OperationId::new(ReplicaId(replica), counter);
        OperationEnvelope {
            id,
            author: AuthorId(0),
            stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical), 0), id),
            causal_context: ctx,
            transaction: None,
            payload: OperationPayload::Primitive(kind),
        }
    }

    fn modify_event_of(event: EventId, pitch: PitchId) -> OperationKind {
        OperationKind::ModifyEvent(crate::payload::ModifyEventOp {
            event: crate::valuegen::insert_event_value(
                event,
                VoiceId::new(ReplicaId(9), 1),
                pos(0),
                epiphany_core::MusicalDuration::whole(),
                &[pitch],
            ),
        })
    }

    #[test]
    fn concurrent_differing_modify_event_conflicts() {
        let event = EventId::new(ReplicaId(1), 100);
        let seen = CausalContext::new().with_seen(ReplicaId(1), 0);
        // Insert the event (replica 1), then two concurrent differing modifies.
        let insert = insert(1, 0, 10, 1, 100, 0);
        let mod_a = prim_env(
            2,
            0,
            20,
            seen.clone(),
            modify_event_of(event, PitchId::new(ReplicaId(1), 1)),
        );
        let mod_b = prim_env(
            3,
            0,
            20,
            seen,
            modify_event_of(event, PitchId::new(ReplicaId(1), 2)),
        );
        let mut set = OperationSet::new();
        set.accept_all(vec![insert, mod_a, mod_b]);
        let state = set.reduce();
        assert_eq!(
            state.conflicts.records().len(),
            1,
            "concurrent differing ModifyEvent must record exactly one conflict"
        );
        assert!(matches!(
            state.conflicts.records()[0].kind,
            ConflictKind::StructuralFieldCollision { .. }
        ));
    }

    // --- ModifyEvent placement changes (trim/move): the make-room enabler. ------

    fn musical(numerator: i64, denominator: i64) -> MusicalDuration {
        MusicalDuration(RationalTime::new(numerator, denominator).unwrap())
    }

    fn position_of(numerator: i64, denominator: i64) -> MusicalPosition {
        MusicalPosition(RationalTime::new(numerator, denominator).unwrap())
    }

    /// An InsertEvent of a rest at an explicit metric span.
    fn insert_at(
        replica: u64,
        counter: u64,
        event: u64,
        voice: u64,
        position: MusicalPosition,
        duration: MusicalDuration,
        ctx: CausalContext,
    ) -> OperationEnvelope {
        prim_env(
            replica,
            counter,
            (counter as i64 + 1) * 10,
            ctx,
            OperationKind::InsertEvent(InsertEventOp {
                staff_instance: StaffInstanceId::new(ReplicaId(9), 0),
                event: crate::valuegen::insert_event_value(
                    EventId::new(ReplicaId(replica), event),
                    VoiceId::new(ReplicaId(9), voice),
                    position,
                    duration,
                    &[],
                ),
            }),
        )
    }

    /// A ModifyEvent that re-places `event` (a rest) at a new metric span.
    fn modify_to(
        replica: u64,
        counter: u64,
        event: u64,
        voice: u64,
        position: MusicalPosition,
        duration: MusicalDuration,
        ctx: CausalContext,
    ) -> OperationEnvelope {
        prim_env(
            replica,
            counter,
            (counter as i64 + 1) * 10,
            ctx,
            OperationKind::ModifyEvent(crate::payload::ModifyEventOp {
                event: crate::valuegen::insert_event_value(
                    EventId::new(ReplicaId(replica), event),
                    VoiceId::new(ReplicaId(9), voice),
                    position,
                    duration,
                    &[],
                ),
            }),
        )
    }

    fn effect_at(state: &MaterializedState, counter: u64) -> Option<&OperationEffect> {
        state
            .effects
            .iter()
            .find(|(id, _)| *id == OperationId::new(ReplicaId(1), counter))
            .map(|(_, effect)| effect)
    }

    #[test]
    fn modify_event_trim_frees_the_voice_slot() {
        // e1 fills [0, 1). Trim it to [0, 1/2), then insert e2 into the freed [1/2, 1):
        // the insert only fits if the trim updated the voice-occupancy index.
        let e1 = insert(1, 0, 10, 1, 100, 0);
        let trim = modify_to(
            1,
            1,
            100,
            1,
            position_of(0, 1),
            musical(1, 2),
            CausalContext::new().with_seen(ReplicaId(1), 0),
        );
        let e2 = insert_at(
            1,
            2,
            101,
            1,
            position_of(1, 2),
            musical(1, 2),
            CausalContext::new().with_seen(ReplicaId(1), 1),
        );
        let mut set = OperationSet::new();
        set.accept_all(vec![e1, trim, e2]);
        let state = set.reduce();
        assert!(
            matches!(effect_at(&state, 1), Some(OperationEffect::Applied)),
            "the trim applies"
        );
        assert!(
            matches!(effect_at(&state, 2), Some(OperationEffect::Applied)),
            "the insert fits the span the trim freed (occupancy was updated)"
        );
    }

    #[test]
    fn modify_event_move_onto_a_sibling_is_refused() {
        // e1 [0, 1), e2 [1, 2). Moving e1 onto e2's span would break invariant 3, so
        // it is refused (a clean NoOp), not silently skipped.
        let e1 = insert(1, 0, 10, 1, 100, 0);
        let e2 = insert(1, 1, 20, 1, 101, 1);
        let onto_sibling = modify_to(
            1,
            2,
            100,
            1,
            position_of(1, 1),
            musical(1, 1),
            CausalContext::new().with_seen(ReplicaId(1), 1),
        );
        let mut set = OperationSet::new();
        set.accept_all(vec![e1, e2, onto_sibling]);
        let state = set.reduce();
        assert!(
            matches!(
                effect_at(&state, 2),
                Some(OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction {
                        reason: PreconditionFailureReason::EventDurationInvalid,
                    },
                })
            ),
            "a move onto a live sibling is refused"
        );
        assert!(
            state.is_clean(),
            "a refused move records no conflict/anomaly (like an insert overlap no-op)"
        );
    }

    #[test]
    fn modify_event_trim_materializes_in_the_graph() {
        use epiphany_core::check_invariants;
        use epiphany_core::generators::valid_score;

        // Shrink the first metric event's duration; the change must now reach the
        // graph (it was previously deferred) and leave the voice invariant-3 valid.
        let base = valid_score(0x5EED);
        let (event_id, duration) = base
            .voices()
            .flat_map(|(_, _, v)| v.events.clone())
            .find_map(|eid| {
                let ev = base.events.get(eid)?;
                match (ev.position(), ev.duration()) {
                    (EventPosition::Musical(_), EventDuration::Musical(d)) => {
                        Some((eid, d.clone()))
                    }
                    _ => None,
                }
            })
            .expect("the fixture has a metric event");
        let half = MusicalDuration(duration.0.mul(&RationalTime::new(1, 2).unwrap()));
        let mut shrunk = base.events.get(event_id).unwrap().clone();
        match &mut shrunk {
            Event::Pitched(pe) => pe.duration = EventDuration::Musical(half.clone()),
            Event::Rest(rest) => rest.duration = EventDuration::Musical(half.clone()),
            _ => panic!("a metric event is pitched or a rest"),
        }
        let modify = prim_env(
            2,
            0,
            10,
            CausalContext::new(),
            OperationKind::ModifyEvent(crate::payload::ModifyEventOp { event: shrunk }),
        );
        let mut set = OperationSet::new();
        set.accept_all(vec![modify]);
        let result = set.reduce_onto(&base);
        assert_eq!(
            result.score.events.get(event_id).map(Event::duration),
            Some(&EventDuration::Musical(half)),
            "the trimmed duration is materialized in the graph"
        );
        assert!(
            check_invariants(&result.score).is_empty(),
            "the trimmed voice stays sorted and non-overlapping"
        );
    }

    #[test]
    fn modify_event_does_not_rewrite_a_non_metric_event_as_metric() {
        use epiphany_core::check_invariants;
        use epiphany_core::generators::valid_score_rich;

        // A ModifyEvent that rewrites a wall-clock event (a proportional region) into a
        // metric one must stay deferred — materializing it would re-place a non-metric
        // event onto a musical grid, breaking the region's time-model invariant.
        let base = valid_score_rich(0x5EED);
        let event_id = base
            .voices()
            .flat_map(|(_, _, v)| v.events.clone())
            .find(|eid| {
                matches!(
                    base.events.get(*eid).map(Event::position),
                    Some(EventPosition::WallClock(_))
                )
            })
            .expect("the fixture has a proportional region with a wall-clock event");
        let voice = base.events.get(event_id).unwrap().voice();
        let as_metric = crate::valuegen::insert_event_value(
            event_id,
            voice,
            position_of(0, 1),
            musical(1, 2),
            &[],
        );
        let modify = prim_env(
            2,
            0,
            10,
            CausalContext::new(),
            OperationKind::ModifyEvent(crate::payload::ModifyEventOp { event: as_metric }),
        );
        let mut set = OperationSet::new();
        set.accept_all(vec![modify]);
        let result = set.reduce_onto(&base);
        assert!(
            matches!(
                result.score.events.get(event_id).map(Event::position),
                Some(EventPosition::WallClock(_))
            ),
            "the non-metric event is left as-is, not rewritten onto the musical grid"
        );
        assert!(
            check_invariants(&result.score).is_empty(),
            "the graph stays invariant-valid"
        );
    }

    #[test]
    fn modify_event_malformed_move_does_not_free_the_slot() {
        // A ModifyEvent rewriting e1 (filling [0, 1)) to an *empty* (malformed) pitched
        // event at [0, 1/2) is not materialized in the graph — so it must not move the
        // occupancy index either. A later insert into [1/2, 1) is therefore refused,
        // since e1 still occupies [0, 1).
        let e1 = insert(1, 0, 10, 1, 100, 0);
        let empty = Event::Pitched(epiphany_core::PitchedEvent {
            id: EventId::new(ReplicaId(1), 100),
            voice: VoiceId::new(ReplicaId(9), 1),
            position: EventPosition::Musical(position_of(0, 1)),
            duration: EventDuration::Musical(musical(1, 2)),
            pitches: vec![],
            articulations: vec![],
            dynamic: None,
            ornaments: vec![],
            stem: epiphany_core::StemConfiguration,
            grace: None,
        });
        let malformed = prim_env(
            1,
            1,
            20,
            CausalContext::new().with_seen(ReplicaId(1), 0),
            OperationKind::ModifyEvent(crate::payload::ModifyEventOp { event: empty }),
        );
        let e2 = insert_at(
            1,
            2,
            101,
            1,
            position_of(1, 2),
            musical(1, 2),
            CausalContext::new().with_seen(ReplicaId(1), 1),
        );
        let mut set = OperationSet::new();
        set.accept_all(vec![e1, malformed, e2]);
        let state = set.reduce();
        assert!(
            matches!(
                effect_at(&state, 2),
                Some(OperationEffect::NoOp {
                    reason: NoOpReason::PreconditionFailedUnderReduction { .. },
                })
            ),
            "the malformed trim did not free the slot, so the later insert is refused"
        );
    }

    #[test]
    fn insert_then_delete_identified_pitch_tombstones() {
        let event = EventId::new(ReplicaId(1), 100);
        let pitch = PitchId::new(ReplicaId(1), 7);
        let insert = insert(1, 0, 10, 1, 100, 0);
        let add = prim_env(
            1,
            1,
            20,
            CausalContext::new().with_seen(ReplicaId(1), 0),
            OperationKind::InsertIdentifiedPitch(crate::payload::InsertIdentifiedPitchOp {
                event,
                pitch: crate::valuegen::identified_pitch(pitch),
            }),
        );
        let del = prim_env(
            1,
            2,
            30,
            CausalContext::new().with_seen(ReplicaId(1), 1),
            OperationKind::DeleteIdentifiedPitch(crate::payload::DeleteIdentifiedPitchOp { pitch }),
        );

        let mut after_add = OperationSet::new();
        after_add.accept_all(vec![insert.clone(), add.clone()]);
        assert_eq!(
            after_add.reduce().objects.get(&TypedObjectId::Pitch(pitch)),
            Some(&ObjectState::Live),
            "InsertIdentifiedPitch mints the pitch live"
        );

        let mut after_del = OperationSet::new();
        after_del.accept_all(vec![insert, add, del]);
        assert!(
            matches!(
                after_del.reduce().objects.get(&TypedObjectId::Pitch(pitch)),
                Some(ObjectState::Tombstoned { .. })
            ),
            "DeleteIdentifiedPitch tombstones the pitch"
        );
    }

    // --- Group 2 (M2) behavior. --------------------------------------------

    fn create_slur(slur: epiphany_core::SlurId, a: EventId, b: EventId) -> OperationKind {
        OperationKind::CreateCrossCutting(crate::payload::CreateCrossCuttingOp {
            structure: CrossCuttingValue::Slur(crate::valuegen::slur(slur, a, b)),
        })
    }

    #[test]
    fn delete_cross_cutting_tombstones_the_structure() {
        let e1 = EventId::new(ReplicaId(1), 100);
        let e2 = EventId::new(ReplicaId(1), 101);
        let slur = epiphany_core::SlurId::new(ReplicaId(1), 1);
        let sid = TypedObjectId::Slur(slur);
        let create = prim_env(
            1,
            2,
            12,
            CausalContext::new().with_seen(ReplicaId(1), 1),
            create_slur(slur, e1, e2),
        );
        let delete = prim_env(
            1,
            3,
            13,
            CausalContext::new().with_seen(ReplicaId(1), 2),
            OperationKind::DeleteCrossCutting(crate::payload::DeleteCrossCuttingOp {
                structure: sid,
            }),
        );
        let mut set = OperationSet::new();
        set.accept_all(vec![
            insert(1, 0, 10, 1, 100, 0),
            insert(1, 1, 11, 1, 101, 1),
            create,
            delete,
        ]);
        let state = set.reduce();
        assert!(
            matches!(
                state.objects.get(&sid),
                Some(ObjectState::Tombstoned { .. })
            ),
            "DeleteCrossCutting tombstones the structure (delete-wins)"
        );
    }

    #[test]
    fn concurrent_differing_modify_cross_cutting_conflicts() {
        let e1 = EventId::new(ReplicaId(1), 100);
        let e2 = EventId::new(ReplicaId(1), 101);
        let e3 = EventId::new(ReplicaId(1), 102);
        let slur = epiphany_core::SlurId::new(ReplicaId(1), 1);
        let create = prim_env(
            1,
            3,
            13,
            CausalContext::new().with_seen(ReplicaId(1), 2),
            create_slur(slur, e1, e2),
        );
        // Two concurrent modifies (seen the create, not each other) with differing
        // endpoints: e1->e2 vs e1->e3.
        let seen_create = CausalContext::new().with_seen(ReplicaId(1), 3);
        let modify = |replica: u64, end: EventId| {
            prim_env(
                replica,
                0,
                20,
                seen_create.clone(),
                OperationKind::ModifyCrossCutting(crate::payload::ModifyCrossCuttingOp {
                    structure: CrossCuttingValue::Slur(crate::valuegen::slur(slur, e1, end)),
                }),
            )
        };
        let mut set = OperationSet::new();
        set.accept_all(vec![
            insert(1, 0, 10, 1, 100, 0),
            insert(1, 1, 11, 1, 101, 1),
            insert(1, 2, 12, 1, 102, 2),
            create,
            modify(2, e2),
            modify(3, e3),
        ]);
        let state = set.reduce();
        assert_eq!(
            state.conflicts.records().len(),
            1,
            "concurrent differing ModifyCrossCutting must record exactly one conflict"
        );
        assert!(matches!(
            state.conflicts.records()[0].kind,
            ConflictKind::StructuralFieldCollision { .. }
        ));
    }
}
