epiphany/crates/epiphany-testkit/src/generators.rs

2121 lines
88 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Deterministic property-test generators for the public types of A
//! ([`epiphany_determinism`]), B ([`epiphany_core`]), C ([`epiphany_ops`]), and
//! D ([`epiphany_bundle`]). Agent E's layout-IR types
//! ([`epiphany_layout_ir`]) are generated in [`crate::layout_stub`], which
//! drives the real crate.
//!
//! Every generator draws from the seeded [`Rng`], so a failing case reproduces
//! from its seed (Appendix D §"Randomness": no platform entropy in the harness).
//! Whole **graphs** are generated by Agent B's own positive/negative generators,
//! re-exported as [`graph`].
use epiphany_bundle::{
chunk_id, content_hash_for, BlobId, BlobRef, ChunkKind, ChunkRef, CommitState,
CompressionAlgorithm, DocumentId, ExtensionDeclaration, ExtensionId, FrontierBytes, LineageId,
Manifest, ProfileConstraints, ProfileDeclaration, ProfileId, ProfileRegistryId,
ReductionAlgorithmVersion, RetentionPolicy, SchemaVersion, SemVer, SnapshotId, SnapshotRef,
Superblock, WallClockDuration as BundleWallClockDuration, WallClockTime as BundleWallClockTime,
};
use epiphany_core::{
AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId, ChordSymbolId,
CommentId, EventId, GraphicGestureId, GraphicObjectId, InstrumentId, IntegrityAnomalyId,
LyricLineId, MarkerId, MeasureId, MusicalDuration, MusicalPosition, ObjectKindRegistryId,
OperationId, PartDefinitionId, PitchId, RationalTime, RegionId, RepeatStructureId, ReplicaId,
SlurId, SpannerId, StaffGroupId, StaffId, StaffInstanceId, TieId, TimeSignatureId,
TransactionId, TranspositionInterval, TupletId, TypedObjectId, ViewId, VoiceId,
WallClockDuration, WallClockTime,
};
use epiphany_determinism::{
CanonicalEncode, CanonicalF64, ChunkId, ContentHash, DomainTag, QuantizedCoord, Tolerance,
ToleranceClass, ToleranceGovernance,
};
use epiphany_ops::valuegen;
use epiphany_ops::{
AnomalousReplicaSegment, AuthorId, CausalContext, ChangeRegionTimeModelOp, ConflictId,
ConflictKind, ConflictKindRegistryId, ConflictRecord, ConflictRegistry,
ConflictResolutionState, CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp,
CreateMeasureOp, CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp,
CreateStaffGroupOp, CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp,
CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp,
DeleteRegionOp, DeleteRepeatStructureOp, 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, RepairKind, RepairKindRegistryId, RepairRecord,
ReplicaAnomalyReason, ReplicaAnomalyRegistryId, ResolutionAction, ResolutionRegistryId,
ResolveConflictPayload, RespellPitchOp, SerializedCanonicalInputs, SetCanvasLayoutDefaultsOp,
SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp,
SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp,
TransactionCategory, TransactionDescriptor, TransposeIntervalOp, TransposeOp,
TupletCompensation, TupletCompensationKind, UndoPolicy, UndoTransactionPayload,
};
use crate::rng::Rng;
/// Agent B's score-graph generators and shrinkers, re-exported so the testkit's
/// consumers have one import for *all* generators: `testkit::generators::graph`.
pub use epiphany_core::generators as graph;
// ===========================================================================
// Agent A — epiphany-determinism
// ===========================================================================
/// A canonical staff-space coordinate. Every `i64` unit count is valid.
pub fn quantized_coord(rng: &mut Rng) -> QuantizedCoord {
QuantizedCoord::from_units(rng.next_u64() as i64)
}
/// A finite canonical `f64`. Non-finite bit patterns (NaN/inf), a tiny fraction
/// of the space, are re-drawn; `-0.0` is canonicalized to `+0.0` by the type.
pub fn canonical_f64(rng: &mut Rng) -> CanonicalF64 {
for _ in 0..8 {
if let Some(c) = CanonicalF64::new(f64::from_bits(rng.next_u64())) {
return c;
}
}
CanonicalF64::new(0.0).unwrap()
}
/// A content hash (any 32 bytes are valid).
pub fn content_hash(rng: &mut Rng) -> ContentHash {
ContentHash(rng.array32())
}
/// A chunk id (newtype over [`ContentHash`]).
pub fn chunk_id_gen(rng: &mut Rng) -> ChunkId {
ChunkId(content_hash(rng))
}
/// A valid domain tag, drawn from the built-in `MUSC*` vocabulary.
pub fn domain_tag(rng: &mut Rng) -> DomainTag {
*rng.choose(&DomainTag::BUILTINS)
}
/// A typed tolerance (not a canonical-serialized type — generated for
/// completeness of the "every public type" charter, exercised by validation
/// tests rather than the byte round-trip).
pub fn tolerance(rng: &mut Rng) -> Tolerance {
let class = *rng.choose(&[
ToleranceClass::AcousticCents,
ToleranceClass::LayoutCoordinate,
ToleranceClass::QualityMetric,
ToleranceClass::TempoIntegration,
ToleranceClass::SolverResidual,
]);
let governance = *rng.choose(&[
ToleranceGovernance::Equality,
ToleranceGovernance::Validation,
ToleranceGovernance::Diagnostic,
]);
let absolute = (rng.range(1, 1_000_000) as f64) / 1_000_000.0;
Tolerance::absolute(class, absolute, governance).expect("finite positive tolerance")
}
// ===========================================================================
// Agent B — epiphany-core: identifiers, time, typed-object id
// ===========================================================================
/// A non-reserved replica identifier (never [`ReplicaId::SYSTEM_DERIVED`]).
pub fn replica_id(rng: &mut Rng) -> ReplicaId {
ReplicaId::from_entropy(rng.next_u64().to_le_bytes()).unwrap_or(ReplicaId(0x5151_5151))
}
/// An operation identifier with a non-reserved replica.
pub fn operation_id(rng: &mut Rng) -> OperationId {
OperationId::new(replica_id(rng), rng.next_u64())
}
/// An opaque author identifier.
pub fn author_id(rng: &mut Rng) -> AuthorId {
AuthorId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}
/// A content-addressed conflict identifier value.
pub fn conflict_id(rng: &mut Rng) -> ConflictId {
ConflictId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}
macro_rules! id_generator {
($(#[$m:meta])* $fn_name:ident -> $ty:ident) => {
$(#[$m])*
pub fn $fn_name(rng: &mut Rng) -> $ty {
$ty::new(replica_id(rng), rng.next_u64())
}
};
}
id_generator!(/// An event identifier.
event_id -> EventId);
id_generator!(/// A pitch identifier.
pitch_id -> PitchId);
id_generator!(/// A voice identifier.
voice_id -> VoiceId);
id_generator!(/// A staff identifier.
staff_id -> StaffId);
id_generator!(/// A staff-instance identifier.
staff_instance_id -> StaffInstanceId);
id_generator!(/// A staff-group identifier.
staff_group_id -> StaffGroupId);
id_generator!(/// A region identifier.
region_id -> RegionId);
id_generator!(/// An instrument identifier.
instrument_id -> InstrumentId);
id_generator!(/// A part-definition identifier.
part_definition_id -> PartDefinitionId);
id_generator!(/// A measure identifier.
measure_id -> MeasureId);
id_generator!(/// A barline-alignment-group identifier.
barline_alignment_group_id -> BarlineAlignmentGroupId);
id_generator!(/// A tuplet identifier.
tuplet_id -> TupletId);
id_generator!(/// A slur identifier.
slur_id -> SlurId);
id_generator!(/// A tie identifier.
tie_id -> TieId);
id_generator!(/// A beam identifier.
beam_id -> BeamId);
id_generator!(/// A spanner identifier.
spanner_id -> SpannerId);
id_generator!(/// A marker identifier.
marker_id -> MarkerId);
id_generator!(/// An analytical-annotation identifier.
analytical_annotation_id -> AnalyticalAnnotationId);
id_generator!(/// A comment identifier.
comment_id -> CommentId);
id_generator!(/// A graphic-object identifier.
graphic_object_id -> GraphicObjectId);
id_generator!(/// A graphic-gesture identifier.
graphic_gesture_id -> GraphicGestureId);
id_generator!(/// A time-signature identifier.
time_signature_id -> TimeSignatureId);
id_generator!(/// An analysis-layer identifier.
analysis_layer_id -> AnalysisLayerId);
id_generator!(/// A repeat-structure identifier.
repeat_structure_id -> RepeatStructureId);
id_generator!(/// A lyric-line identifier.
lyric_line_id -> LyricLineId);
id_generator!(/// A chord-symbol identifier.
chord_symbol_id -> ChordSymbolId);
id_generator!(/// A view identifier.
view_id -> ViewId);
id_generator!(/// An object-kind registry identifier.
object_kind_registry_id -> ObjectKindRegistryId);
id_generator!(/// A transaction identifier.
transaction_id -> TransactionId);
id_generator!(/// An integrity-anomaly identifier.
integrity_anomaly_id -> IntegrityAnomalyId);
/// A small (inline) rational time value with a non-zero denominator.
pub fn rational_time_small(rng: &mut Rng) -> RationalTime {
loop {
let num = (rng.next_u64() % (1 << 20)) as i64 - (1 << 19);
let den = rng.range(1, 4096) as i64;
if let Some(r) = RationalTime::new(num, den) {
return r;
}
}
}
/// A promoted (arbitrary-precision `Large`) rational time value. The numerator
/// is forced far enough beyond the inline `i32` range that even after dividing
/// by the (small) denominator's gcd the reduced numerator still exceeds
/// `i32::MAX`, so the value cannot demote to the inline arm.
pub fn rational_time_large(rng: &mut Rng) -> RationalTime {
// `i32::MAX * 16` guarantees `num / den > i32::MAX` for every den <= 13.
let big = (i32::MAX as i64) * 16 + (rng.next_u64() % 1_000_000_000) as i64;
let den = *rng.choose(&[1i64, 3, 7, 11, 13]);
RationalTime::new(big, den).expect("non-zero denominator")
}
/// A rational time value, half inline and half promoted — so the round-trip
/// corpus exercises both [`RationalTime`] arms.
pub fn rational_time(rng: &mut Rng) -> RationalTime {
if rng.boolean() {
rational_time_small(rng)
} else {
rational_time_large(rng)
}
}
/// A musical position (rational, possibly promoted).
pub fn musical_position(rng: &mut Rng) -> MusicalPosition {
MusicalPosition(rational_time(rng))
}
/// A musical duration (rational, possibly promoted).
pub fn musical_duration(rng: &mut Rng) -> MusicalDuration {
MusicalDuration(rational_time(rng))
}
/// A wall-clock time (signed nanoseconds).
pub fn wallclock_time(rng: &mut Rng) -> WallClockTime {
WallClockTime(rng.next_u64() as i64)
}
/// A wall-clock duration (signed nanoseconds).
pub fn wallclock_duration(rng: &mut Rng) -> WallClockDuration {
WallClockDuration(rng.next_u64() as i64)
}
/// A tagged object identifier across the **whole** graph identity family — every
/// [`TypedObjectId`] variant including the variable-width `Registered` form, so
/// the round-trip harness exercises every discriminant and both encodings.
pub fn typed_object_id(rng: &mut Rng) -> TypedObjectId {
match rng.below(28) {
0 => TypedObjectId::Event(event_id(rng)),
1 => TypedObjectId::Pitch(pitch_id(rng)),
2 => TypedObjectId::Voice(voice_id(rng)),
3 => TypedObjectId::Staff(staff_id(rng)),
4 => TypedObjectId::StaffInstance(staff_instance_id(rng)),
5 => TypedObjectId::StaffGroup(staff_group_id(rng)),
6 => TypedObjectId::Region(region_id(rng)),
7 => TypedObjectId::Instrument(instrument_id(rng)),
8 => TypedObjectId::PartDefinition(part_definition_id(rng)),
9 => TypedObjectId::Measure(measure_id(rng)),
10 => TypedObjectId::BarlineAlignmentGroup(barline_alignment_group_id(rng)),
11 => TypedObjectId::Slur(slur_id(rng)),
12 => TypedObjectId::Tie(tie_id(rng)),
13 => TypedObjectId::Beam(beam_id(rng)),
14 => TypedObjectId::Spanner(spanner_id(rng)),
15 => TypedObjectId::Marker(marker_id(rng)),
16 => TypedObjectId::AnalyticalAnnotation(analytical_annotation_id(rng)),
17 => TypedObjectId::Comment(comment_id(rng)),
18 => TypedObjectId::GraphicObject(graphic_object_id(rng)),
19 => TypedObjectId::GraphicGesture(graphic_gesture_id(rng)),
20 => TypedObjectId::TimeSignature(time_signature_id(rng)),
21 => TypedObjectId::AnalysisLayer(analysis_layer_id(rng)),
22 => TypedObjectId::Tuplet(tuplet_id(rng)),
23 => TypedObjectId::RepeatStructure(repeat_structure_id(rng)),
24 => TypedObjectId::LyricLine(lyric_line_id(rng)),
25 => TypedObjectId::ChordSymbol(chord_symbol_id(rng)),
26 => TypedObjectId::View(view_id(rng)),
_ => TypedObjectId::Registered(
object_kind_registry_id(rng),
((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
),
}
}
// ===========================================================================
// Agent C — epiphany-ops: real operation envelopes
// ===========================================================================
/// The shared object-identifier namespace authoring replicas mint *references*
/// into, so operations from different replicas address the *same* events,
/// pitches, and voices — which is what makes their reductions interact
/// (tombstones, already-applied, field conflicts) and what gives the convergence
/// harness something non-trivial to converge.
const OBJ_REPLICA: ReplicaId = ReplicaId(0x0B7E_C700);
fn obj_event(n: u64) -> EventId {
EventId::new(OBJ_REPLICA, n)
}
fn obj_pitch(n: u64) -> PitchId {
PitchId::new(OBJ_REPLICA, n)
}
/// A conflict-resolution action (every core and registered variant,
/// including `Dismiss`).
pub fn resolution_action(rng: &mut Rng) -> ResolutionAction {
match rng.below(6) {
0 => ResolutionAction::AcceptLoser,
1 => ResolutionAction::KeepWinner,
2 => ResolutionAction::Override {
override_operation: operation_id(rng),
},
3 => ResolutionAction::Reanchor {
new_target: typed_object_id(rng),
},
4 => ResolutionAction::Dismiss,
_ => ResolutionAction::Registered(ResolutionRegistryId(
((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
)),
}
}
/// An undo policy (`StrictInverse`/`BestEffort`/`Cascade`).
pub fn undo_policy(rng: &mut Rng) -> UndoPolicy {
*rng.choose(&[
UndoPolicy::StrictInverse,
UndoPolicy::BestEffort,
UndoPolicy::Cascade,
])
}
/// A conflict kind covering every core and extension variant.
pub fn conflict_kind(rng: &mut Rng) -> ConflictKind {
match rng.below(6) {
0 => ConflictKind::StructuralFieldCollision {
winner: operation_id(rng),
loser: operation_id(rng),
field: FieldPath(String::from("event.pitch")),
},
1 => ConflictKind::TransactionConflict {
transaction: transaction_id(rng),
failed_members: vec![operation_id(rng), operation_id(rng)],
},
2 => ConflictKind::TombstonedTarget {
target: typed_object_id(rng),
operation: operation_id(rng),
},
3 => ConflictKind::ReanchorFailure {
original_referent: typed_object_id(rng),
referencing_object: typed_object_id(rng),
},
4 => ConflictKind::TimeModelMigrationFailure {
region: region_id(rng),
incompatible_events: vec![typed_object_id(rng), typed_object_id(rng)],
},
_ => ConflictKind::ExtensionConflict {
kind_id: ConflictKindRegistryId(
((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
),
details: rng.byte_vec(0, 24),
},
}
}
/// A conflict resolution state covering every variant.
pub fn conflict_resolution_state(rng: &mut Rng) -> ConflictResolutionState {
match rng.below(3) {
0 => ConflictResolutionState::Unresolved,
1 => ConflictResolutionState::Resolved {
by: operation_id(rng),
action: resolution_action(rng),
},
_ => ConflictResolutionState::Dismissed {
by: operation_id(rng),
},
}
}
/// A self-consistent content-addressed conflict record.
pub fn conflict_record(rng: &mut Rng) -> ConflictRecord {
let mut record = ConflictRecord::new(
conflict_kind(rng),
vec![operation_id(rng), operation_id(rng)],
vec![typed_object_id(rng), typed_object_id(rng)],
);
record.resolution_state = conflict_resolution_state(rng);
record
}
/// A canonical conflict registry containing a few generated records.
pub fn conflict_registry(rng: &mut Rng) -> ConflictRegistry {
let mut registry = ConflictRegistry::new();
for _ in 0..rng.range_usize(0, 4) {
registry.insert(conflict_record(rng));
}
registry
}
/// A typed precondition failure (every core and registered variant).
pub fn precondition_failure_reason(rng: &mut Rng) -> PreconditionFailureReason {
match rng.below(19) {
0 => PreconditionFailureReason::TargetMissing,
1 => PreconditionFailureReason::TargetTombstoned,
2 => PreconditionFailureReason::WrongRegionTimeModel,
3 => PreconditionFailureReason::TupletCompensationInvalid,
4 => PreconditionFailureReason::EventDurationInvalid,
5 => PreconditionFailureReason::PositionOutsideRegion,
6 => PreconditionFailureReason::PitchSpaceMismatch,
7 => PreconditionFailureReason::VoiceMissing,
8 => PreconditionFailureReason::ContainerNotEmpty,
9 => PreconditionFailureReason::TempoMapMalformed,
10 => PreconditionFailureReason::SystemDerivedContentImmutable,
11 => PreconditionFailureReason::RecreateContentMismatch,
// Push 4a (previously a decoder hole this generator never reached —
// P13-S20, closed by genesis tranche G3b's row 12a repair).
12 => PreconditionFailureReason::AcousticRealizationPinned,
13 => PreconditionFailureReason::TranspositionOutOfRange,
// Genesis tranche G3b.
14 => PreconditionFailureReason::MeasureMeterMismatch,
15 => PreconditionFailureReason::MeasureOutOfOrder,
16 => PreconditionFailureReason::MeasureOrderUnverifiable,
17 => PreconditionFailureReason::ExtensionPrecondition(ExtensionPreconditionId(
rng.next_u64() as u128,
)),
_ => PreconditionFailureReason::Registered(PreconditionFailureRegistryId(
rng.next_u64() as u128
)),
}
}
/// A no-op reason covering every variant.
pub fn no_op_reason(rng: &mut Rng) -> NoOpReason {
match rng.below(5) {
0 => NoOpReason::TargetTombstoned,
1 => NoOpReason::AlreadyApplied,
2 => NoOpReason::SupersededByLaterOperation {
superseder: operation_id(rng),
},
3 => NoOpReason::PreconditionFailedUnderReduction {
reason: precondition_failure_reason(rng),
},
_ => NoOpReason::TransactionConflict,
}
}
/// A re-anchor reason covering every variant.
pub fn reanchor_reason(rng: &mut Rng) -> ReanchorReason {
match rng.below(7) {
0 => ReanchorReason::SameVoiceNearer,
1 => ReanchorReason::SameStaffInstanceNearer,
2 => ReanchorReason::SameStaffNearer,
3 => ReanchorReason::SameRegionNearer,
4 => ReanchorReason::ExplicitFallback,
5 => ReanchorReason::SameCanvasNearer,
_ => ReanchorReason::DeclaredByExtension(ReanchorReasonRegistryId(rng.next_u64() as u128)),
}
}
/// A tuplet compensation result covering every variant.
pub fn tuplet_compensation_kind(rng: &mut Rng) -> TupletCompensationKind {
*rng.choose(&[
TupletCompensationKind::ReplaceWithRest,
TupletCompensationKind::RewriteTuplets,
TupletCompensationKind::CascadeDeleteTuplets,
])
}
/// A repair kind covering every core and registered variant.
pub fn repair_kind(rng: &mut Rng) -> RepairKind {
match rng.below(8) {
0 => RepairKind::Reanchored {
from: typed_object_id(rng),
to: typed_object_id(rng),
reason: reanchor_reason(rng),
},
1 => RepairKind::SpannerTruncated {
removed_members: vec![typed_object_id(rng), typed_object_id(rng)],
},
2 => RepairKind::Orphaned,
3 => RepairKind::CascadeDeleted,
4 => RepairKind::AttachmentTombstoned,
5 => RepairKind::VoicePromoted {
from: voice_id(rng),
to: voice_id(rng),
},
6 => RepairKind::TupletCompensated {
compensation_kind: tuplet_compensation_kind(rng),
},
_ => RepairKind::Registered(RepairKindRegistryId(rng.next_u64() as u128)),
}
}
/// A complete repair record.
pub fn repair_record(rng: &mut Rng) -> RepairRecord {
RepairRecord {
kind: repair_kind(rng),
target: typed_object_id(rng),
}
}
/// An operation effect covering every variant.
pub fn operation_effect(rng: &mut Rng) -> OperationEffect {
match rng.below(5) {
0 => OperationEffect::Applied,
1 => OperationEffect::AppliedWithRepair {
repairs: vec![repair_record(rng)],
},
2 => OperationEffect::Conflicted {
conflict: ConflictId(rng.next_u64() as u128),
},
3 => OperationEffect::TombstonedTarget {
target: typed_object_id(rng),
},
_ => OperationEffect::NoOp {
reason: no_op_reason(rng),
},
}
}
/// The result of a re-anchor attempt (every variant).
pub fn reanchor_result(rng: &mut Rng) -> ReanchorResult {
match rng.below(5) {
0 => ReanchorResult::Reanchored {
new_target: typed_object_id(rng),
reason: reanchor_reason(rng),
},
1 => ReanchorResult::TombstonedTarget,
2 => ReanchorResult::Orphaned,
3 => ReanchorResult::Conflicted {
conflict: ConflictId(rng.next_u64() as u128),
},
_ => ReanchorResult::CascadeDeleted,
}
}
/// An integrity anomaly kind covering every core and registered variant.
pub fn integrity_anomaly_kind(rng: &mut Rng) -> IntegrityAnomalyKind {
match rng.below(4) {
0 => IntegrityAnomalyKind::SystemIdentifierCollision {
kind: match rng.below(3) {
0 => ObjectKind::Voice,
1 => ObjectKind::Pitch,
_ => ObjectKind::Registered(OperationKindRegistryId(rng.next_u64() as u128)),
},
colliding_counter: rng.next_u64(),
input_set_a: SerializedCanonicalInputs(rng.byte_vec(0, 16)),
input_set_b: SerializedCanonicalInputs(rng.byte_vec(0, 16)),
},
1 => IntegrityAnomalyKind::OperationSlotEquivocated {
operation_id: operation_id(rng),
},
2 => IntegrityAnomalyKind::ReplicaStreamQuarantined {
replica: replica_id(rng),
first_bad_counter: rng.next_u64(),
},
_ => IntegrityAnomalyKind::Registered(IntegrityAnomalyRegistryId(rng.next_u64() as u128)),
}
}
/// A content-derived integrity anomaly.
pub fn integrity_anomaly(rng: &mut Rng) -> IntegrityAnomaly {
IntegrityAnomaly::new(integrity_anomaly_kind(rng))
}
/// A per-replica anomaly reason covering core and registered forms.
pub fn replica_anomaly_reason(rng: &mut Rng) -> ReplicaAnomalyReason {
if rng.boolean() {
ReplicaAnomalyReason::HlcMonotonicityViolation {
violating_pair: (operation_id(rng), operation_id(rng)),
}
} else {
ReplicaAnomalyReason::Registered(ReplicaAnomalyRegistryId(rng.next_u64() as u128))
}
}
/// An anomalous replica segment with a sorted excluded-id list.
pub fn anomalous_replica_segment(rng: &mut Rng) -> AnomalousReplicaSegment {
let replica = replica_id(rng);
let first_bad_counter = rng.range(0, 32);
AnomalousReplicaSegment {
replica,
first_bad_counter,
reason: replica_anomaly_reason(rng),
excluded: (first_bad_counter..first_bad_counter + rng.range(1, 4))
.map(|counter| OperationId::new(replica, counter))
.collect(),
}
}
/// An object-existence state covering live and tombstoned forms.
pub fn object_state(rng: &mut Rng) -> ObjectState {
if rng.boolean() {
ObjectState::Live
} else {
ObjectState::Tombstoned {
deleted_by: operation_id(rng),
minted_by: operation_id(rng),
}
}
}
/// A pending reason covering every blocker category.
pub fn pending_reason(rng: &mut Rng) -> PendingReason {
let blocker = operation_id(rng);
match rng.below(4) {
0 => PendingReason::MissingCausalPredecessor { missing: blocker },
1 => PendingReason::DependsOnEquivocated { on: blocker },
2 => PendingReason::DependsOnExcluded { on: blocker },
_ => PendingReason::DependsOnPending { on: blocker },
}
}
/// A random representative operation payload over a shared id space of `events`
/// events and `pitches` pitches. Covers the primitive reduction disciplines
/// (Chapter 6 §6.10: insert, delete, respell, cross-cutting create,
/// layout-semantic break, time-model change, transaction declaration,
/// registered extension op) and the two meta-operations (resolve-conflict and
/// undo-transaction).
///
/// Zero-sized id spaces are treated as size 1 (the shared object namespace
/// always has at least one addressable id), so this never calls
/// [`Rng::below`](crate::rng::Rng::below) with a zero bound.
pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationPayload {
let events = events.max(1);
let pitches = pitches.max(1);
// ~1 in 6 operations is a meta-operation (resolve-conflict / undo).
match rng.below(12) {
10 => {
return OperationPayload::ResolveConflict(ResolveConflictPayload {
target: ConflictId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128),
action: resolution_action(rng),
})
}
11 => {
return OperationPayload::UndoTransaction(UndoTransactionPayload {
target: TransactionId::new(OBJ_REPLICA, rng.below(events)),
policy: undo_policy(rng),
})
}
_ => {}
}
let kind = match rng.below(40) {
0 => {
let pitches = if rng.boolean() {
vec![obj_pitch(rng.below(pitches))]
} else {
vec![]
};
OperationKind::InsertEvent(InsertEventOp {
staff_instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
event: valuegen::insert_event_value(
obj_event(rng.below(events)),
VoiceId::new(OBJ_REPLICA, rng.below(4)),
MusicalPosition(RationalTime::from_int(rng.below(events) as i32)),
MusicalDuration::whole(),
&pitches,
),
})
}
1 => OperationKind::DeleteEvent(DeleteEventOp {
event: obj_event(rng.below(events)),
tuplet_compensation: TupletCompensation::NotInTuplet,
}),
2 => OperationKind::RespellPitch(RespellPitchOp {
pitch: obj_pitch(rng.below(pitches)),
spelling: valuegen::spelling(rng.below(4) as u8 + 1),
}),
3 => OperationKind::CreateCrossCutting(CreateCrossCuttingOp {
structure: CrossCuttingValue::Slur(valuegen::slur(
SlurId::new(OBJ_REPLICA, rng.below(events)),
obj_event(rng.below(events)),
obj_event(rng.below(events)),
)),
}),
4 => OperationKind::SetUserSystemBreak(SetUserSystemBreakOp {
region: RegionId::new(OBJ_REPLICA, 0),
anchor: valuegen::region_start_anchor(
RegionId::new(OBJ_REPLICA, 0),
MusicalPosition(RationalTime::from_int(rng.below(4) as i32)),
),
present: rng.boolean(),
}),
5 => OperationKind::ChangeRegionTimeModel(ChangeRegionTimeModelOp {
region: RegionId::new(OBJ_REPLICA, rng.below(2)),
new_time_model: match rng.below(3) {
0 => valuegen::metric_model(),
1 => valuegen::proportional_model(),
_ => valuegen::aleatoric_model(),
},
declared_incompatible: Vec::new(),
remapping: PositionRemapping::PreserveTime,
}),
6 => OperationKind::DeclareTransaction(TransactionDescriptor {
id: TransactionId::new(OBJ_REPLICA, rng.below(events)),
label: String::from("edit"),
category: Some(*rng.choose(&[
TransactionCategory::NoteEntry,
TransactionCategory::Structural,
TransactionCategory::Layout,
])),
}),
// Group 1 (M2): event & pitch leaf-field ops over the shared id space.
7 => OperationKind::ModifyEvent(ModifyEventOp {
event: valuegen::insert_event_value(
obj_event(rng.below(events)),
VoiceId::new(OBJ_REPLICA, rng.below(4)),
MusicalPosition(RationalTime::from_int(rng.below(events) as i32)),
MusicalDuration::whole(),
&[obj_pitch(rng.below(pitches))],
),
}),
8 => OperationKind::Transpose(TransposeOp {
targets: vec![obj_pitch(rng.below(pitches))],
chromatic_steps: rng.below(5) as i32 - 2,
}),
9 => OperationKind::InsertIdentifiedPitch(InsertIdentifiedPitchOp {
event: obj_event(rng.below(events)),
pitch: valuegen::identified_pitch(obj_pitch(rng.below(pitches))),
}),
10 => OperationKind::DeleteIdentifiedPitch(DeleteIdentifiedPitchOp {
pitch: obj_pitch(rng.below(pitches)),
}),
11 => OperationKind::ModifyIdentifiedPitch(ModifyIdentifiedPitchOp {
pitch: obj_pitch(rng.below(pitches)),
value: valuegen::pitch_value_nth(rng.below(4) as u8 + 1),
}),
// Group 2 (M2): cross-cutting CRUD over the shared id space.
12 => OperationKind::DeleteCrossCutting(DeleteCrossCuttingOp {
structure: TypedObjectId::Slur(SlurId::new(OBJ_REPLICA, rng.below(events))),
}),
13 => OperationKind::ModifyCrossCutting(ModifyCrossCuttingOp {
structure: CrossCuttingValue::Slur(valuegen::slur(
SlurId::new(OBJ_REPLICA, rng.below(events)),
obj_event(rng.below(events)),
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)),
}),
// Group 4 (M2d): score settings (LWW) over the shared id space.
20 => OperationKind::SetMetadata(SetMetadataOp {
metadata: valuegen::score_metadata(rng.below(3) as u8),
}),
21 => OperationKind::SetMetricGrid(SetMetricGridOp {
region: RegionId::new(OBJ_REPLICA, rng.below(2)),
grid: rng.boolean().then(valuegen::metric_grid),
}),
22 => OperationKind::SetUserPageBreak(SetUserPageBreakOp {
region: RegionId::new(OBJ_REPLICA, 0),
anchor: valuegen::region_start_anchor(
RegionId::new(OBJ_REPLICA, 0),
MusicalPosition(RationalTime::from_int(rng.below(4) as i32)),
),
present: rng.boolean(),
}),
// Phase-3 tranche: staff mint, meter/tempo overwrites, layout advisory
// over the shared id space. The signature's value derives from its id
// so an id re-carry is byte-identical (the idempotent mint branch);
// distinct ids give distinct values (the differing-value branch).
23 => OperationKind::CreateStaff(CreateStaffOp {
staff: valuegen::staff(
StaffId::new(OBJ_REPLICA, rng.below(2)),
InstrumentId::new(OBJ_REPLICA, 0),
),
}),
24 => {
let region = RegionId::new(OBJ_REPLICA, rng.below(2));
let signature = rng.below(2);
OperationKind::SetTimeSignature(SetTimeSignatureOp {
region,
anchor: valuegen::region_start_anchor(
region,
MusicalPosition(RationalTime::from_int(rng.below(3) as i32 * 4)),
),
time_signature: rng.boolean().then(|| {
valuegen::time_signature(
TimeSignatureId::new(OBJ_REPLICA, signature),
signature as u16 + 3,
)
}),
})
}
25 => {
let region = RegionId::new(OBJ_REPLICA, rng.below(2));
let at = MusicalPosition(RationalTime::from_int(rng.below(3) as i32 * 4));
OperationKind::SetTempoSegment(SetTempoSegmentOp {
region: rng.boolean().then_some(region),
start: valuegen::region_start_anchor(region, at.clone()),
segment: rng.boolean().then(|| {
valuegen::tempo_segment(region, at, 60.0 + rng.below(4) as f64 * 30.0)
}),
})
}
26 => OperationKind::SetStaffLayout(SetStaffLayoutOp {
staff_instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
instrument_override: None,
staff_lines_override: rng
.boolean()
.then(epiphany_core::StaffLineConfiguration::default),
visible: rng.boolean(),
}),
// Repeat authoring (schema-major-2 revision) over the shared
// event-id space, so anchors sometimes resolve and sometimes miss.
27 => OperationKind::CreateRepeatStructure(CreateRepeatStructureOp {
repeat: if rng.boolean() {
valuegen::repeat_structure(
RepeatStructureId::new(OBJ_REPLICA, rng.below(2)),
obj_event(rng.below(events)),
obj_event(rng.below(events)),
)
} else {
valuegen::volta_repeat(
RepeatStructureId::new(OBJ_REPLICA, rng.below(2)),
obj_event(rng.below(events)),
obj_event(rng.below(events)),
)
},
}),
28 => OperationKind::DeleteRepeatStructure(DeleteRepeatStructureOp {
repeat: RepeatStructureId::new(OBJ_REPLICA, rng.below(2)),
}),
// Push 4a (previously absent from this corpus — see row 28's contract
// note): the faithful transpose over the shared pitch-id space.
29 => OperationKind::TransposeInterval(TransposeIntervalOp {
targets: (0..1 + rng.below(2))
.map(|_| obj_pitch(rng.below(pitches)))
.collect(),
interval: TranspositionInterval {
diatonic_steps: rng.below(5) as i32 - 2,
chromatic_steps: rng.below(9) as i32 - 4,
},
}),
// Genesis tranche G1 (previously absent from this corpus — see row
// 28's contract note): mint an instrument on the score root.
30 => OperationKind::CreateInstrument(CreateInstrumentOp {
instrument: valuegen::instrument(InstrumentId::new(OBJ_REPLICA, rng.below(2))),
}),
// Genesis tranche G2a: the two major-0 settings setters.
31 => OperationKind::SetCanvasLayoutDefaults(SetCanvasLayoutDefaultsOp {
layout_defaults: valuegen::canvas_layout_defaults(rng.below(3) as u8),
}),
32 => OperationKind::SetSpellingPrecedence(SetSpellingPrecedenceOp {
precedence: valuegen::spelling_precedence(rng.below(3) as u8),
}),
// Genesis tranche G2b: the sole genesis payload born at schema major 3.
33 => OperationKind::SetTuningContext(SetTuningContextOp {
settings: valuegen::tuning_context_settings(rng.below(3) as u8),
}),
// Genesis tranche G3a: the four remaining root-level mints, over the
// shared staff/analysis-layer id spaces.
34 => OperationKind::CreateStaffGroup(CreateStaffGroupOp {
group: valuegen::staff_group(
StaffGroupId::new(OBJ_REPLICA, rng.below(2)),
vec![StaffId::new(OBJ_REPLICA, rng.below(2))],
),
}),
35 => OperationKind::CreatePartDefinition(CreatePartDefinitionOp {
part: valuegen::part_definition(
PartDefinitionId::new(OBJ_REPLICA, rng.below(2)),
vec![StaffId::new(OBJ_REPLICA, rng.below(2))],
),
}),
36 => OperationKind::CreateAnalysisLayer(CreateAnalysisLayerOp {
layer: valuegen::analysis_layer(AnalysisLayerId::new(OBJ_REPLICA, rng.below(2))),
}),
37 => OperationKind::CreateView(CreateViewOp {
view: valuegen::view(
ViewId::new(OBJ_REPLICA, rng.below(2)),
vec![AnalysisLayerId::new(OBJ_REPLICA, rng.below(2))],
),
}),
// Genesis tranche G3b: append a measure onto the shared
// staff-instance id space.
38 => OperationKind::CreateMeasure(CreateMeasureOp {
instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
measure: valuegen::measure(
MeasureId::new(OBJ_REPLICA, rng.below(2)),
TimeSignatureId::new(OBJ_REPLICA, rng.below(2)),
rng.below(4) as u32,
),
}),
_ => OperationKind::Registered(
OperationKindRegistryId(rng.next_u64() as u128),
rng.byte_vec(0, 16),
),
};
OperationPayload::Primitive(kind)
}
/// A well-formed set of real [`OperationEnvelope`]s authored by `n_replicas`
/// replicas over a shared id space, **honoring the HLC authoring contract**
/// (spec §"Identifiers", the `OperationStamp` requirement): every operation's
/// stamp is strictly greater than the stamp of every operation in its causal
/// context. This is achieved with the standard hybrid-logical-clock send rule —
/// a new stamp is `max(local clock reading, this replica's previous stamp, the
/// maximum stamp over the declared causal predecessors)`, with the logical
/// counter bumped on ties — so the canonical reduction order (which sorts by the
/// HLC tuple) genuinely places causal predecessors before successors.
///
/// The DVV causal context references only **prior** operations: the author
/// always sees its own contiguous history, plus a random prefix of each other
/// replica's history. Concurrency across replicas is what makes delivery order
/// differ from the canonical reduction order.
///
/// [`assert_causal_order_respected`](crate::convergence::assert_causal_order_respected)
/// verifies the resulting histories actually satisfy the authoring contract.
/// A reusable authoring context: each [`Session::author`] call mints one
/// well-formed, causally-conformant envelope on a chosen replica via the HLC
/// send rule + prior-history DVV described on [`operation_envelopes`].
struct Session {
counters: Vec<u64>,
clocks: Vec<i64>,
/// Per-replica stamp history `(physical, logical)`, indexed by counter.
/// Stamps are monotonic within a replica, so `stamps[rr][k]` is the max over
/// `0..=k`.
stamps: Vec<Vec<(i64, u32)>>,
out: Vec<OperationEnvelope>,
}
impl Session {
fn new(n_replicas: usize) -> Self {
let n = n_replicas.max(1);
Session {
counters: vec![0; n],
clocks: vec![0; n],
stamps: vec![Vec::new(); n],
out: Vec::new(),
}
}
fn n_replicas(&self) -> usize {
self.counters.len()
}
/// Authors one operation on replica `r` carrying `payload`, honoring the HLC
/// authoring contract (the new stamp strictly outranks every causal
/// predecessor) with a prior-history DVV (own history always seen; others
/// sampled).
fn author(&mut self, rng: &mut Rng, r: usize, payload: OperationPayload) {
self.author_tx(rng, r, None, payload);
}
/// As [`Session::author`], additionally stamping the envelope as a member
/// of `tx`. Same-replica sequential authorship makes each member causally
/// cover its transaction descriptor (the descriptor-precedence rule).
fn author_tx(
&mut self,
rng: &mut Rng,
r: usize,
tx: Option<TransactionId>,
payload: OperationPayload,
) {
let n_replicas = self.counters.len();
let replica = ReplicaId(r as u64 + 1);
let c = self.counters[r];
let id = OperationId::new(replica, c);
let mut ctx = CausalContext::new();
let mut pred_max = (0i64, 0u32);
if c > 0 {
ctx = ctx.with_seen(replica, c - 1);
pred_max = pred_max.max(self.stamps[r][(c - 1) as usize]);
}
for rr in 0..n_replicas {
if rr == r {
continue;
}
let known = self.counters[rr];
if known > 0 && rng.boolean() {
let k = rng.below(known);
ctx = ctx.with_seen(ReplicaId(rr as u64 + 1), k);
pred_max = pred_max.max(self.stamps[rr][k as usize]);
}
}
self.clocks[r] += rng.below(3) as i64;
let pt = self.clocks[r];
let prev = self.stamps[r].last().copied().unwrap_or((0, 0));
let l = pt.max(prev.0).max(pred_max.0);
let logical = if l == prev.0 && l == pred_max.0 {
prev.1.max(pred_max.1) + 1
} else if l == prev.0 {
prev.1 + 1
} else if l == pred_max.0 {
pred_max.1 + 1
} else {
0
};
self.stamps[r].push((l, logical));
self.counters[r] += 1;
let env = OperationEnvelope {
id,
author: AuthorId(replica.0 as u128),
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(l), logical), id),
causal_context: ctx,
transaction: tx,
payload,
};
debug_assert!(epiphany_ops::well_formed(&env).is_ok());
self.out.push(env);
}
}
/// A well-formed set of real [`OperationEnvelope`]s authored by `n_replicas`
/// replicas over a shared id space, **honoring the HLC authoring contract**
/// (spec §"Identifiers", the `OperationStamp` requirement): every operation's
/// stamp is strictly greater than the stamp of every operation in its causal
/// context. This is achieved with the standard hybrid-logical-clock send rule —
/// a new stamp is `max(local clock reading, this replica's previous stamp, the
/// maximum stamp over the declared causal predecessors)`, with the logical
/// counter bumped on ties — so the canonical reduction order (which sorts by the
/// HLC tuple) genuinely places causal predecessors before successors.
///
/// The DVV causal context references only **prior** operations: the author
/// always sees its own contiguous history, plus a random prefix of each other
/// replica's history. Concurrency across replicas is what makes delivery order
/// differ from the canonical reduction order.
///
/// [`assert_causal_order_respected`](crate::convergence::assert_causal_order_respected)
/// verifies the resulting histories actually satisfy the authoring contract.
pub fn operation_envelopes(
rng: &mut Rng,
n_ops: usize,
n_replicas: usize,
events: u64,
pitches: u64,
) -> Vec<OperationEnvelope> {
let mut session = Session::new(n_replicas);
for _ in 0..n_ops {
let r = rng.below(session.n_replicas() as u64) as usize;
let payload = operation_payload(rng, events, pitches);
session.author(rng, r, payload);
}
session.out
}
/// A real materialized state produced by reducing a generated conformant
/// operation set. This is preferable to assembling reducer-owned invariants by
/// hand for ordinary property tests.
pub fn materialized_state(rng: &mut Rng, n_ops: usize) -> MaterializedState {
let envelopes = operation_envelopes(rng, n_ops, 3, 8, 8);
let mut set = OperationSet::new();
set.accept_all(envelopes);
set.reduce()
}
/// Bars per staff in [`two_staff_edit_session`].
pub const TWO_STAFF_BARS: u64 = 50;
/// Half-note events per 4/4 bar (so the session spans exactly [`TWO_STAFF_BARS`]).
const EVENTS_PER_BAR: u64 = 2;
/// Per-staff event count in [`two_staff_edit_session`]: [`TWO_STAFF_BARS`] bars ×
/// two half-notes per 4/4 bar.
pub const TWO_STAFF_EVENTS_PER_STAFF: u64 = TWO_STAFF_BARS * EVENTS_PER_BAR;
/// An `InsertEvent` payload minting `event` (with `pitch`) into the given
/// staff-instance / voice as the `index`-th half-note (position `index/2`
/// whole-notes, i.e. two per 4/4 bar).
fn insert_at(instance: u64, voice: u64, event: u64, pitch: u64, index: u64) -> OperationPayload {
OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp {
staff_instance: StaffInstanceId::new(OBJ_REPLICA, instance),
event: valuegen::insert_event_value(
obj_event(event),
VoiceId::new(OBJ_REPLICA, voice),
MusicalPosition(RationalTime::new(index as i64, EVENTS_PER_BAR as i64).unwrap()),
MusicalDuration(RationalTime::new(1, EVENTS_PER_BAR as i64).unwrap()),
&[obj_pitch(pitch)],
),
}))
}
/// The v0 criterion-1 scenario, **instantiated** (not merely modeled): a
/// two-staff score built by two replicas. Each staff (instance 0 / voice 0 and
/// instance 1 / voice 1) is filled with [`TWO_STAFF_EVENTS_PER_STAFF`] events
/// spanning ~50 bars, authored by *both* replicas (alternating), so both staves
/// are genuinely populated. A batch of overlapping deletes and respellings over
/// the shared id space then makes reduction order — not delivery order — decide
/// the materialized result. [`assert_two_staff_populated`] checks both staves
/// actually survive reduction.
pub fn two_staff_edit_session(rng: &mut Rng) -> Vec<OperationEnvelope> {
let n = TWO_STAFF_EVENTS_PER_STAFF;
let mut session = Session::new(2);
for i in 0..n {
session.author(rng, (i % 2) as usize, insert_at(0, 0, i, i, i));
session.author(
rng,
((i + 1) % 2) as usize,
insert_at(1, 1, n + i, n + i, i),
);
}
let total = 2 * n;
for _ in 0..80 {
let r = rng.below(2) as usize;
let payload = if rng.boolean() {
OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp {
event: obj_event(rng.below(total)),
tuplet_compensation: TupletCompensation::NotInTuplet,
}))
} else {
OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
pitch: obj_pitch(rng.below(total)),
spelling: valuegen::spelling(rng.below(4) as u8 + 1),
}))
};
session.author(rng, r, payload);
}
session.out
}
/// Asserts both staves of a [`two_staff_edit_session`] are actually populated:
/// each staff's voice is live and at least one of its events survives reduction.
/// (The reducer materializes voices/events but not staff-instance objects, so
/// liveness is checked at the voice and event level.)
pub fn assert_two_staff_populated(envelopes: &[OperationEnvelope]) {
let mut set = OperationSet::new();
set.accept_all(envelopes.iter().cloned());
let state = set.reduce();
let n = TWO_STAFF_EVENTS_PER_STAFF;
let voice_live = |v: u64| {
matches!(
state
.objects
.get(&TypedObjectId::Voice(VoiceId::new(OBJ_REPLICA, v))),
Some(ObjectState::Live)
)
};
assert!(voice_live(0), "staff 0 voice is not live after reduction");
assert!(voice_live(1), "staff 1 voice is not live after reduction");
let live_events = |lo: u64, hi: u64| {
(lo..hi)
.filter(|e| {
matches!(
state.objects.get(&TypedObjectId::Event(obj_event(*e))),
Some(ObjectState::Live)
)
})
.count()
};
assert!(live_events(0, n) > 0, "staff 0 has no live events");
assert!(live_events(n, 2 * n) > 0, "staff 1 has no live events");
}
/// Whole-note position past which [`graph_edit_session`] inserts, chosen to
/// clear `valid_score`'s base content (quarter-note events in `[0, 1)`).
const GRAPH_SESSION_OFFSET: i64 = 4;
/// An `InsertEvent` payload targeting a **real** base voice (`staff_instance`
/// must be the voice's actual container), minting `event`/`pitch` under
/// [`OBJ_REPLICA`] as the `index`-th half-note past the base content. Unlike
/// [`insert_at`], this addresses ids that already exist in a base [`graph`]
/// score, so the payload survives [`OperationSet::reduce_onto`] (which rejects
/// inserts into unknown voices) rather than only the base-free reducer.
fn insert_into(
staff_instance: StaffInstanceId,
voice: VoiceId,
event: u64,
pitch: u64,
index: u64,
) -> OperationPayload {
OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp {
staff_instance,
// position = GRAPH_SESSION_OFFSET + index/2 (two half-notes per 4/4 bar).
event: valuegen::insert_event_value(
obj_event(event),
voice,
MusicalPosition(
RationalTime::new(
GRAPH_SESSION_OFFSET * EVENTS_PER_BAR as i64 + index as i64,
2,
)
.unwrap(),
),
MusicalDuration(RationalTime::new(1, 2).unwrap()),
&[obj_pitch(pitch)],
),
}))
}
/// The graph-level twin of [`two_staff_edit_session`]: a real ~50-bar edit
/// session targeting the **actual** voices of `base`, for the
/// [`OperationSet::reduce_onto`] convergence gate (acceptance criterion 1).
///
/// Two replicas alternately insert [`TWO_STAFF_EVENTS_PER_STAFF`] half-note
/// events past the base content into the base's first two voices (so both
/// staves are genuinely edited), then concurrently respell and delete over the
/// shared minted id space, so the canonical reduction — not delivery order —
/// decides the materialized `Score`. Returns the targeted voices alongside the
/// envelopes (the convergence harness checks each one actually grew).
///
/// Requires a base with at least one voice; callers pass a base scanned for two
/// (see `epiphany_core::generators::valid_score`).
pub fn graph_edit_session(
base: &epiphany_core::Score,
rng: &mut Rng,
) -> (Vec<(StaffInstanceId, VoiceId)>, Vec<OperationEnvelope>) {
let targets: Vec<(StaffInstanceId, VoiceId)> = base
.voices()
.map(|(_, instance, voice)| (instance, voice.id))
.take(2)
.collect();
assert!(
!targets.is_empty(),
"graph_edit_session requires a base score with at least one voice"
);
let n = TWO_STAFF_EVENTS_PER_STAFF;
let mut session = Session::new(2);
for i in 0..n {
for (ti, &(instance, voice)) in targets.iter().enumerate() {
// Each target voice's consecutive half-notes alternate authoring
// replicas; the two voices are edited by opposite replicas at any
// given index. Positions are distinct within a voice (no
// same-position collision → clean inserts), while cross-replica
// causal sampling makes delivery order differ from reduction order.
let r = (i as usize + ti) % 2;
let object = ti as u64 * n + i;
session.author(rng, r, insert_into(instance, voice, object, object, i));
}
}
let total = targets.len() as u64 * n;
// Create a handful of slurs over replica-0 events (the even object indices of
// the first target voice — replica 0 authored and therefore sees them), so the
// Group-2 cross-cutting CRUD below has live structures to delete and modify,
// and the DeleteEvent edits exercise slur re-anchoring on the real graph.
let n_slurs = (n / 4).clamp(1, 6);
for k in 0..n_slurs {
session.author(
rng,
0,
OperationPayload::Primitive(OperationKind::CreateCrossCutting(CreateCrossCuttingOp {
structure: CrossCuttingValue::Slur(valuegen::slur(
SlurId::new(OBJ_REPLICA, k),
obj_event(4 * k),
obj_event(4 * k + 2),
)),
})),
);
}
// Phase-3 tranche: the targeted tx-then-undo flow. One declared transaction
// overwrites LWW keys across the families (spelling, meter, tempo, staff
// layout) and is then undone, so the convergence gates genuinely exercise
// value-restoring undo (a randomly-generated undo almost always hits
// TargetMissing). The overwrite keys (position 400) are disjoint from the
// random-edit keys below, so the undo's verdict — restore vs. superseded —
// stays a pure function of this deterministic seed.
let region = base.canvas.regions[0].id;
let instrument = base.instruments[0].id;
let far = MusicalPosition(RationalTime::from_int(400));
let undo_tx = TransactionId::new(OBJ_REPLICA, 7001);
session.author(
rng,
0,
OperationPayload::Primitive(OperationKind::CreateStaff(CreateStaffOp {
staff: valuegen::staff(StaffId::new(OBJ_REPLICA, 7000), instrument),
})),
);
session.author_tx(
rng,
0,
Some(undo_tx),
OperationPayload::Primitive(OperationKind::DeclareTransaction(TransactionDescriptor {
id: undo_tx,
label: String::from("tx-then-undo flow"),
category: Some(TransactionCategory::Layout),
})),
);
for kind in [
OperationKind::RespellPitch(RespellPitchOp {
pitch: obj_pitch(0),
spelling: valuegen::spelling(6),
}),
OperationKind::SetTimeSignature(SetTimeSignatureOp {
region,
anchor: valuegen::region_start_anchor(region, far.clone()),
time_signature: Some(valuegen::time_signature(
TimeSignatureId::new(OBJ_REPLICA, 7002),
5,
)),
}),
OperationKind::SetTempoSegment(SetTempoSegmentOp {
region: None,
start: valuegen::region_start_anchor(region, far.clone()),
segment: Some(valuegen::tempo_segment(region, far.clone(), 132.0)),
}),
OperationKind::SetStaffLayout(SetStaffLayoutOp {
staff_instance: targets[0].0,
instrument_override: None,
staff_lines_override: None,
visible: false,
}),
] {
session.author_tx(rng, 0, Some(undo_tx), OperationPayload::Primitive(kind));
}
let policy = if rng.boolean() {
UndoPolicy::StrictInverse
} else {
UndoPolicy::BestEffort
};
session.author(
rng,
0,
OperationPayload::UndoTransaction(UndoTransactionPayload {
target: undo_tx,
policy,
}),
);
for _ in 0..80 {
let r = rng.below(2) as usize;
// Mix the original edit kinds with the Group-1/2 (M2) ops — and the
// Phase-3 meter/tempo/layout overwrites — so the real-Score gate
// exercises their *graph* materialization (reduce_onto +
// check_invariants), not just the bookkeeping projection. Each targets a
// live object minted by the phases above (or the base region).
let kind = match rng.below(14) {
0 => OperationKind::DeleteEvent(DeleteEventOp {
event: obj_event(rng.below(total)),
tuplet_compensation: TupletCompensation::NotInTuplet,
}),
1 => OperationKind::RespellPitch(RespellPitchOp {
pitch: obj_pitch(rng.below(total)),
spelling: valuegen::spelling(rng.below(4) as u8 + 1),
}),
2 => {
// Rebuild the event at its *original* placement (so the graph
// applies the modify rather than deferring it as a move).
let idx = rng.below(total);
let (_, voice) = targets[(idx / n) as usize];
OperationKind::ModifyEvent(ModifyEventOp {
event: valuegen::insert_event_value(
obj_event(idx),
voice,
MusicalPosition(
RationalTime::new(
GRAPH_SESSION_OFFSET * EVENTS_PER_BAR as i64 + (idx % n) as i64,
2,
)
.unwrap(),
),
MusicalDuration(RationalTime::new(1, 2).unwrap()),
&[obj_pitch(idx)],
),
})
}
3 => OperationKind::Transpose(TransposeOp {
targets: vec![obj_pitch(rng.below(total))],
chromatic_steps: rng.below(5) as i32 - 2,
}),
// Fresh pitch id (beyond the inserted 0..total range): adds a pitch to
// a note, or turns a rest (left by a last-pitch delete) back into one.
4 => OperationKind::InsertIdentifiedPitch(InsertIdentifiedPitchOp {
event: obj_event(rng.below(total)),
pitch: valuegen::identified_pitch(obj_pitch(total + rng.below(total))),
}),
// Deletes a single-pitch note's only pitch → exercises the note→rest
// degradation path.
5 => OperationKind::DeleteIdentifiedPitch(DeleteIdentifiedPitchOp {
pitch: obj_pitch(rng.below(total)),
}),
6 => OperationKind::ModifyIdentifiedPitch(ModifyIdentifiedPitchOp {
pitch: obj_pitch(rng.below(total)),
value: valuegen::pitch_value_nth(rng.below(7) as u8),
}),
// Group 2 (M2): cross-cutting CRUD over the slurs created above.
7 => OperationKind::DeleteCrossCutting(DeleteCrossCuttingOp {
structure: TypedObjectId::Slur(SlurId::new(OBJ_REPLICA, rng.below(n_slurs))),
}),
8 => {
let k = rng.below(n_slurs);
OperationKind::ModifyCrossCutting(ModifyCrossCuttingOp {
// Re-point the slur's end to another even (replica-0) event.
structure: CrossCuttingValue::Slur(valuegen::slur(
SlurId::new(OBJ_REPLICA, k),
obj_event(4 * k),
obj_event(4 * ((k + 1) % n_slurs)),
)),
})
}
// Phase-3 tranche: meter / tempo / layout overwrites on the base
// region, keyed away from the tx-then-undo flow's key (400). The
// signature value derives from its id so an id re-carry is
// byte-identical (mint idempotence) while distinct ids differ.
9 => {
let signature = rng.below(2);
OperationKind::SetTimeSignature(SetTimeSignatureOp {
region,
anchor: valuegen::region_start_anchor(
region,
MusicalPosition(RationalTime::from_int(rng.below(3) as i32 * 8 + 200)),
),
time_signature: rng.boolean().then(|| {
valuegen::time_signature(
TimeSignatureId::new(OBJ_REPLICA, 7100 + signature),
signature as u16 + 3,
)
}),
})
}
10 => {
let at = MusicalPosition(RationalTime::from_int(rng.below(3) as i32 * 8 + 200));
OperationKind::SetTempoSegment(SetTempoSegmentOp {
region: rng.boolean().then_some(region),
start: valuegen::region_start_anchor(region, at.clone()),
segment: rng.boolean().then(|| {
valuegen::tempo_segment(region, at, 60.0 + rng.below(4) as f64 * 20.0)
}),
})
}
// Repeat authoring over the session's event space: anchors mostly
// resolve live, so mints land in the graph and later DeleteEvents
// exercise the "Repeat structure / Anchor" re-anchoring row under
// the invariant gate.
12 => OperationKind::CreateRepeatStructure(CreateRepeatStructureOp {
repeat: valuegen::repeat_structure(
RepeatStructureId::new(OBJ_REPLICA, rng.below(4)),
obj_event(rng.below(total)),
obj_event(rng.below(total)),
),
}),
13 => OperationKind::DeleteRepeatStructure(DeleteRepeatStructureOp {
repeat: RepeatStructureId::new(OBJ_REPLICA, rng.below(4)),
}),
_ => OperationKind::SetStaffLayout(SetStaffLayoutOp {
staff_instance: targets[rng.below(targets.len() as u64) as usize].0,
instrument_override: None,
staff_lines_override: rng
.boolean()
.then(epiphany_core::StaffLineConfiguration::default),
visible: rng.boolean(),
}),
};
session.author(rng, r, OperationPayload::Primitive(kind));
}
(targets, session.out)
}
/// A `(base, mutated)` pair of operation sets whose operations have **identical
/// identities, stamps, and causal contexts** but differ in payload *content*
/// (one respelling's spelling). Both sets insert a pitch and respell it, so the
/// respelling takes effect; reducing them therefore yields *different* canonical
/// bytes — the exact rebuttal to an id-only "serializer" that would collapse
/// distinct scores. `mutated` is `base.clone()` with only one payload changed,
/// so the identity/ordering metadata is provably preserved.
pub fn content_mutation_pair() -> (Vec<OperationEnvelope>, Vec<OperationEnvelope>) {
let mut rng = Rng::new(0x00C0_1117_E27E_5EED);
let mut session = Session::new(1);
// Insert event 0 carrying pitch 0 (so the pitch is live), then respell it.
session.author(&mut rng, 0, insert_at(0, 0, 0, 0, 0));
session.author(
&mut rng,
0,
OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
pitch: obj_pitch(0),
spelling: valuegen::spelling(0xAA),
})),
);
let base = session.out;
let mut mutated = base.clone();
if let OperationPayload::Primitive(OperationKind::RespellPitch(op)) = &mut mutated[1].payload {
op.spelling = valuegen::spelling(0xBB);
} else {
unreachable!("the second op is the respelling");
}
(base, mutated)
}
/// The canonical [`CausalContext`] bytes for exactly the structurally accepted
/// operation identifiers in `envelopes`.
///
/// Each replica's contiguous prefix beginning at counter zero is represented in
/// the DVV vector; identifiers after a gap are represented as dots. Malformed
/// envelopes are excluded because [`OperationSet`] excludes them from the
/// materialized snapshot as well. The returned bytes are the operation layer's
/// real canonical encoding, not a bundle-local approximation.
pub fn frontier_bytes(envelopes: &[OperationEnvelope]) -> Vec<u8> {
use std::collections::{BTreeMap, BTreeSet};
let mut by_replica: BTreeMap<ReplicaId, BTreeSet<u64>> = BTreeMap::new();
for envelope in envelopes {
if epiphany_ops::well_formed(envelope).is_ok() {
by_replica
.entry(envelope.id.replica)
.or_default()
.insert(envelope.id.counter);
}
}
let mut frontier = CausalContext::new();
for (replica, counters) in by_replica {
let mut next = 0u64;
while counters.contains(&next) {
if next == u64::MAX {
break;
}
next += 1;
}
if next > 0 {
frontier = frontier.with_seen(replica, next - 1);
}
for counter in counters.range(next..) {
frontier = frontier.with_dot(OperationId::new(replica, *counter));
}
}
frontier.to_canonical_bytes()
}
/// A second envelope addressing the same [`OperationId`] as `env` but with
/// different canonical bytes — the input that drives a `Single` slot to
/// `Equivocated`. The stamp's id stays pinned to `env.id`, so it is still
/// well-formed.
pub fn equivocating_twin(env: &OperationEnvelope) -> OperationEnvelope {
let mut twin = env.clone();
twin.payload = OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
pitch: obj_pitch(0),
spelling: valuegen::spelling(0xEE),
}));
if twin.envelope_hash() == env.envelope_hash() {
twin.payload = OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
pitch: obj_pitch(1),
spelling: valuegen::spelling(0x11),
}));
}
twin
}
// ===========================================================================
// Agent D — epiphany-bundle: refs, snapshots, blobs, a rich manifest
// ===========================================================================
/// One of the chunk kinds.
pub fn chunk_kind(rng: &mut Rng) -> ChunkKind {
*rng.choose(&[
ChunkKind::OperationEnvelopeBlock,
ChunkKind::OperationIndex,
ChunkKind::Snapshot,
ChunkKind::Blob,
ChunkKind::ExtensionData,
ChunkKind::TextProjection,
ChunkKind::LayoutCache,
ChunkKind::IntegrityIndex,
ChunkKind::Manifest,
])
}
/// A compression algorithm (every variant). Compression is metadata, not
/// identity, so any value is valid in a [`ChunkRef`].
pub fn compression_algorithm(rng: &mut Rng) -> CompressionAlgorithm {
match rng.below(3) {
0 => CompressionAlgorithm::None,
1 => CompressionAlgorithm::Zstd {
level: rng.range(1, 22) as u8,
},
_ => CompressionAlgorithm::Reserved(rng.range(2, 255) as u8),
}
}
/// A content-addressed chunk reference of the given kind, derived from a random
/// payload so its id and hash are internally consistent (content identity is the
/// uncompressed payload, independent of the `compression` metadata).
pub fn chunk_ref(rng: &mut Rng, kind: ChunkKind) -> ChunkRef {
let payload = rng.byte_vec(1, 64);
let schema = SchemaVersion::V0;
ChunkRef {
id: chunk_id(kind, schema, &payload),
kind,
schema_version: schema,
offset: rng.range(0, 1 << 32),
compressed_length: payload.len() as u64,
uncompressed_length: payload.len() as u64,
compression: compression_algorithm(rng),
hash: content_hash_for(kind, schema, &payload),
}
}
/// A committed superblock with varied fields. Committed so it parses as
/// [`epiphany_bundle::SlotParse::Valid`] for ordinary selection.
pub fn superblock(rng: &mut Rng) -> Superblock {
Superblock {
generation: rng.range(0, 1_000),
manifest_offset: rng.range(0, 1 << 32),
manifest_length: rng.range(1, 1 << 20),
manifest_hash: content_hash(rng),
manifest_schema_version: SchemaVersion::V0,
reduction_algorithm_version: ReductionAlgorithmVersion(rng.range(0, 8) as u32),
profile_id: profile_id(rng),
commit_state: CommitState::Committed,
commit_timestamp: BundleWallClockTime(rng.range(0, 1 << 40) as i64),
}
}
/// A snapshot reference (a canonical base or acceleration snapshot) under a
/// varied profile. The `hash` is the snapshot root chunk's hash (the field's
/// documented meaning: "hash of the snapshot's root chunk, for fast verification").
pub fn snapshot_ref(rng: &mut Rng) -> SnapshotRef {
let profile = profile_id(rng);
snapshot_ref_with_profile(rng, profile)
}
/// A snapshot reference whose `profile_id` is the caller's choice (so a manifest
/// can keep its snapshots' profiles among its declared profiles).
pub fn snapshot_ref_with_profile(rng: &mut Rng, profile: ProfileId) -> SnapshotRef {
let root = chunk_ref(rng, ChunkKind::Snapshot);
let hash = root.hash;
SnapshotRef {
snapshot_id: SnapshotId(rng.array16()),
covers_causal_frontier: FrontierBytes::from_bytes(rng.byte_vec(0, 24)),
reduction_algorithm_version: ReductionAlgorithmVersion(rng.range(0, 8) as u32),
profile_id: profile,
root,
hash,
}
}
/// A profile identifier (every variant).
pub fn profile_id(rng: &mut Rng) -> ProfileId {
match rng.below(4) {
0 => ProfileId::Full,
1 => ProfileId::ReadOnly,
2 => ProfileId::Lite,
_ => ProfileId::Custom(ProfileRegistryId(rng.array16())),
}
}
/// A profile identifier that is never [`ProfileId::Full`] — for a *second*
/// declaration alongside a Full one, so the two ids do not collide.
pub fn non_full_profile_id(rng: &mut Rng) -> ProfileId {
match rng.below(3) {
0 => ProfileId::ReadOnly,
1 => ProfileId::Lite,
_ => ProfileId::Custom(ProfileRegistryId(rng.array16())),
}
}
/// A blob reference with a valid RFC 6838 media type. The `hash` is the payload's
/// content hash, consistent with the content-derived `blob_id`.
pub fn blob_ref(rng: &mut Rng) -> BlobRef {
let payload = rng.byte_vec(1, 64);
let len = payload.len() as u64;
let blob_id = BlobId::of_payload(&payload);
BlobRef {
// The verification hash is the blob's content hash, consistent with its
// content-derived id.
hash: blob_id.0,
blob_id,
media_type: String::from("application/octet-stream"),
offset: rng.range(0, 1 << 32),
compressed_length: len,
uncompressed_length: len,
compression: CompressionAlgorithm::None,
declared_max_uncompressed_length: if rng.boolean() {
Some(len.max(1))
} else {
None
},
}
}
/// An extension declaration. `preserved_chunk_roots` is kept to at most one
/// entry so the canonical sort/dedup `encode` applies is a no-op and the decoded
/// value compares equal to the original.
pub fn extension_declaration(rng: &mut Rng) -> ExtensionDeclaration {
ExtensionDeclaration {
extension_id: ExtensionId(rng.array16()),
version: SemVer::new(
rng.range(0, 3) as u32,
rng.range(0, 9) as u32,
rng.range(0, 9) as u32,
),
required: rng.boolean(),
preserved_chunk_roots: if rng.boolean() {
vec![chunk_ref(rng, ChunkKind::ExtensionData)]
} else {
vec![]
},
affected_object_kinds: rng.byte_vec(0, 8),
edit_barriers: rng.byte_vec(0, 8),
}
}
/// A retention policy with varied (non-default) fields.
pub fn retention_policy(rng: &mut Rng) -> RetentionPolicy {
RetentionPolicy {
retain_previous_manifests: rng.range(0, 8) as u32,
retain_duration: if rng.boolean() {
Some(BundleWallClockDuration(rng.range(0, 1 << 40) as i64))
} else {
None
},
retain_named_checkpoints: rng.boolean(),
}
}
/// A profile declaration with a varied profile id and constraints (a varied
/// [`retention_policy`] and block-size limit).
pub fn profile_declaration(rng: &mut Rng) -> ProfileDeclaration {
let profile = profile_id(rng);
profile_declaration_with(rng, profile)
}
/// A profile declaration with the caller's `profile_id` and varied constraints.
pub fn profile_declaration_with(rng: &mut Rng, profile: ProfileId) -> ProfileDeclaration {
ProfileDeclaration {
profile_id: profile,
version: SemVer::new(rng.range(0, 2) as u32, rng.range(0, 9) as u32, 0),
constraints: ProfileConstraints {
max_uncompressed_block_size: rng.range(1 << 16, 64 << 20),
retention_policy: retention_policy(rng),
},
}
}
/// A **rich** manifest exercising every optional field and reference vector:
/// lineage, multiple operation roots, an operation index, a canonical base,
/// acceleration snapshots, blobs, extension and (two, distinct-id) profile
/// declarations, and the text-projection / integrity optional roots. Snapshots
/// reference only declared profiles, and the two profile declarations have
/// distinct ids (no `Full`/`Full` collision).
///
/// **Scope.** This is a *codec / structural* manifest generator — it exercises
/// `Manifest::encode`/`decode` byte-stability and the decoder's validation. It is
/// not a fully bundle-valid manifest: its `ChunkRef`/`BlobRef` references are not
/// backed by chunks in any store (so `Bundle::open` + `verify_canonical_chunks`
/// would not accept it). Bundle *lifecycle* validity is covered by
/// [`crate::roundtrip::committed_manifest`] (a manifest produced by real commits)
/// and the bundle's own crash/manifest-selection harnesses.
pub fn rich_manifest(rng: &mut Rng) -> Manifest {
let mut m = Manifest::empty(DocumentId(rng.array16()));
m.generation = rng.range(0, 1_000);
m.lineage_id = if rng.boolean() {
Some(LineageId(rng.array16()))
} else {
None
};
for _ in 0..rng.range_usize(0, 3) {
m.operation_roots
.push(chunk_ref(rng, ChunkKind::OperationEnvelopeBlock));
}
m.operation_index_root = if rng.boolean() {
Some(chunk_ref(rng, ChunkKind::OperationIndex))
} else {
None
};
// Two profile declarations with distinct ids (Full + a non-Full one).
let extra_id = non_full_profile_id(rng);
let extra = profile_declaration_with(rng, extra_id);
m.profile_declarations = vec![ProfileDeclaration::full(), extra];
// Snapshots reference only declared profiles (Full or the extra).
let declared = [ProfileId::Full, extra_id];
m.canonical_base = if rng.boolean() {
let p = *rng.choose(&declared);
Some(snapshot_ref_with_profile(rng, p))
} else {
None
};
for _ in 0..rng.range_usize(0, 2) {
let p = *rng.choose(&declared);
m.acceleration_snapshots
.push(snapshot_ref_with_profile(rng, p));
}
for _ in 0..rng.range_usize(0, 2) {
m.blob_roots.push(blob_ref(rng));
}
for _ in 0..rng.range_usize(0, 2) {
m.extension_declarations.push(extension_declaration(rng));
}
m.text_projection_root = if rng.boolean() {
Some(chunk_ref(rng, ChunkKind::TextProjection))
} else {
None
};
m.integrity_root = if rng.boolean() {
Some(chunk_ref(rng, ChunkKind::IntegrityIndex))
} else {
None
};
m
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envelope_set_is_well_formed_and_deterministic() {
let mut a = Rng::new(42);
let mut b = Rng::new(42);
let sa = operation_envelopes(&mut a, 64, 3, 6, 6);
let sb = operation_envelopes(&mut b, 64, 3, 6, 6);
assert_eq!(sa.len(), 64);
assert_eq!(sa, sb, "generation must be reproducible from the seed");
assert!(sa.iter().all(|e| epiphany_ops::well_formed(e).is_ok()));
}
#[test]
fn equivocating_twin_differs_in_bytes_but_not_id() {
let mut rng = Rng::new(7);
let set = operation_envelopes(&mut rng, 4, 2, 6, 6);
let twin = equivocating_twin(&set[0]);
assert_eq!(twin.id, set[0].id);
assert_eq!(twin.stamp.id, set[0].id);
assert_ne!(twin.envelope_hash(), set[0].envelope_hash());
}
#[test]
fn rational_large_is_actually_promoted() {
let mut rng = Rng::new(3);
for _ in 0..100 {
let r = rational_time_large(&mut rng);
assert!(
matches!(r, RationalTime::Large(_)),
"rational_time_large must produce the promoted arm, got {r:?}"
);
}
}
#[test]
fn rich_manifest_is_deterministic() {
let mut a = Rng::new(99);
let mut b = Rng::new(99);
assert_eq!(rich_manifest(&mut a), rich_manifest(&mut b));
}
#[test]
fn zero_sized_operation_spaces_are_supported() {
let mut rng = Rng::new(5);
for _ in 0..128 {
let _ = operation_payload(&mut rng, 0, 0);
}
}
/// (s6, half) Genesis tranche G2a's boundary against G2b: both new
/// setters are schema major 0 unconditionally, so a block carrying either
/// stamps well within the `OperationEnvelopeBlock` accept-set no matter
/// where that set's ceiling sits.
///
/// The accept-set ceiling itself (`max_supported_major(
/// OperationEnvelopeBlock)`, raised from 2 to **3** by genesis tranche
/// G2b, which introduced the first operation payload born at major 3)
/// is **already** asserted in a compiled
/// test inside `epiphany-bundle` (`bundle.rs:1322`), which predates this
/// packet and which this packet leaves untouched. An earlier draft of
/// this comment claimed that half of s6 was unassertable because
/// `max_supported_major` is not re-exported — the privacy observation is
/// true but the conclusion was wrong, since the assertion does not need
/// to live *outside* the crate. **No re-export is needed, here or by the
/// G-minor rung.**
///
/// What this test adds is the half the in-crate assertion cannot cover:
/// that a *staged block* carrying either new kind stamps major 0, which
/// is the contract's actual claim and which exercises the writer-side
/// derivation (`stage_operation_block`) rather than the bare kind.
///
/// **Mutation:** stamp a payload carrying either new kind at a non-zero
/// major (as if it had been wrongly placed in the `=> 2` arm alongside
/// `SetMetadata`, exactly the bug pin 3 and test s5 both name) — both
/// assertions fire.
#[test]
fn the_two_new_settings_kinds_stamp_within_the_accept_set() {
let kinds = [
OperationKind::SetCanvasLayoutDefaults(SetCanvasLayoutDefaultsOp {
layout_defaults: valuegen::canvas_layout_defaults(1),
}),
OperationKind::SetSpellingPrecedence(SetSpellingPrecedenceOp {
precedence: valuegen::spelling_precedence(1),
}),
];
for kind in &kinds {
assert_eq!(
kind.schema_major(),
0,
"both new kinds must stamp at major 0"
);
}
// The contract's claim is about the BLOCK, not the bare kind: a block
// carrying either kind must stamp major 0 through the writer-side
// derivation every real staging path uses.
for (n, kind) in kinds.into_iter().enumerate() {
let id = OperationId::new(ReplicaId(1), n as u64);
let env = OperationEnvelope {
id,
author: AuthorId(1),
stamp: OperationStamp::new(
HybridLogicalClock::new(WallClockTime(10 + n as i64), 0),
id,
),
causal_context: CausalContext::new(),
transaction: None,
payload: OperationPayload::Primitive(kind),
};
assert!(epiphany_ops::well_formed(&env).is_ok());
let staged = crate::bundle_harness::stage_operation_block(&[env]);
assert_eq!(
staged.schema_version.major, 0,
"a staged block carrying a G2a setter stamps major 0"
);
}
}
/// (s10, row 28) `operation_payload`'s bounded draw is a hand-maintained
/// arm list over `rng.below(N)`, which nothing forces to move when the
/// operation vocabulary grows — exactly how `TransposeInterval` (kind 30,
/// Push-4a debt) and `CreateInstrument` (kind 31, G1 debt) went missing
/// from every corpus this generator feeds despite every downstream suite
/// staying green. Assert a bounded draw actually reaches every kind
/// appended past the historically-tested range (discriminants 30..=39),
/// not just that the function does not panic.
#[test]
fn operation_payload_emits_every_appended_kind() {
let mut rng = Rng::new(17);
let (mut saw_transpose_interval, mut saw_create_instrument) = (false, false);
let (mut saw_canvas_layout_defaults, mut saw_spelling_precedence) = (false, false);
let mut saw_tuning_context = false;
let (mut saw_create_staff_group, mut saw_create_part_definition) = (false, false);
let (mut saw_create_analysis_layer, mut saw_create_view) = (false, false);
let mut saw_create_measure = false;
for _ in 0..2000 {
let OperationPayload::Primitive(kind) = operation_payload(&mut rng, 8, 8) else {
continue;
};
match kind {
OperationKind::TransposeInterval(_) => saw_transpose_interval = true,
OperationKind::CreateInstrument(_) => saw_create_instrument = true,
OperationKind::SetCanvasLayoutDefaults(_) => saw_canvas_layout_defaults = true,
OperationKind::SetSpellingPrecedence(_) => saw_spelling_precedence = true,
OperationKind::SetTuningContext(_) => saw_tuning_context = true,
OperationKind::CreateStaffGroup(_) => saw_create_staff_group = true,
OperationKind::CreatePartDefinition(_) => saw_create_part_definition = true,
OperationKind::CreateAnalysisLayer(_) => saw_create_analysis_layer = true,
OperationKind::CreateView(_) => saw_create_view = true,
OperationKind::CreateMeasure(_) => saw_create_measure = true,
_ => {}
}
}
assert!(
saw_transpose_interval,
"TransposeInterval (kind 30) never drawn in 2000 samples"
);
assert!(
saw_create_instrument,
"CreateInstrument (kind 31) never drawn in 2000 samples"
);
assert!(
saw_canvas_layout_defaults,
"SetCanvasLayoutDefaults (kind 32) never drawn in 2000 samples"
);
assert!(
saw_spelling_precedence,
"SetSpellingPrecedence (kind 33) never drawn in 2000 samples"
);
assert!(
saw_tuning_context,
"SetTuningContext (kind 34, G2b debt) never drawn in 2000 samples"
);
assert!(
saw_create_staff_group,
"CreateStaffGroup (kind 35, G3a debt) never drawn in 2000 samples"
);
assert!(
saw_create_part_definition,
"CreatePartDefinition (kind 36, G3a debt) never drawn in 2000 samples"
);
assert!(
saw_create_analysis_layer,
"CreateAnalysisLayer (kind 37, G3a debt) never drawn in 2000 samples"
);
assert!(
saw_create_view,
"CreateView (kind 38, G3a debt) never drawn in 2000 samples"
);
assert!(
saw_create_measure,
"CreateMeasure (kind 39, G3b debt) never drawn in 2000 samples"
);
}
/// (M66, row 23) `precondition_failure_reason`'s doc comment claims
/// "every core and registered variant" — assert the bounded draw
/// actually reaches every one, including the genesis tranche G3b
/// additions (1618) and the previously-untested Push 4a pair (1415,
/// P13-S20).
///
/// **Mutation:** revert `rng.below(19)` to `rng.below(14)`; must fail —
/// the generator would then never reach discriminants 14 through 18.
#[test]
fn precondition_failure_reason_reaches_every_variant() {
let mut rng = Rng::new(23);
let mut seen: std::collections::BTreeSet<u8> = std::collections::BTreeSet::new();
for _ in 0..2000 {
let reason = precondition_failure_reason(&mut rng);
let discriminant = match reason {
PreconditionFailureReason::TargetMissing => 0,
PreconditionFailureReason::TargetTombstoned => 1,
PreconditionFailureReason::WrongRegionTimeModel => 2,
PreconditionFailureReason::TupletCompensationInvalid => 3,
PreconditionFailureReason::EventDurationInvalid => 4,
PreconditionFailureReason::PositionOutsideRegion => 5,
PreconditionFailureReason::PitchSpaceMismatch => 6,
PreconditionFailureReason::VoiceMissing => 7,
PreconditionFailureReason::ContainerNotEmpty => 8,
PreconditionFailureReason::TempoMapMalformed => 9,
PreconditionFailureReason::SystemDerivedContentImmutable => 10,
PreconditionFailureReason::RecreateContentMismatch => 11,
PreconditionFailureReason::AcousticRealizationPinned => 12,
PreconditionFailureReason::TranspositionOutOfRange => 13,
PreconditionFailureReason::MeasureMeterMismatch => 14,
PreconditionFailureReason::MeasureOutOfOrder => 15,
PreconditionFailureReason::MeasureOrderUnverifiable => 16,
PreconditionFailureReason::ExtensionPrecondition(_) => 17,
PreconditionFailureReason::Registered(_) => 18,
};
seen.insert(discriminant);
}
let expected: std::collections::BTreeSet<u8> = (0..=18).collect();
assert_eq!(
seen, expected,
"the bounded draw must reach every core and registered variant"
);
}
#[test]
fn frontier_uses_the_real_dvv_encoding_and_preserves_gaps() {
let mut rng = Rng::new(11);
let mut envelope = operation_envelopes(&mut rng, 1, 1, 1, 1).remove(0);
envelope.id = OperationId::new(ReplicaId(1), 5);
envelope.stamp.id = envelope.id;
let expected = CausalContext::new()
.with_dot(envelope.id)
.to_canonical_bytes();
assert_eq!(frontier_bytes(std::slice::from_ref(&envelope)), expected);
envelope.stamp.hlc.physical_time = WallClockTime(-1);
assert_eq!(
frontier_bytes(&[envelope]),
CausalContext::new().to_canonical_bytes(),
"rejected envelopes must not enter the snapshot frontier"
);
}
#[test]
fn generated_reduction_outputs_decode_canonically() {
for seed in 0..256 {
let mut rng = Rng::new(seed);
let mut state = MaterializedState::default();
state
.effects
.push((operation_id(&mut rng), operation_effect(&mut rng)));
state.conflicts = conflict_registry(&mut rng);
state.anomalies.push(integrity_anomaly(&mut rng));
state
.objects
.insert(typed_object_id(&mut rng), object_state(&mut rng));
state
.spellings
.insert(pitch_id(&mut rng), valuegen::spelling(rng.below(7) as u8));
state.breaks.insert(
(region_id(&mut rng), musical_position(&mut rng)),
rng.boolean(),
);
state.page_breaks.insert(
(region_id(&mut rng), musical_position(&mut rng)),
rng.boolean(),
);
state
.pending
.push((operation_id(&mut rng), pending_reason(&mut rng)));
let bytes = state.canonical_bytes();
let decoded = MaterializedState::decode_canonical(&bytes).unwrap();
assert_eq!(decoded.canonical_bytes(), bytes);
let _ = reanchor_result(&mut rng);
let _ = anomalous_replica_segment(&mut rng);
}
}
}