Agent K M2c (Group 3): structural container CRUD operations

Third broad-K0 subsystem group — six new value-typed ops for the score-graph
containers, reusing M1's disciplines (additive: OperationKind variants 15-20,
new apply arms + reduction methods; framework frozen). Per the project lead's
call, container deletes are EMPTY-ONLY (no cascade): a precondition NoOp unless
the container has no live children, so the caller deletes contents first.

- CreateRegion / DeleteRegion, CreateStaffInstance / DeleteStaffInstance,
  CreateVoice / DeleteVoice. Creates are value-typed mints of an empty container
  (set-union creation); deletes are delete-wins tombstones gated on emptiness.
- core: expose Region / StaffInstance / Voice via CanonicalValue (no new byte
  layout — they already have whole-score Codec impls), with round-trip coverage.
- New PreconditionFailureReason::ContainerNotEmpty (additive discriminant 10;
  encode + decode), reported when an empty-only delete hits a non-empty container.

Reduction (reduce.rs):
- Two child-existence indices, region_instances and instance_voices, drive the
  emptiness checks (a voice's events are read from voice_occupancy), so the
  ledger projection and the graph agree on every delete result. Populated by
  seed_from_graph, the create ops, and implicit voice creation in insert_event.
- CreateStaffInstance / DeleteStaffInstance maintain the region's staff_extent so
  it lists exactly the manifested staves (Chapter 5 RegionExtents); valuegen's
  fresh region uses a far-future wall-clock extent so it never overlaps an
  existing region in both time and staff.

Migration: v1-native (no lossy v0 predecessor) -> project/migrate by identity;
group1_and_group2_kinds_round_trip_by_identity extended to cover Group 3.

Coverage:
- testkit operation_payload + ops fuzz gen_payload emit the six kinds, so the
  convergence / determinism / migration-equivalence gates exercise the
  bookkeeping projection at scale.
- A reduce_onto graph test materializes a region -> staff instance -> voice
  subtree (invariant-clean), asserts the empty-only delete refuses a non-empty
  container with ContainerNotEmpty, and verifies an ordered teardown clears the
  subtree from both graph and ledger.

Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green (criterion
1 green with the new container objects in the corpus); conformance_suite scale 1
passes. Catalog sections + DECISIONS for these ops land in M2e per the staged
plan. The unrelated Agent-I working tree is left uncommitted; this commit stages
only core/ops/testkit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-06-25 18:10:15 -04:00
parent 0f1b209e54
commit a207077cd7
12 changed files with 841 additions and 28 deletions

View File

@ -2093,6 +2093,10 @@ canonical_value! {
Spanner,
RegionTimeModel,
TimeAnchor,
// Structural containers (M2c) — value-typed create payloads embed these.
Region,
StaffInstance,
Voice,
}
#[cfg(test)]
@ -2126,6 +2130,13 @@ mod value_codec_tests {
let s = valid_score_rich(seed.wrapping_mul(0x0100_0193).wrapping_add(7));
for region in &s.canvas.regions {
assert_value_round_trips(&region.time_model);
assert_value_round_trips(region);
for instance in region.staff_instances() {
assert_value_round_trips(instance);
for voice in &instance.voices {
assert_value_round_trips(voice);
}
}
}
for tie in &s.cross_cutting.ties {
assert_value_round_trips(tie);

View File

@ -218,6 +218,7 @@ fn precondition_reason(reader: &mut Reader<'_>) -> Result<PreconditionFailureRea
reader,
PreconditionFailureRegistryId,
)?)),
10 => Ok(PreconditionFailureReason::ContainerNotEmpty),
tag => Err(MaterializedDecodeError::InvalidTag {
kind: "PreconditionFailureReason",
tag,

View File

@ -132,6 +132,10 @@ pub enum PreconditionFailureReason {
PitchSpaceMismatch,
/// The operation targeted a voice that does not exist or is tombstoned.
VoiceMissing,
/// A structural-container delete (Group 3) targeted a container that still
/// has live children (an empty-only delete; the caller deletes contents
/// first).
ContainerNotEmpty,
/// An extension-declared precondition failed.
ExtensionPrecondition(ExtensionPreconditionId),
/// A registered precondition code from a versioned registry.
@ -151,6 +155,8 @@ impl PreconditionFailureReason {
PreconditionFailureReason::VoiceMissing => 7,
PreconditionFailureReason::ExtensionPrecondition(_) => 8,
PreconditionFailureReason::Registered(_) => 9,
// Additive (Group 3); keeps the ratified 0..=9 discriminants stable.
PreconditionFailureReason::ContainerNotEmpty => 10,
}
}
}

View File

@ -22,7 +22,7 @@
use epiphany_core::{
EventId, MusicalDuration, MusicalPosition, OperationId, PitchId, RationalTime, RegionId,
ReplicaId, SlurId, StaffInstanceId, TypedObjectId, VoiceId,
ReplicaId, SlurId, StaffId, StaffInstanceId, TypedObjectId, VoiceId,
};
use epiphany_determinism::fuzz::SplitMix64;
@ -30,10 +30,11 @@ use crate::causal::CausalContext;
use crate::envelope::OperationEnvelope;
use crate::opset::OperationSet;
use crate::payload::{
CreateCrossCuttingOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp,
DeleteIdentifiedPitchOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp,
ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, RespellPitchOp,
SetUserSystemBreakOp, TransposeOp, TupletCompensation,
CreateCrossCuttingOp, CreateRegionOp, CreateStaffInstanceOp, CreateVoiceOp, CrossCuttingValue,
DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp,
DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp,
ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload,
RespellPitchOp, SetUserSystemBreakOp, TransposeOp, TupletCompensation,
};
use crate::stamp::{HybridLogicalClock, OperationStamp};
use crate::support::AuthorId;
@ -83,7 +84,7 @@ fn pitch(n: u64) -> PitchId {
/// Generates a random payload over the shared id space.
fn gen_payload(rng: &mut SplitMix64) -> OperationPayload {
let kind = match rng.below(12) {
let kind = match rng.below(18) {
0 => {
let voice = VoiceId::new(ReplicaId(7), rng.below(3));
let position = MusicalPosition(RationalTime::from_int(rng.below(4) as i32));
@ -155,13 +156,37 @@ fn gen_payload(rng: &mut SplitMix64) -> OperationPayload {
10 => OperationKind::DeleteCrossCutting(DeleteCrossCuttingOp {
structure: TypedObjectId::Slur(SlurId::new(ReplicaId(7), rng.below(ID_SPACE))),
}),
_ => OperationKind::ModifyCrossCutting(ModifyCrossCuttingOp {
11 => OperationKind::ModifyCrossCutting(ModifyCrossCuttingOp {
structure: CrossCuttingValue::Slur(valuegen::slur(
SlurId::new(ReplicaId(7), rng.below(ID_SPACE)),
event(rng.below(ID_SPACE)),
event(rng.below(ID_SPACE)),
)),
}),
// Group 3 (M2c): structural container CRUD over the shared id space.
12 => OperationKind::CreateRegion(CreateRegionOp {
region: valuegen::region(RegionId::new(ReplicaId(7), rng.below(3))),
}),
13 => OperationKind::DeleteRegion(DeleteRegionOp {
region: RegionId::new(ReplicaId(7), rng.below(3)),
}),
14 => OperationKind::CreateStaffInstance(CreateStaffInstanceOp {
region: RegionId::new(ReplicaId(7), rng.below(3)),
instance: valuegen::staff_instance(
StaffInstanceId::new(ReplicaId(7), rng.below(3)),
StaffId::new(ReplicaId(7), 0),
),
}),
15 => OperationKind::DeleteStaffInstance(DeleteStaffInstanceOp {
staff_instance: StaffInstanceId::new(ReplicaId(7), rng.below(3)),
}),
16 => OperationKind::CreateVoice(CreateVoiceOp {
staff_instance: StaffInstanceId::new(ReplicaId(7), rng.below(3)),
voice: valuegen::voice(VoiceId::new(ReplicaId(7), rng.below(3))),
}),
_ => OperationKind::DeleteVoice(DeleteVoiceOp {
voice: VoiceId::new(ReplicaId(7), rng.below(3)),
}),
};
OperationPayload::Primitive(kind)
}

View File

@ -113,8 +113,9 @@ pub use envelope::{well_formed, EnvelopeHash, OperationEnvelope, WellFormednessE
pub use migrate::{migrate_v0_envelope, project_v1_to_v0, MigrationError};
pub use opset::OperationSet;
pub use payload::{
ChangeRegionTimeModelOp, CreateCrossCuttingOp, CrossCuttingValue, DeleteCrossCuttingOp,
DeleteEventOp, DeleteIdentifiedPitchOp, InsertEventOp, InsertIdentifiedPitchOp,
ChangeRegionTimeModelOp, CreateCrossCuttingOp, CreateRegionOp, CreateStaffInstanceOp,
CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp,
DeleteRegionOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp,
ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationKindTag,
OperationPayload, PositionRemapping, ResolveConflictPayload, RespellPitchOp,
SetUserSystemBreakOp, TransactionCategory, TransactionDescriptor, TransposeOp,

View File

@ -154,6 +154,13 @@ fn project_kind(kind: &OperationKind) -> V0OperationKind {
// v1-native (Group 2): projected verbatim.
OperationKind::DeleteCrossCutting(op) => V0OperationKind::DeleteCrossCutting(*op),
OperationKind::ModifyCrossCutting(op) => V0OperationKind::ModifyCrossCutting(op.clone()),
// v1-native (Group 3): projected verbatim.
OperationKind::CreateRegion(op) => V0OperationKind::CreateRegion(op.clone()),
OperationKind::DeleteRegion(op) => V0OperationKind::DeleteRegion(*op),
OperationKind::CreateStaffInstance(op) => V0OperationKind::CreateStaffInstance(op.clone()),
OperationKind::DeleteStaffInstance(op) => V0OperationKind::DeleteStaffInstance(*op),
OperationKind::CreateVoice(op) => V0OperationKind::CreateVoice(op.clone()),
OperationKind::DeleteVoice(op) => V0OperationKind::DeleteVoice(*op),
}
}
@ -282,6 +289,13 @@ fn migrate_kind(kind: &V0OperationKind, context: &Score) -> Result<OperationKind
// v1-native (Group 2): identity round-trip.
V0OperationKind::DeleteCrossCutting(op) => OperationKind::DeleteCrossCutting(*op),
V0OperationKind::ModifyCrossCutting(op) => OperationKind::ModifyCrossCutting(op.clone()),
// v1-native (Group 3): identity round-trip.
V0OperationKind::CreateRegion(op) => OperationKind::CreateRegion(op.clone()),
V0OperationKind::DeleteRegion(op) => OperationKind::DeleteRegion(*op),
V0OperationKind::CreateStaffInstance(op) => OperationKind::CreateStaffInstance(op.clone()),
V0OperationKind::DeleteStaffInstance(op) => OperationKind::DeleteStaffInstance(*op),
V0OperationKind::CreateVoice(op) => OperationKind::CreateVoice(op.clone()),
V0OperationKind::DeleteVoice(op) => OperationKind::DeleteVoice(*op),
})
}
@ -578,6 +592,29 @@ mod tests {
ev(2),
)),
}),
OperationKind::CreateRegion(crate::payload::CreateRegionOp {
region: valuegen::region(epiphany_core::RegionId::new(ReplicaId(3), 7)),
}),
OperationKind::DeleteRegion(crate::payload::DeleteRegionOp {
region: epiphany_core::RegionId::new(ReplicaId(3), 7),
}),
OperationKind::CreateStaffInstance(crate::payload::CreateStaffInstanceOp {
region: epiphany_core::RegionId::new(ReplicaId(3), 7),
instance: valuegen::staff_instance(
epiphany_core::StaffInstanceId::new(ReplicaId(3), 8),
StaffId::new(ReplicaId(3), 1),
),
}),
OperationKind::DeleteStaffInstance(crate::payload::DeleteStaffInstanceOp {
staff_instance: epiphany_core::StaffInstanceId::new(ReplicaId(3), 8),
}),
OperationKind::CreateVoice(crate::payload::CreateVoiceOp {
staff_instance: epiphany_core::StaffInstanceId::new(ReplicaId(3), 8),
voice: valuegen::voice(VoiceId::new(ReplicaId(3), 9)),
}),
OperationKind::DeleteVoice(crate::payload::DeleteVoiceOp {
voice: VoiceId::new(ReplicaId(3), 9),
}),
];
for kind in kinds {
let e = env(primitive(kind));

View File

@ -33,9 +33,9 @@
use epiphany_core::{
Beam, CanonicalValue, Event, EventDuration, EventId, EventPosition, IdentifiedPitch,
MusicalDuration, MusicalPosition, Pitch, PitchId, PitchSpelling, RegionId, RegionTimeModel,
Rest, Slur, Spanner, StaffInstanceId, Tie, TimeAnchor, TransactionId, TupletId, TypedObjectId,
VoiceId,
MusicalDuration, MusicalPosition, Pitch, PitchId, PitchSpelling, Region, RegionId,
RegionTimeModel, Rest, Slur, Spanner, StaffInstance, StaffInstanceId, Tie, TimeAnchor,
TransactionId, TupletId, TypedObjectId, Voice, VoiceId,
};
use epiphany_determinism::{sorted_canonical, CanonicalEncode};
@ -125,6 +125,19 @@ pub enum OperationKind {
/// Overwrite a cross-cutting structure's value (later-in-canonical-order
/// wins).
ModifyCrossCutting(ModifyCrossCuttingOp),
// --- Group 3 (M2c): structural container CRUD. Mint + empty-only delete. ---
/// Mint an empty region into the canvas.
CreateRegion(CreateRegionOp),
/// Tombstone an empty region (delete-wins; precondition: no live instances).
DeleteRegion(DeleteRegionOp),
/// Mint an empty staff instance into a live region.
CreateStaffInstance(CreateStaffInstanceOp),
/// Tombstone an empty staff instance (precondition: no live voices).
DeleteStaffInstance(DeleteStaffInstanceOp),
/// Mint an empty voice into a live staff instance.
CreateVoice(CreateVoiceOp),
/// Tombstone an empty voice (precondition: no live events).
DeleteVoice(DeleteVoiceOp),
}
impl OperationKind {
@ -145,6 +158,12 @@ impl OperationKind {
OperationKind::ModifyIdentifiedPitch(_) => 12,
OperationKind::DeleteCrossCutting(_) => 13,
OperationKind::ModifyCrossCutting(_) => 14,
OperationKind::CreateRegion(_) => 15,
OperationKind::DeleteRegion(_) => 16,
OperationKind::CreateStaffInstance(_) => 17,
OperationKind::DeleteStaffInstance(_) => 18,
OperationKind::CreateVoice(_) => 19,
OperationKind::DeleteVoice(_) => 20,
}
}
@ -168,6 +187,12 @@ impl OperationKind {
OperationKind::ModifyIdentifiedPitch(_) => OperationKindTag::ModifyIdentifiedPitch,
OperationKind::DeleteCrossCutting(_) => OperationKindTag::DeleteCrossCutting,
OperationKind::ModifyCrossCutting(_) => OperationKindTag::ModifyCrossCutting,
OperationKind::CreateRegion(_) => OperationKindTag::InsertRegion,
OperationKind::DeleteRegion(_) => OperationKindTag::DeleteRegion,
OperationKind::CreateStaffInstance(_) => OperationKindTag::InsertStaffInstance,
OperationKind::DeleteStaffInstance(_) => OperationKindTag::DeleteStaffInstance,
OperationKind::CreateVoice(_) => OperationKindTag::CreateVoice,
OperationKind::DeleteVoice(_) => OperationKindTag::DeleteVoice,
}
}
}
@ -194,6 +219,12 @@ impl CanonicalEncode for OperationKind {
OperationKind::ModifyIdentifiedPitch(op) => op.encode_canonical(out),
OperationKind::DeleteCrossCutting(op) => op.encode_canonical(out),
OperationKind::ModifyCrossCutting(op) => op.encode_canonical(out),
OperationKind::CreateRegion(op) => op.encode_canonical(out),
OperationKind::DeleteRegion(op) => op.encode_canonical(out),
OperationKind::CreateStaffInstance(op) => op.encode_canonical(out),
OperationKind::DeleteStaffInstance(op) => op.encode_canonical(out),
OperationKind::CreateVoice(op) => op.encode_canonical(out),
OperationKind::DeleteVoice(op) => op.encode_canonical(out),
}
}
}
@ -224,6 +255,8 @@ pub enum OperationKindTag {
InsertIdentifiedPitch,
DeleteIdentifiedPitch,
ModifyIdentifiedPitch,
CreateVoice,
DeleteVoice,
}
impl OperationKindTag {
@ -249,6 +282,8 @@ impl OperationKindTag {
OperationKindTag::InsertIdentifiedPitch => 17,
OperationKindTag::DeleteIdentifiedPitch => 18,
OperationKindTag::ModifyIdentifiedPitch => 19,
OperationKindTag::CreateVoice => 20,
OperationKindTag::DeleteVoice => 21,
}
}
}
@ -805,6 +840,118 @@ impl CanonicalEncode for ModifyCrossCuttingOp {
}
}
// --- Group 3 (M2c): structural container CRUD (Chapter 6 §6.10). -------------
//
// Creates are value-typed mints of an *empty* container (set-union creation);
// deletes are empty-only delete-wins tombstones (the container must have no live
// children — the caller deletes contents first). See `DECISIONS.md`.
/// Mint an empty region into the canvas (Chapter 6 §6.10 InsertRegion). Carries
/// the full [`Region`] value (v1); the reduction preconditions it carries no
/// staff instances (an empty container).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CreateRegionOp {
pub region: Region,
}
impl CreateRegionOp {
/// The minted region's identifier.
pub fn region_id(&self) -> RegionId {
self.region.id
}
}
impl CanonicalEncode for CreateRegionOp {
fn encode_canonical(&self, out: &mut Vec<u8>) {
push_lp_bytes(out, &self.region.canonical_bytes());
}
}
/// Tombstone an empty region (Chapter 6 §6.10 DeleteRegion). Delete-wins, but a
/// precondition NoOp if the region still has live staff instances.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct DeleteRegionOp {
pub region: RegionId,
}
impl CanonicalEncode for DeleteRegionOp {
fn encode_canonical(&self, out: &mut Vec<u8>) {
push_canon(out, &self.region);
}
}
/// Mint an empty staff instance into a live region (Chapter 6 §6.10
/// InsertStaffInstance). Carries the full [`StaffInstance`] value (v1); the
/// reduction preconditions it carries no voices.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CreateStaffInstanceOp {
pub region: RegionId,
pub instance: StaffInstance,
}
impl CreateStaffInstanceOp {
/// The minted staff instance's identifier.
pub fn instance_id(&self) -> StaffInstanceId {
self.instance.id
}
}
impl CanonicalEncode for CreateStaffInstanceOp {
fn encode_canonical(&self, out: &mut Vec<u8>) {
push_canon(out, &self.region);
push_lp_bytes(out, &self.instance.canonical_bytes());
}
}
/// Tombstone an empty staff instance (Chapter 6 §6.10 DeleteStaffInstance).
/// Delete-wins, but a precondition NoOp if it still has live voices.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct DeleteStaffInstanceOp {
pub staff_instance: StaffInstanceId,
}
impl CanonicalEncode for DeleteStaffInstanceOp {
fn encode_canonical(&self, out: &mut Vec<u8>) {
push_canon(out, &self.staff_instance);
}
}
/// Mint an empty voice into a live staff instance (Chapter 6 §6.10 CreateVoice).
/// Carries the full [`Voice`] value (v1); the reduction preconditions it carries
/// no events.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CreateVoiceOp {
pub staff_instance: StaffInstanceId,
pub voice: Voice,
}
impl CreateVoiceOp {
/// The minted voice's identifier.
pub fn voice_id(&self) -> VoiceId {
self.voice.id
}
}
impl CanonicalEncode for CreateVoiceOp {
fn encode_canonical(&self, out: &mut Vec<u8>) {
push_canon(out, &self.staff_instance);
push_lp_bytes(out, &self.voice.canonical_bytes());
}
}
/// Tombstone an empty voice (Chapter 6 §6.10 DeleteVoice). Delete-wins, but a
/// precondition NoOp if it still has live events.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct DeleteVoiceOp {
pub voice: VoiceId,
}
impl CanonicalEncode for DeleteVoiceOp {
fn encode_canonical(&self, out: &mut Vec<u8>) {
push_canon(out, &self.voice);
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -33,8 +33,8 @@ use std::collections::{BTreeMap, BTreeSet};
use epiphany_core::{
derive_promoted_voice_id, AnchorOffset, CanonicalValue, Event, EventDuration, EventId,
EventPosition, MusicalDuration, MusicalPosition, OperationId, Pitch, PitchId, PitchSpelling,
RegionEdge, RegionId, RegionTimeModel, Score, TimeAnchor, TransactionId, TypedObjectId, Voice,
VoiceId, VoiceOrigin,
RegionEdge, RegionId, RegionTimeModel, Score, StaffInstance, StaffInstanceId, TimeAnchor,
TransactionId, TypedObjectId, Voice, VoiceId, VoiceOrigin,
};
use epiphany_determinism::CanonicalEncode;
@ -50,10 +50,11 @@ 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, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp,
DeleteIdentifiedPitchOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp,
ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, RespellPitchOp,
TransposeOp, TupletCompensation,
CreateCrossCuttingOp, CreateRegionOp, CreateStaffInstanceOp, CreateVoiceOp, CrossCuttingValue,
DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp,
DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp,
ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload,
RespellPitchOp, TransposeOp, TupletCompensation,
};
use crate::undo::{UndoPolicy, UndoTransactionPayload};
@ -328,6 +329,11 @@ struct Reducer<'a> {
// modify maps above: last modifier + value it wrote, keyed by structure id.
last_cross_cutting_modify: BTreeMap<TypedObjectId, (OperationId, CrossCuttingValue)>,
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>>,
migrated_regions: BTreeSet<RegionId>,
region_migrator: BTreeMap<RegionId, OperationId>,
descriptors: BTreeMap<TransactionId, OperationId>,
@ -352,6 +358,8 @@ struct WorkingSnapshot {
last_pitch_modify: BTreeMap<PitchId, (OperationId, Pitch)>,
last_cross_cutting_modify: BTreeMap<TypedObjectId, (OperationId, CrossCuttingValue)>,
structures: BTreeMap<TypedObjectId, Vec<TypedObjectId>>,
region_instances: BTreeMap<RegionId, BTreeSet<StaffInstanceId>>,
instance_voices: BTreeMap<StaffInstanceId, BTreeSet<VoiceId>>,
migrated_regions: BTreeSet<RegionId>,
region_migrator: BTreeMap<RegionId, OperationId>,
descriptors: BTreeMap<TransactionId, OperationId>,
@ -425,6 +433,8 @@ impl<'a> Reducer<'a> {
last_pitch_modify: BTreeMap::new(),
last_cross_cutting_modify: BTreeMap::new(),
structures: BTreeMap::new(),
region_instances: BTreeMap::new(),
instance_voices: BTreeMap::new(),
migrated_regions: BTreeSet::new(),
region_migrator: BTreeMap::new(),
descriptors: BTreeMap::new(),
@ -485,9 +495,17 @@ impl<'a> Reducer<'a> {
for region in &score.canvas.regions {
self.objects
.insert(TypedObjectId::Region(region.id), ObjectState::Live);
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);
@ -1202,6 +1220,12 @@ impl<'a> Reducer<'a> {
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),
},
OperationPayload::ResolveConflict(op) => self.resolve_conflict(env, op),
OperationPayload::UndoTransaction(op) => self.undo_transaction(env, op),
@ -1309,6 +1333,11 @@ impl<'a> Reducer<'a> {
// 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);
}
}
@ -1807,6 +1836,286 @@ impl<'a> Reducer<'a> {
}
}
// --- 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;
}
self.graph_create_region(&op.region);
self.mint_container(env, robj);
self.region_instances.entry(op.region_id()).or_default();
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;
}
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;
}
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.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,
@ -2677,6 +2986,8 @@ impl<'a> Reducer<'a> {
last_pitch_modify: self.last_pitch_modify.clone(),
last_cross_cutting_modify: self.last_cross_cutting_modify.clone(),
structures: self.structures.clone(),
region_instances: self.region_instances.clone(),
instance_voices: self.instance_voices.clone(),
migrated_regions: self.migrated_regions.clone(),
region_migrator: self.region_migrator.clone(),
descriptors: self.descriptors.clone(),
@ -2698,6 +3009,8 @@ impl<'a> Reducer<'a> {
self.last_pitch_modify = s.last_pitch_modify;
self.last_cross_cutting_modify = s.last_cross_cutting_modify;
self.structures = s.structures;
self.region_instances = s.region_instances;
self.instance_voices = s.instance_voices;
self.migrated_regions = s.migrated_regions;
self.region_migrator = s.region_migrator;
self.descriptors = s.descriptors;

View File

@ -77,6 +77,13 @@ pub enum V0OperationKind {
// Group 2 (M2) — also v1-native; round-trip by identity.
DeleteCrossCutting(crate::payload::DeleteCrossCuttingOp),
ModifyCrossCutting(crate::payload::ModifyCrossCuttingOp),
// Group 3 (M2c) — also v1-native; round-trip by identity.
CreateRegion(crate::payload::CreateRegionOp),
DeleteRegion(crate::payload::DeleteRegionOp),
CreateStaffInstance(crate::payload::CreateStaffInstanceOp),
DeleteStaffInstance(crate::payload::DeleteStaffInstanceOp),
CreateVoice(crate::payload::CreateVoiceOp),
DeleteVoice(crate::payload::DeleteVoiceOp),
}
/// v0 `InsertEvent`: the event was a bare [`EventId`] plus the reduction-relevant

View File

@ -18,10 +18,11 @@ use epiphany_core::{
AcousticPitch, AcousticRealization, AleatoricAnchoringDiscipline, AleatoricTimeModel,
AnchorOffset, Beam, BeamId, CmnNominal, Event, EventId, EventOrderingDAG, EventPosition,
IdentifiedPitch, MetricTimeModel, MusicalDuration, MusicalPosition, Pitch, PitchId,
PitchSpaceId, PitchSpacePosition, PitchSpelling, PitchedEvent, ProportionalTimeModel,
RegionEdge, RegionId, RegionTimeModel, Rest, ScalePosition, Slur, SlurId, SpellingAttachment,
SpellingDirective, SpellingScope, SpellingSource, StemConfiguration, Tie, TieClass, TieId,
TimeAnchor, VoiceId, WallClockDuration,
PitchSpaceId, PitchSpacePosition, PitchSpelling, PitchedEvent, ProportionalTimeModel, Region,
RegionContent, RegionEdge, RegionId, RegionTimeModel, Rest, ScalePosition, Slur, SlurId,
SpellingAttachment, SpellingDirective, SpellingScope, SpellingSource, StaffBasedContent,
StaffExtent, StaffId, StaffInstance, StaffInstanceId, StemConfiguration, Tie, TieClass, TieId,
TimeAnchor, TimeExtent, Voice, VoiceId, VoiceOrigin, WallClockDuration, WallClockTime,
};
/// A deterministic, fully-specified C4 pitch in the cmn-12 space — the neutral
@ -211,6 +212,62 @@ pub fn aleatoric_model() -> RegionTimeModel {
})
}
/// An empty, user-declared [`Voice`] (M2c) — the container a `CreateVoice` mints
/// before any event is inserted into it.
pub fn voice(id: VoiceId) -> Voice {
Voice {
id,
events: Vec::new(),
default_stem_direction: None,
is_primary: false,
origin: VoiceOrigin::UserDeclared,
}
}
/// An empty [`StaffInstance`] (M2c) over the given global `staff` — the container
/// a `CreateStaffInstance` mints before any voice is created in it.
pub fn staff_instance(id: StaffInstanceId, staff: StaffId) -> StaffInstance {
StaffInstance {
id,
staff,
voices: Vec::new(),
clef_sequence: Vec::new(),
key_sequence: Vec::new(),
local_metric_grid: None,
measures: Vec::new(),
instrument_override: None,
staff_lines_override: None,
visible: true,
}
}
/// An empty metric [`Region`] (M2c) — the container a `CreateRegion` mints before
/// any staff instance is added to it. Carries no staff instances and an empty
/// staff extent (a region with no instances is reference-clean, Chapter 5).
pub fn region(id: RegionId) -> Region {
Region {
id,
time_model: metric_model(),
content: RegionContent::StaffBased(StaffBasedContent {
staff_instances: Vec::new(),
..Default::default()
}),
// A far-future wall-clock extent so a freshly-created (initially empty)
// region does not overlap an existing region in both time and staff once
// a staff instance is added (Chapter 5 RegionExtents).
time_extent: TimeExtent {
start: TimeAnchor::WallClock {
time: WallClockTime(1_000_000_000),
},
end: TimeAnchor::WallClock {
time: WallClockTime(1_000_001_000),
},
},
staff_extent: StaffExtent { staves: Vec::new() },
local_tempo_map: None,
}
}
/// An explicit, user-chosen per-pitch [`SpellingAttachment`] — the engraved-layer
/// spelling a materialized score carries after a `RespellPitch`. The v0 → v1
/// migration recovers a respell's spelling from exactly these attachments

