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

1517 lines
60 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, 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, CreateCrossCuttingOp, CrossCuttingValue, DeleteEventOp,
DeleteIdentifiedPitchOp, ExtensionPreconditionId, FieldPath, HybridLogicalClock, InsertEventOp,
InsertIdentifiedPitchOp, IntegrityAnomaly, IntegrityAnomalyKind, IntegrityAnomalyRegistryId,
MaterializedState, 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, SetUserSystemBreakOp, TransactionCategory, TransactionDescriptor,
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(10) {
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::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(6) {
0 => ReanchorReason::SameVoiceNearer,
1 => ReanchorReason::SameStaffInstanceNearer,
2 => ReanchorReason::SameStaffNearer,
3 => ReanchorReason::SameRegionNearer,
4 => ReanchorReason::ExplicitFallback,
_ => 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(13) {
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),
}),
_ => 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) {
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: None,
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;
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);
}
(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);
}
}
#[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
.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);
}
}
}