348 lines
13 KiB
Rust
348 lines
13 KiB
Rust
//! Genesis tranche G3b packet 2 (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`,
|
|
//! architecture note): invariant 20's pin 6/6b comparable relation and
|
|
//! musical delta are implemented TWICE — once in `epiphany-core`'s
|
|
//! `invariants.rs` (over a materialized `Score`, no operational chains to
|
|
//! reconstruct) and once in `epiphany-ops`'s `Reducer` (over operational
|
|
//! write chains, both graph-aware and base-free). `epiphany-ops` depends on
|
|
//! `epiphany-core`, never the reverse, so invariant 20 cannot call the
|
|
//! reducer's private methods, and there is no third crate either could
|
|
//! delegate to instead. Two independent implementations of one normative
|
|
//! relation is a divergence hazard, and this is the guard against it: drive
|
|
//! the SAME anchor pairs through both (`epiphany_core::invariants::
|
|
//! measure_anchor_relation` and `epiphany_ops::
|
|
//! measure_anchor_relation_for_agreement_test`) and assert they agree, on
|
|
//! BOTH the comparable-or-not verdict and (when comparable) the ordering —
|
|
//! plus the musical delta, pin 6b's companion relation.
|
|
//!
|
|
//! **Mutation:** perturb ONE implementation only (e.g. let core's relation
|
|
//! order across differing `pos`/`edge` selectors, exactly the unsoundness
|
|
//! contract pin 6 rules out) and observe this test go red. Counted as an
|
|
//! extra mutation beyond M34-M47, reported separately.
|
|
|
|
use std::cmp::Ordering;
|
|
|
|
use epiphany_core::{
|
|
AnchorOffset, EventId, IdentityContext, Measure, MeasureId, MeasureNumberVisibility,
|
|
MeasurePosition, MetricTimeModel, MusicalDuration, RationalTime, Region, RegionContent,
|
|
RegionEdge, RegionId, RegionTimeModel, ReplicaId, Score, StaffBasedContent, StaffExtent,
|
|
StaffId, StaffInstance, StaffInstanceId, TimeAnchor, TimeExtent, WallClockDuration,
|
|
WallClockTime,
|
|
};
|
|
|
|
/// Builds a fixture `Score` with TWO staff instances (in the SAME region,
|
|
/// to keep it minimal): `inst_a` carries measures `[m1, m2, m3]` in that
|
|
/// vector order (positions 0, 1, 2 — c3's "vector index"); `inst_b` carries
|
|
/// a single measure `m4`, so a `Measure` pair spanning `inst_a`/`inst_b` is
|
|
/// the cross-instance case c3 must refuse.
|
|
fn fixture() -> (
|
|
Score,
|
|
RegionId,
|
|
StaffInstanceId,
|
|
StaffInstanceId,
|
|
MeasureId,
|
|
MeasureId,
|
|
MeasureId,
|
|
MeasureId,
|
|
) {
|
|
let replica = ReplicaId(9);
|
|
let mut idc = IdentityContext::new(replica);
|
|
let region_id: RegionId = idc.mint();
|
|
let staff_a: StaffId = idc.mint();
|
|
let staff_b: StaffId = idc.mint();
|
|
let inst_a: StaffInstanceId = idc.mint();
|
|
let inst_b: StaffInstanceId = idc.mint();
|
|
|
|
let m1 = MeasureId::new(replica, 101);
|
|
let m2 = MeasureId::new(replica, 102);
|
|
let m3 = MeasureId::new(replica, 103);
|
|
let m4 = MeasureId::new(replica, 104);
|
|
|
|
let bare = |id: MeasureId, offset_wholes: i32| Measure {
|
|
id,
|
|
start: TimeAnchor::Region {
|
|
id: region_id,
|
|
edge: RegionEdge::Start,
|
|
offset: if offset_wholes == 0 {
|
|
AnchorOffset::Zero
|
|
} else {
|
|
AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(offset_wholes)))
|
|
},
|
|
},
|
|
time_signature: None,
|
|
explicit_number: None,
|
|
number_visibility: MeasureNumberVisibility::Auto,
|
|
};
|
|
|
|
let mut instance_a = StaffInstance::new(inst_a, staff_a);
|
|
instance_a.measures = vec![bare(m1, 0), bare(m2, 1), bare(m3, 2)];
|
|
let mut instance_b = StaffInstance::new(inst_b, staff_b);
|
|
instance_b.measures = vec![bare(m4, 0)];
|
|
|
|
let region = Region {
|
|
id: region_id,
|
|
time_model: RegionTimeModel::Metric(MetricTimeModel::default()),
|
|
content: RegionContent::StaffBased(StaffBasedContent {
|
|
staff_instances: vec![instance_a, instance_b],
|
|
..Default::default()
|
|
}),
|
|
time_extent: TimeExtent {
|
|
start: TimeAnchor::WallClock {
|
|
time: WallClockTime(0),
|
|
},
|
|
end: TimeAnchor::WallClock {
|
|
time: WallClockTime(1_000_000),
|
|
},
|
|
},
|
|
staff_extent: StaffExtent {
|
|
staves: vec![staff_a, staff_b],
|
|
},
|
|
local_tempo_map: None,
|
|
permits_spanning_slurs: false,
|
|
};
|
|
|
|
let mut score = Score::empty(idc.clone());
|
|
score.identity = idc;
|
|
score.canvas.regions = vec![region];
|
|
(score, region_id, inst_a, inst_b, m1, m2, m3, m4)
|
|
}
|
|
|
|
fn measure_anchor(id: MeasureId, pos: MeasurePosition, offset: AnchorOffset) -> TimeAnchor {
|
|
TimeAnchor::Measure {
|
|
id,
|
|
position: pos,
|
|
offset,
|
|
}
|
|
}
|
|
|
|
fn region_anchor(region: RegionId, edge: RegionEdge, offset: AnchorOffset) -> TimeAnchor {
|
|
TimeAnchor::Region {
|
|
id: region,
|
|
edge,
|
|
offset,
|
|
}
|
|
}
|
|
|
|
fn event_anchor(id: EventId, offset: AnchorOffset) -> TimeAnchor {
|
|
TimeAnchor::Event { id, offset }
|
|
}
|
|
|
|
fn musical(n: i32) -> AnchorOffset {
|
|
AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(n)))
|
|
}
|
|
|
|
fn wallclock(n: i64) -> AnchorOffset {
|
|
AnchorOffset::WallClock(WallClockDuration(n))
|
|
}
|
|
|
|
/// Asserts both implementations agree on the comparable-or-not verdict, the
|
|
/// ordering when comparable, and the musical delta — for one anchor pair,
|
|
/// in BOTH orientations (a,b) and (b,a), since the relation and delta are
|
|
/// meant to be antisymmetric.
|
|
fn assert_agrees(score: &Score, label: &str, a: &TimeAnchor, b: &TimeAnchor) {
|
|
let (core_order, core_delta) = epiphany_core::measure_anchor_relation(score, a, b);
|
|
let (ops_order, ops_delta) =
|
|
epiphany_ops::measure_anchor_relation_for_agreement_test(score, a, b);
|
|
assert_eq!(
|
|
core_order, ops_order,
|
|
"{label}: comparable-order verdict disagrees (core: {core_order:?}, ops: {ops_order:?})"
|
|
);
|
|
assert_eq!(
|
|
core_delta, ops_delta,
|
|
"{label}: musical-delta verdict disagrees (core: {core_delta:?}, ops: {ops_delta:?})"
|
|
);
|
|
|
|
// Antisymmetric check, the opposite direction.
|
|
let (core_order_rev, core_delta_rev) = epiphany_core::measure_anchor_relation(score, b, a);
|
|
let (ops_order_rev, ops_delta_rev) =
|
|
epiphany_ops::measure_anchor_relation_for_agreement_test(score, b, a);
|
|
assert_eq!(
|
|
core_order_rev, ops_order_rev,
|
|
"{label} (reversed): comparable-order verdict disagrees"
|
|
);
|
|
assert_eq!(
|
|
core_delta_rev, ops_delta_rev,
|
|
"{label} (reversed): musical-delta verdict disagrees"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_crate_anchor_relation_agrees_on_every_table_row() {
|
|
let (score, region, _inst_a, _inst_b, m1, m2, m3, m4) = fixture();
|
|
let e1 = EventId::new(ReplicaId(9), 1);
|
|
let e2 = EventId::new(ReplicaId(9), 2);
|
|
|
|
// c1: Event, same id.
|
|
assert_agrees(
|
|
&score,
|
|
"c1 same event id, Zero vs Musical(1)",
|
|
&event_anchor(e1, AnchorOffset::Zero),
|
|
&event_anchor(e1, musical(1)),
|
|
);
|
|
// c1: Event, different id -- not comparable.
|
|
assert_agrees(
|
|
&score,
|
|
"c1 different event ids",
|
|
&event_anchor(e1, AnchorOffset::Zero),
|
|
&event_anchor(e2, AnchorOffset::Zero),
|
|
);
|
|
// Cross-clock, same event id -- not comparable.
|
|
assert_agrees(
|
|
&score,
|
|
"c1 same event id, Musical vs WallClock",
|
|
&event_anchor(e1, musical(1)),
|
|
&event_anchor(e1, wallclock(1)),
|
|
);
|
|
|
|
// c2: Measure, same id, same pos (Start), differing Musical offsets.
|
|
assert_agrees(
|
|
&score,
|
|
"c2 same measure id, Start, Musical(0) vs Musical(2)",
|
|
&measure_anchor(m1, MeasurePosition::Start, musical(0)),
|
|
&measure_anchor(m1, MeasurePosition::Start, musical(2)),
|
|
);
|
|
// c2: Measure, same id, but DIFFERENT pos -- not comparable.
|
|
assert_agrees(
|
|
&score,
|
|
"c2 same measure id, Start vs End",
|
|
&measure_anchor(m1, MeasurePosition::Start, AnchorOffset::Zero),
|
|
&measure_anchor(m1, MeasurePosition::End, AnchorOffset::Zero),
|
|
);
|
|
|
|
// c3: distinct measure ids, Start/Zero, SAME instance's vector,
|
|
// adjacent and non-adjacent pairs.
|
|
assert_agrees(
|
|
&score,
|
|
"c3 same-vector adjacent (m1, m2)",
|
|
&measure_anchor(m1, MeasurePosition::Start, AnchorOffset::Zero),
|
|
&measure_anchor(m2, MeasurePosition::Start, AnchorOffset::Zero),
|
|
);
|
|
assert_agrees(
|
|
&score,
|
|
"c3 same-vector non-adjacent (m1, m3)",
|
|
&measure_anchor(m1, MeasurePosition::Start, AnchorOffset::Zero),
|
|
&measure_anchor(m3, MeasurePosition::Start, AnchorOffset::Zero),
|
|
);
|
|
// c3: distinct measure ids, Start/Zero, CROSS-INSTANCE -- not
|
|
// comparable (m2 in inst_a's vector, m4 in inst_b's).
|
|
assert_agrees(
|
|
&score,
|
|
"c3 cross-instance (m2, m4)",
|
|
&measure_anchor(m2, MeasurePosition::Start, AnchorOffset::Zero),
|
|
&measure_anchor(m4, MeasurePosition::Start, AnchorOffset::Zero),
|
|
);
|
|
// c3's restriction: nonzero offset -- not comparable even though both
|
|
// are Start and in the same vector.
|
|
assert_agrees(
|
|
&score,
|
|
"c3 restriction: nonzero offset",
|
|
&measure_anchor(m1, MeasurePosition::Start, musical(1)),
|
|
&measure_anchor(m2, MeasurePosition::Start, AnchorOffset::Zero),
|
|
);
|
|
// c3's restriction: End position -- not comparable.
|
|
assert_agrees(
|
|
&score,
|
|
"c3 restriction: End position",
|
|
&measure_anchor(m1, MeasurePosition::End, AnchorOffset::Zero),
|
|
&measure_anchor(m2, MeasurePosition::End, AnchorOffset::Zero),
|
|
);
|
|
|
|
// c4: Region, same id, same edge, differing offsets (Musical and
|
|
// WallClock clocks separately, plus Zero-normalization).
|
|
assert_agrees(
|
|
&score,
|
|
"c4 same region/edge, Musical(0) vs Musical(3)",
|
|
®ion_anchor(region, RegionEdge::Start, musical(0)),
|
|
®ion_anchor(region, RegionEdge::Start, musical(3)),
|
|
);
|
|
assert_agrees(
|
|
&score,
|
|
"c4 same region/edge, WallClock(10) vs WallClock(20)",
|
|
®ion_anchor(region, RegionEdge::Start, wallclock(10)),
|
|
®ion_anchor(region, RegionEdge::Start, wallclock(20)),
|
|
);
|
|
assert_agrees(
|
|
&score,
|
|
"c4 same region/edge, Zero vs Musical(5)",
|
|
®ion_anchor(region, RegionEdge::Start, AnchorOffset::Zero),
|
|
®ion_anchor(region, RegionEdge::Start, musical(5)),
|
|
);
|
|
// c4: same id, DIFFERENT edge -- not comparable, even at Zero/Zero.
|
|
assert_agrees(
|
|
&score,
|
|
"c4 same region id, Start vs End",
|
|
®ion_anchor(region, RegionEdge::Start, AnchorOffset::Zero),
|
|
®ion_anchor(region, RegionEdge::End, AnchorOffset::Zero),
|
|
);
|
|
// c4: differing region id -- not comparable.
|
|
assert_agrees(
|
|
&score,
|
|
"c4 different region ids",
|
|
®ion_anchor(region, RegionEdge::Start, AnchorOffset::Zero),
|
|
®ion_anchor(
|
|
RegionId::new(ReplicaId(9), 999),
|
|
RegionEdge::Start,
|
|
AnchorOffset::Zero,
|
|
),
|
|
);
|
|
// Draft 3's unsoundness, explicitly regression-locked here too: Start
|
|
// vs End with a nonzero offset must NOT be ordered by selector.
|
|
assert_agrees(
|
|
&score,
|
|
"c4 Start/Musical(100) vs End/Zero -- must be unverifiable in both",
|
|
®ion_anchor(region, RegionEdge::Start, musical(100)),
|
|
®ion_anchor(region, RegionEdge::End, AnchorOffset::Zero),
|
|
);
|
|
|
|
// c5: WallClock, no referent id.
|
|
assert_agrees(
|
|
&score,
|
|
"c5 WallClock(0) vs WallClock(500)",
|
|
&TimeAnchor::WallClock {
|
|
time: WallClockTime(0),
|
|
},
|
|
&TimeAnchor::WallClock {
|
|
time: WallClockTime(500),
|
|
},
|
|
);
|
|
|
|
// Cross-variant pairs -- never comparable.
|
|
assert_agrees(
|
|
&score,
|
|
"Event vs Measure",
|
|
&event_anchor(e1, AnchorOffset::Zero),
|
|
&measure_anchor(m1, MeasurePosition::Start, AnchorOffset::Zero),
|
|
);
|
|
assert_agrees(
|
|
&score,
|
|
"Measure vs Region",
|
|
&measure_anchor(m1, MeasurePosition::Start, AnchorOffset::Zero),
|
|
®ion_anchor(region, RegionEdge::Start, AnchorOffset::Zero),
|
|
);
|
|
assert_agrees(
|
|
&score,
|
|
"Region vs WallClock",
|
|
®ion_anchor(region, RegionEdge::Start, AnchorOffset::Zero),
|
|
&TimeAnchor::WallClock {
|
|
time: WallClockTime(0),
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Direct, minimal sanity check that the two functions actually AGREE on a
|
|
/// concrete verdict (not merely on each other's None/None) -- guards
|
|
/// against a degenerate agreement where both sides trivially return `None`
|
|
/// for everything.
|
|
#[test]
|
|
fn cross_crate_anchor_relation_actually_produces_non_trivial_verdicts() {
|
|
let (score, region, ..) = fixture();
|
|
let a = region_anchor(region, RegionEdge::Start, musical(0));
|
|
let b = region_anchor(region, RegionEdge::Start, musical(3));
|
|
let (core_order, core_delta) = epiphany_core::measure_anchor_relation(&score, &a, &b);
|
|
let (ops_order, ops_delta) =
|
|
epiphany_ops::measure_anchor_relation_for_agreement_test(&score, &a, &b);
|
|
assert_eq!(core_order, Some(Ordering::Less));
|
|
assert_eq!(ops_order, Some(Ordering::Less));
|
|
assert_eq!(core_delta, Some(MusicalDuration(RationalTime::from_int(3))));
|
|
assert_eq!(ops_delta, Some(MusicalDuration(RationalTime::from_int(3))));
|
|
}
|