View File

@ -1030,3 +1030,186 @@ fn deleting_both_slur_endpoints_cascades_in_both_graph_and_ledger() {
);
assert!(check_invariants(&result.score).is_empty());
}
/// Helper: the effect recorded for `id` in a reduction.
fn effect_of(result: &epiphany_ops::GraphMaterialization, id: OperationId) -> OperationEffect {
result
.state
.effects
.iter()
.find(|(e, _)| *e == id)
.map(|(_, eff)| eff.clone())
.expect("the operation has an effect")
}
#[test]
fn structural_containers_create_and_empty_only_delete_in_the_graph() {
use epiphany_core::{RegionId, StaffInstanceId};
let base = epiphany_core::generators::valid_score(100);
let staff = base.staves[0].id;
let region = RegionId::new(ReplicaId(72), 0);
let instance = StaffInstanceId::new(ReplicaId(72), 1);
let v = VoiceId::new(ReplicaId(72), 2);
let r = 72;
let create_region = envelope(
r,
0,
10,
CausalContext::new(),
None,
OperationPayload::Primitive(OperationKind::CreateRegion(epiphany_ops::CreateRegionOp {
region: valuegen::region(region),
})),
);
let create_instance = envelope(
r,
1,
11,
CausalContext::new().with_seen(ReplicaId(r), 0),
None,
OperationPayload::Primitive(OperationKind::CreateStaffInstance(
epiphany_ops::CreateStaffInstanceOp {
region,
instance: valuegen::staff_instance(instance, staff),
},
)),
);
let create_voice = envelope(
r,
2,
12,
CausalContext::new().with_seen(ReplicaId(r), 1),
None,
OperationPayload::Primitive(OperationKind::CreateVoice(epiphany_ops::CreateVoiceOp {
staff_instance: instance,
voice: valuegen::voice(v),
})),
);
let creates = [
create_region.clone(),
create_instance.clone(),
create_voice.clone(),
];
// Creates materialize the container subtree, invariant-clean.
let mut after_create = OperationSet::new();
after_create.accept_all(creates.clone());
let created = after_create.reduce_onto(&base);
let materialized_region = created
.score
.canvas
.regions
.iter()
.find(|rg| rg.id == region)
.expect("the region is materialized");
assert!(
materialized_region
.staff_instances()
.iter()
.any(|i| i.id == instance),
"the staff instance is materialized in its region"
);
assert!(
materialized_region
.staff_instances()
.iter()
.flat_map(|i| &i.voices)
.any(|vo| vo.id == v),
"the voice is materialized in its staff instance"
);
assert!(check_invariants(&created.score).is_empty());
// Empty-only: deleting the non-empty region (and instance) is refused.
let del_region_early = envelope(
r,
3,
13,
CausalContext::new().with_seen(ReplicaId(r), 2),
None,
OperationPayload::Primitive(OperationKind::DeleteRegion(epiphany_ops::DeleteRegionOp {
region,
})),
);
let mut early = OperationSet::new();
early.accept_all(creates.iter().cloned().chain([del_region_early.clone()]));
let early_res = early.reduce_onto(&base);
assert!(
early_res
.score
.canvas
.regions
.iter()
.any(|rg| rg.id == region),
"a non-empty region delete is refused and leaves the region in the graph"
);
assert_eq!(
effect_of(&early_res, del_region_early.id),
OperationEffect::NoOp {
reason: NoOpReason::PreconditionFailedUnderReduction {
reason: PreconditionFailureReason::ContainerNotEmpty,
},
},
"the refused delete reports ContainerNotEmpty"
);
// Ordered teardown (voice, instance, region) clears the subtree from graph
// and ledger, invariant-clean.
let del_voice = envelope(
r,
3,
13,
CausalContext::new().with_seen(ReplicaId(r), 2),
None,
OperationPayload::Primitive(OperationKind::DeleteVoice(epiphany_ops::DeleteVoiceOp {
voice: v,
})),
);
let del_instance = envelope(
r,
4,
14,
CausalContext::new().with_seen(ReplicaId(r), 3),
None,
OperationPayload::Primitive(OperationKind::DeleteStaffInstance(
epiphany_ops::DeleteStaffInstanceOp {
staff_instance: instance,
},
)),
);
let del_region = envelope(
r,
5,
15,
CausalContext::new().with_seen(ReplicaId(r), 4),
None,
OperationPayload::Primitive(OperationKind::DeleteRegion(epiphany_ops::DeleteRegionOp {
region,
})),
);
let mut teardown = OperationSet::new();
teardown.accept_all(
creates
.into_iter()
.chain([del_voice, del_instance, del_region]),
);
let result = teardown.reduce_onto(&base);
assert!(
!result.score.canvas.regions.iter().any(|rg| rg.id == region),
"the region is removed from the graph after the ordered teardown"
);
for obj in [
TypedObjectId::Region(region),
TypedObjectId::StaffInstance(instance),
TypedObjectId::Voice(v),
] {
assert!(
matches!(
result.state.objects.get(&obj),
Some(epiphany_ops::ObjectState::Tombstoned { .. })
),
"{obj:?} is tombstoned in the ledger after teardown"
);
}
assert!(check_invariants(&result.score).is_empty());
}

View File

@ -32,11 +32,12 @@ use epiphany_ops::valuegen;
use epiphany_ops::{
AnomalousReplicaSegment, AuthorId, CausalContext, ChangeRegionTimeModelOp, ConflictId,
ConflictKind, ConflictKindRegistryId, ConflictRecord, ConflictRegistry,
ConflictResolutionState, CreateCrossCuttingOp, CrossCuttingValue, DeleteCrossCuttingOp,
DeleteEventOp, DeleteIdentifiedPitchOp, ExtensionPreconditionId, FieldPath, HybridLogicalClock,
InsertEventOp, InsertIdentifiedPitchOp, IntegrityAnomaly, IntegrityAnomalyKind,
IntegrityAnomalyRegistryId, MaterializedState, ModifyCrossCuttingOp, ModifyEventOp,
ModifyIdentifiedPitchOp, NoOpReason, ObjectKind, ObjectState, OperationEffect,
ConflictResolutionState, CreateCrossCuttingOp, CreateRegionOp, CreateStaffInstanceOp,
CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp,
DeleteRegionOp, DeleteStaffInstanceOp, DeleteVoiceOp, ExtensionPreconditionId, FieldPath,
HybridLogicalClock, InsertEventOp, InsertIdentifiedPitchOp, IntegrityAnomaly,
IntegrityAnomalyKind, IntegrityAnomalyRegistryId, MaterializedState, ModifyCrossCuttingOp,
ModifyEventOp, ModifyIdentifiedPitchOp, NoOpReason, ObjectKind, ObjectState, OperationEffect,
OperationEnvelope, OperationKind, OperationKindRegistryId, OperationPayload, OperationSet,
OperationStamp, PendingReason, PositionRemapping, PreconditionFailureReason,
PreconditionFailureRegistryId, ReanchorReason, ReanchorReasonRegistryId, ReanchorResult,
@ -635,7 +636,7 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP
}
_ => {}
}
let kind = match rng.below(15) {
let kind = match rng.below(21) {
0 => {
let pitches = if rng.boolean() {
vec![obj_pitch(rng.below(pitches))]
@ -731,6 +732,30 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP
obj_event(rng.below(events)),
)),
}),
// Group 3 (M2c): structural container CRUD over the shared id space.
14 => OperationKind::CreateRegion(CreateRegionOp {
region: valuegen::region(RegionId::new(OBJ_REPLICA, rng.below(2))),
}),
15 => OperationKind::DeleteRegion(DeleteRegionOp {
region: RegionId::new(OBJ_REPLICA, rng.below(2)),
}),
16 => OperationKind::CreateStaffInstance(CreateStaffInstanceOp {
region: RegionId::new(OBJ_REPLICA, rng.below(2)),
instance: valuegen::staff_instance(
StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
StaffId::new(OBJ_REPLICA, 0),
),
}),
17 => OperationKind::DeleteStaffInstance(DeleteStaffInstanceOp {
staff_instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
}),
18 => OperationKind::CreateVoice(CreateVoiceOp {
staff_instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
voice: valuegen::voice(VoiceId::new(OBJ_REPLICA, rng.below(4))),
}),
19 => OperationKind::DeleteVoice(DeleteVoiceOp {
voice: VoiceId::new(OBJ_REPLICA, rng.below(4)),
}),
_ => OperationKind::Registered(
OperationKindRegistryId(rng.next_u64() as u128),
rng.byte_vec(0, 16),