G3b packet 1 repair: three defects the white-box tests could not see
Base-free c3 ordering was unsound three ways: it never checked that the two measures share a StaffInstance, never checked they are live, and ordered by OperationId, whose Ord is (replica, counter) authoring identity rather than canonical reduction order. It now reads the parent from measure_values, requires both Live, and compares the minters' stamp reduction tuples -- the same tuple chain_recency uses. The old test discarded the parent, so it encoded the bug. An empty effective grid refused instead of abstaining. Pin 6c case 1 makes a missing active signature vacuous for agreement and abstaining for boundary distance, but agreement grouped GoverningElement::None with a disagreeing Unique, and the boundary clause collapsed None into Indeterminate and then failed closed. None and Indeterminate are now separate outcomes. The white-box selector test passed throughout, so the new coverage is end-to-end. Simultaneous prospective whole-grid and per-key restorations had no tie-break: Recency::Prospective is a unit variant, so both compared equal and the strict > handed it to the whole grid -- while real restoration records the meter change after the whole grid under the same undo operation. The per-key write now governs on equal recency, scoped to the prospective family only; a plain >= would delete the whole grid's entry when both sides are Base. The aggregate test asserts the oracle against the materialized post-undo graph, not against itself. Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md pins 6, 6c, 7, 9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
parent
e64a4b7103
commit
b92023a032
|
|
@ -4600,9 +4600,38 @@ impl<'a> Reducer<'a> {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
// Repair (spec/CONTRACT_GENESIS_G3B_MEASURE.md pin 6 c3, review
|
||||||
|
// fix): c3 requires both measures in the SAME
|
||||||
|
// `StaffInstance.measures` — the graph-aware branch above
|
||||||
|
// enforces this implicitly (it only returns from within one
|
||||||
|
// instance's Vec), so the base-free branch must check it
|
||||||
|
// explicitly via `measure_values`'s carried parent id.
|
||||||
|
let (parent_a, _) = self.measure_values.get(&a)?;
|
||||||
|
let (parent_b, _) = self.measure_values.get(&b)?;
|
||||||
|
if parent_a != parent_b {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Both measures must be LIVE — a retained-but-tombstoned entry
|
||||||
|
// (pin 10.4's retention discipline) must not contribute to the
|
||||||
|
// ordering.
|
||||||
|
if !matches!(
|
||||||
|
self.objects.get(&TypedObjectId::Measure(a)),
|
||||||
|
Some(ObjectState::Live)
|
||||||
|
) || !matches!(
|
||||||
|
self.objects.get(&TypedObjectId::Measure(b)),
|
||||||
|
Some(ObjectState::Live)
|
||||||
|
) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Order by the minters' canonical reduction-order stamp tuple —
|
||||||
|
// NOT by `OperationId`'s own `Ord`, which is `(replica,
|
||||||
|
// counter)` authoring identity (ids.rs:409), not reduction
|
||||||
|
// order. This is the same tuple `chain_recency` compares.
|
||||||
let oa = self.minted_by.get(&TypedObjectId::Measure(a))?;
|
let oa = self.minted_by.get(&TypedObjectId::Measure(a))?;
|
||||||
let ob = self.minted_by.get(&TypedObjectId::Measure(b))?;
|
let ob = self.minted_by.get(&TypedObjectId::Measure(b))?;
|
||||||
Some(oa.cmp(ob))
|
let ta = self.env_of(*oa)?.stamp.reduction_tuple();
|
||||||
|
let tb = self.env_of(*ob)?.stamp.reduction_tuple();
|
||||||
|
Some(ta.cmp(&tb))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4844,7 +4873,23 @@ impl<'a> Reducer<'a> {
|
||||||
let per_key_is_prospective = meter_change_overrides.contains_key(&key);
|
let per_key_is_prospective = meter_change_overrides.contains_key(&key);
|
||||||
let per_key_recency = self.chain_recency(per_key_chain, per_key_is_prospective);
|
let per_key_recency = self.chain_recency(per_key_chain, per_key_is_prospective);
|
||||||
|
|
||||||
if per_key_recency > whole_recency {
|
// Repair (spec/CONTRACT_GENESIS_G3B_MEASURE.md pin 6c, review
|
||||||
|
// fix): `Recency::Prospective` is a unit variant, so two
|
||||||
|
// simultaneous prospective restorations (a whole-grid
|
||||||
|
// restoration and a per-key restoration under the same undo)
|
||||||
|
// compare EQUAL, and a strict `>` would hand governance to the
|
||||||
|
// whole grid even though restoration actually applies the
|
||||||
|
// per-key write AFTER the whole grid, under the same undo
|
||||||
|
// operation id. Ratified tie-break: on equal recency, the
|
||||||
|
// per-key write governs — but ONLY for the prospective family.
|
||||||
|
// Do NOT widen this to a plain `>=`: when both sides are
|
||||||
|
// `Recency::Base` the per-key chain has no write, so `value`
|
||||||
|
// below resolves to `None` and the overlay would DELETE the
|
||||||
|
// whole grid's entry at this key — a regression.
|
||||||
|
if per_key_recency > whole_recency
|
||||||
|
|| (per_key_recency == Recency::Prospective
|
||||||
|
&& whole_recency == Recency::Prospective)
|
||||||
|
{
|
||||||
let value: Option<MeterChange> = if per_key_is_prospective {
|
let value: Option<MeterChange> = if per_key_is_prospective {
|
||||||
meter_change_overrides.get(&key).cloned().flatten()
|
meter_change_overrides.get(&key).cloned().flatten()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -5065,40 +5110,54 @@ impl<'a> Reducer<'a> {
|
||||||
if let Some(sig) = op.measure.time_signature {
|
if let Some(sig) = op.measure.time_signature {
|
||||||
match self.governing_time_signature(&sequence, &op.measure.start) {
|
match self.governing_time_signature(&sequence, &op.measure.start) {
|
||||||
GoverningElement::Unique(active) if active == sig => {}
|
GoverningElement::Unique(active) if active == sig => {}
|
||||||
GoverningElement::Unique(_) | GoverningElement::None => {
|
GoverningElement::Unique(_) => {
|
||||||
return OperationEffect::NoOp {
|
return OperationEffect::NoOp {
|
||||||
reason: NoOpReason::PreconditionFailedUnderReduction {
|
reason: NoOpReason::PreconditionFailedUnderReduction {
|
||||||
reason: PreconditionFailureReason::MeasureMeterMismatch,
|
reason: PreconditionFailureReason::MeasureMeterMismatch,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Pin 6c case 1 / pin 7: no active signature governs this
|
||||||
|
// start — agreement is vacuous, not a violation. Fall
|
||||||
|
// through (no refusal).
|
||||||
|
GoverningElement::None => {}
|
||||||
GoverningElement::Indeterminate => return unverifiable(),
|
GoverningElement::Indeterminate => return unverifiable(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pin 9 clause 3: boundary distance — vacuous for the first measure.
|
// Pin 9 clause 3: boundary distance — vacuous for the first measure.
|
||||||
|
// `None` (no active signature governs the previous start) and
|
||||||
|
// `Indeterminate` (selection cannot be resolved) are NOT the same
|
||||||
|
// outcome: pin 6c case 1 makes `None` an abstention (allow the
|
||||||
|
// mint), while `Indeterminate` still fails closed with
|
||||||
|
// `MeasureOrderUnverifiable` (pin 7). Collapsing them was the bug.
|
||||||
if let Some(prev_start) = &predecessor {
|
if let Some(prev_start) = &predecessor {
|
||||||
let prev_duration: Option<MusicalDuration> =
|
match self.governing_time_signature(&sequence, prev_start) {
|
||||||
match self.governing_time_signature(&sequence, prev_start) {
|
GoverningElement::Unique(sig) => {
|
||||||
GoverningElement::Unique(sig) => self
|
let expected = self
|
||||||
.time_signature_values
|
.time_signature_values
|
||||||
.get(&sig)
|
.get(&sig)
|
||||||
.map(|ts| ts.measure_duration().clone()),
|
.map(|ts| ts.measure_duration().clone());
|
||||||
GoverningElement::None | GoverningElement::Indeterminate => None,
|
match (
|
||||||
};
|
self.anchor_musical_delta(prev_start, &op.measure.start),
|
||||||
match (
|
expected,
|
||||||
self.anchor_musical_delta(prev_start, &op.measure.start),
|
) {
|
||||||
prev_duration,
|
(Some(delta), Some(expected)) if delta == expected => {}
|
||||||
) {
|
(Some(_), Some(_)) => {
|
||||||
(Some(delta), Some(expected)) if delta == expected => {}
|
return OperationEffect::NoOp {
|
||||||
(Some(_), Some(_)) => {
|
reason: NoOpReason::PreconditionFailedUnderReduction {
|
||||||
return OperationEffect::NoOp {
|
reason: PreconditionFailureReason::MeasureMeterMismatch,
|
||||||
reason: NoOpReason::PreconditionFailedUnderReduction {
|
},
|
||||||
reason: PreconditionFailureReason::MeasureMeterMismatch,
|
};
|
||||||
},
|
}
|
||||||
};
|
_ => return unverifiable(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => return unverifiable(),
|
// Pin 6c case 1 / pin 7: no active signature governs the
|
||||||
|
// previous measure's start — boundary consistency abstains.
|
||||||
|
// Not a violation; allow the mint.
|
||||||
|
GoverningElement::None => {}
|
||||||
|
GoverningElement::Indeterminate => return unverifiable(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17391,15 +17450,43 @@ mod tests {
|
||||||
/// packet 2, reuses these same functions).
|
/// packet 2, reuses these same functions).
|
||||||
#[test]
|
#[test]
|
||||||
fn g3b_comparable_offsets_and_anchor_shapes() {
|
fn g3b_comparable_offsets_and_anchor_shapes() {
|
||||||
let op_set = OperationSet::new();
|
|
||||||
let mut r = Reducer::new(&op_set);
|
|
||||||
|
|
||||||
let ev_a = EventId::new(ReplicaId(1), 1);
|
let ev_a = EventId::new(ReplicaId(1), 1);
|
||||||
let ev_b = EventId::new(ReplicaId(1), 2);
|
let ev_b = EventId::new(ReplicaId(1), 2);
|
||||||
let m_a = MeasureId::new(ReplicaId(1), 10);
|
let m_a = MeasureId::new(ReplicaId(1), 10);
|
||||||
let m_b = MeasureId::new(ReplicaId(1), 11);
|
let m_b = MeasureId::new(ReplicaId(1), 11);
|
||||||
let rg_a = RegionId::new(ReplicaId(1), 20);
|
let rg_a = RegionId::new(ReplicaId(1), 20);
|
||||||
let rg_b = RegionId::new(ReplicaId(1), 21);
|
let rg_b = RegionId::new(ReplicaId(1), 21);
|
||||||
|
let instance = StaffInstanceId::new(ReplicaId(1), 99);
|
||||||
|
|
||||||
|
// Repair 1 (spec/CONTRACT_GENESIS_G3B_MEASURE.md, review fix): the
|
||||||
|
// base-free c3 branch now reads real minter envelopes' stamp
|
||||||
|
// reduction tuples, so the op set must actually carry them — a
|
||||||
|
// stamp order that AGREES with OperationId order here (replica 1,
|
||||||
|
// counters 1 then 2, physical 1_000 then 2_000), so this base case
|
||||||
|
// does not itself distinguish the two orderings (that is
|
||||||
|
// `g3b_c3_order_follows_stamp_not_operation_id`, below).
|
||||||
|
let measure_a = g3b_wallclock_measure(m_a, 1_000);
|
||||||
|
let measure_b = g3b_wallclock_measure(m_b, 2_000);
|
||||||
|
let mut op_set = OperationSet::new();
|
||||||
|
op_set.accept_all(vec![
|
||||||
|
g3b_measure_env(1, 1, 1_000, instance, measure_a.clone()),
|
||||||
|
g3b_measure_env(1, 2, 2_000, instance, measure_b.clone()),
|
||||||
|
]);
|
||||||
|
let mut r = Reducer::new(&op_set);
|
||||||
|
r.objects
|
||||||
|
.insert(TypedObjectId::Measure(m_a), ObjectState::Live);
|
||||||
|
r.objects
|
||||||
|
.insert(TypedObjectId::Measure(m_b), ObjectState::Live);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_a),
|
||||||
|
OperationId::new(ReplicaId(1), 1),
|
||||||
|
);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_b),
|
||||||
|
OperationId::new(ReplicaId(1), 2),
|
||||||
|
);
|
||||||
|
r.measure_values.insert(m_a, (instance, measure_a));
|
||||||
|
r.measure_values.insert(m_b, (instance, measure_b));
|
||||||
|
|
||||||
// c1: same Event id, comparable by offset.
|
// c1: same Event id, comparable by offset.
|
||||||
let e1 = TimeAnchor::Event {
|
let e1 = TimeAnchor::Event {
|
||||||
|
|
@ -17461,21 +17548,11 @@ mod tests {
|
||||||
assert_eq!(r.anchor_musical_delta(&ms1, &ms_end), None);
|
assert_eq!(r.anchor_musical_delta(&ms1, &ms_end), None);
|
||||||
|
|
||||||
// c3: distinct Measure ids, both Start+Zero, in the same instance's
|
// c3: distinct Measure ids, both Start+Zero, in the same instance's
|
||||||
// (base-free) mint order — via `minted_by` OperationId comparison.
|
// (base-free) mint order — via the minters' stamp reduction-tuple
|
||||||
let instance = StaffInstanceId::new(ReplicaId(1), 99);
|
// comparison (Repair 1: NOT `OperationId`'s own `Ord`, and both
|
||||||
r.objects
|
// measures must share the SAME `StaffInstanceId` in `measure_values`
|
||||||
.insert(TypedObjectId::Measure(m_a), ObjectState::Live);
|
// and be Live — see `g3b_c3_requires_same_staff_instance` and
|
||||||
r.objects
|
// `g3b_c3_requires_live_measures` below for the negative cases).
|
||||||
.insert(TypedObjectId::Measure(m_b), ObjectState::Live);
|
|
||||||
r.minted_by.insert(
|
|
||||||
TypedObjectId::Measure(m_a),
|
|
||||||
OperationId::new(ReplicaId(1), 1),
|
|
||||||
);
|
|
||||||
r.minted_by.insert(
|
|
||||||
TypedObjectId::Measure(m_b),
|
|
||||||
OperationId::new(ReplicaId(1), 2),
|
|
||||||
);
|
|
||||||
let _ = instance; // c3 does not need the parent directly here; minted_by suffices.
|
|
||||||
let c3_a = TimeAnchor::Measure {
|
let c3_a = TimeAnchor::Measure {
|
||||||
id: m_a,
|
id: m_a,
|
||||||
position: MeasurePosition::Start,
|
position: MeasurePosition::Start,
|
||||||
|
|
@ -17586,6 +17663,185 @@ mod tests {
|
||||||
assert_eq!(r.anchor_musical_delta(&wc1, &wc2), None);
|
assert_eq!(r.anchor_musical_delta(&wc1, &wc2), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Repair 1 regression (spec/CONTRACT_GENESIS_G3B_MEASURE.md pin 6 c3):
|
||||||
|
/// two measures anchored via `Measure{pos: Start, off: Zero}` are NOT
|
||||||
|
/// c3-comparable when `measure_values` records them under DIFFERENT
|
||||||
|
/// `StaffInstanceId`s — the graph-aware branch enforces "same instance"
|
||||||
|
/// implicitly (it only returns from within one instance's `Vec`); the
|
||||||
|
/// base-free branch must check it explicitly.
|
||||||
|
///
|
||||||
|
/// **Mutation:** drop the `parent_a != parent_b` check from
|
||||||
|
/// `measure_vector_order`'s base-free branch. This row must observe red.
|
||||||
|
#[test]
|
||||||
|
fn g3b_c3_requires_same_staff_instance() {
|
||||||
|
let m_a = MeasureId::new(ReplicaId(1), 30);
|
||||||
|
let m_b = MeasureId::new(ReplicaId(1), 31);
|
||||||
|
let instance_a = StaffInstanceId::new(ReplicaId(1), 199);
|
||||||
|
let instance_b = StaffInstanceId::new(ReplicaId(1), 200);
|
||||||
|
|
||||||
|
let measure_a = g3b_wallclock_measure(m_a, 1_000);
|
||||||
|
let measure_b = g3b_wallclock_measure(m_b, 2_000);
|
||||||
|
let mut op_set = OperationSet::new();
|
||||||
|
op_set.accept_all(vec![
|
||||||
|
g3b_measure_env(1, 1, 1_000, instance_a, measure_a.clone()),
|
||||||
|
g3b_measure_env(1, 2, 2_000, instance_b, measure_b.clone()),
|
||||||
|
]);
|
||||||
|
let mut r = Reducer::new(&op_set);
|
||||||
|
r.objects
|
||||||
|
.insert(TypedObjectId::Measure(m_a), ObjectState::Live);
|
||||||
|
r.objects
|
||||||
|
.insert(TypedObjectId::Measure(m_b), ObjectState::Live);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_a),
|
||||||
|
OperationId::new(ReplicaId(1), 1),
|
||||||
|
);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_b),
|
||||||
|
OperationId::new(ReplicaId(1), 2),
|
||||||
|
);
|
||||||
|
// The load-bearing part of this fixture: the two measures are
|
||||||
|
// recorded under DIFFERENT parent instances.
|
||||||
|
r.measure_values.insert(m_a, (instance_a, measure_a));
|
||||||
|
r.measure_values.insert(m_b, (instance_b, measure_b));
|
||||||
|
|
||||||
|
let c3_a = TimeAnchor::Measure {
|
||||||
|
id: m_a,
|
||||||
|
position: MeasurePosition::Start,
|
||||||
|
offset: AnchorOffset::Zero,
|
||||||
|
};
|
||||||
|
let c3_b = TimeAnchor::Measure {
|
||||||
|
id: m_b,
|
||||||
|
position: MeasurePosition::Start,
|
||||||
|
offset: AnchorOffset::Zero,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
r.anchors_comparable_order(&c3_a, &c3_b),
|
||||||
|
None,
|
||||||
|
"c3: measures anchored in DIFFERENT StaffInstances must not be comparable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repair 1 regression: a c3 comparison against a measure that is not
|
||||||
|
/// `Live` (e.g. retained-but-tombstoned, pin 10.4's retention
|
||||||
|
/// discipline) must not contribute to the ordering.
|
||||||
|
///
|
||||||
|
/// **Mutation:** drop the `Live` conjunct from `measure_vector_order`'s
|
||||||
|
/// base-free branch. This row must observe red.
|
||||||
|
#[test]
|
||||||
|
fn g3b_c3_requires_live_measures() {
|
||||||
|
let m_a = MeasureId::new(ReplicaId(1), 32);
|
||||||
|
let m_b = MeasureId::new(ReplicaId(1), 33);
|
||||||
|
let instance = StaffInstanceId::new(ReplicaId(1), 201);
|
||||||
|
|
||||||
|
let measure_a = g3b_wallclock_measure(m_a, 1_000);
|
||||||
|
let measure_b = g3b_wallclock_measure(m_b, 2_000);
|
||||||
|
let mut op_set = OperationSet::new();
|
||||||
|
op_set.accept_all(vec![
|
||||||
|
g3b_measure_env(1, 1, 1_000, instance, measure_a.clone()),
|
||||||
|
g3b_measure_env(1, 2, 2_000, instance, measure_b.clone()),
|
||||||
|
]);
|
||||||
|
let mut r = Reducer::new(&op_set);
|
||||||
|
r.objects
|
||||||
|
.insert(TypedObjectId::Measure(m_a), ObjectState::Live);
|
||||||
|
// b is tombstoned, not live, but its value is still retained (pin
|
||||||
|
// 10.4's uniform retention discipline).
|
||||||
|
r.objects.insert(
|
||||||
|
TypedObjectId::Measure(m_b),
|
||||||
|
ObjectState::Tombstoned {
|
||||||
|
deleted_by: OperationId::new(ReplicaId(9), 9),
|
||||||
|
minted_by: OperationId::new(ReplicaId(1), 2),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_a),
|
||||||
|
OperationId::new(ReplicaId(1), 1),
|
||||||
|
);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_b),
|
||||||
|
OperationId::new(ReplicaId(1), 2),
|
||||||
|
);
|
||||||
|
r.measure_values.insert(m_a, (instance, measure_a));
|
||||||
|
r.measure_values.insert(m_b, (instance, measure_b));
|
||||||
|
|
||||||
|
let c3_a = TimeAnchor::Measure {
|
||||||
|
id: m_a,
|
||||||
|
position: MeasurePosition::Start,
|
||||||
|
offset: AnchorOffset::Zero,
|
||||||
|
};
|
||||||
|
let c3_b = TimeAnchor::Measure {
|
||||||
|
id: m_b,
|
||||||
|
position: MeasurePosition::Start,
|
||||||
|
offset: AnchorOffset::Zero,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
r.anchors_comparable_order(&c3_a, &c3_b),
|
||||||
|
None,
|
||||||
|
"c3: a Tombstoned measure must not contribute to the ordering"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repair 1 regression: `measure_vector_order`'s base-free branch must
|
||||||
|
/// order by the minters' canonical reduction-order STAMP tuple, not by
|
||||||
|
/// `OperationId`'s own `(replica, counter)` `Ord` (ids.rs:409). Here `m_a`
|
||||||
|
/// was minted with the LARGER counter but the EARLIER physical stamp, and
|
||||||
|
/// `m_b` with the SMALLER counter but the LATER physical stamp — the two
|
||||||
|
/// orderings disagree, so this is the only fixture that distinguishes
|
||||||
|
/// them.
|
||||||
|
///
|
||||||
|
/// **Mutation:** revert the base-free branch to `oa.cmp(ob)` on the raw
|
||||||
|
/// `OperationId`s. This row must observe red (it would report
|
||||||
|
/// `Some(Ordering::Greater)`, the opposite of the correct answer).
|
||||||
|
#[test]
|
||||||
|
fn g3b_c3_order_follows_stamp_not_operation_id() {
|
||||||
|
let m_a = MeasureId::new(ReplicaId(1), 34);
|
||||||
|
let m_b = MeasureId::new(ReplicaId(1), 35);
|
||||||
|
let instance = StaffInstanceId::new(ReplicaId(1), 202);
|
||||||
|
|
||||||
|
let measure_a = g3b_wallclock_measure(m_a, 1_000);
|
||||||
|
let measure_b = g3b_wallclock_measure(m_b, 2_000);
|
||||||
|
let mut op_set = OperationSet::new();
|
||||||
|
op_set.accept_all(vec![
|
||||||
|
// m_a: OperationId counter 2, but the EARLIER physical stamp.
|
||||||
|
g3b_measure_env(1, 2, 100, instance, measure_a.clone()),
|
||||||
|
// m_b: OperationId counter 1, but the LATER physical stamp.
|
||||||
|
g3b_measure_env(1, 1, 200, instance, measure_b.clone()),
|
||||||
|
]);
|
||||||
|
let mut r = Reducer::new(&op_set);
|
||||||
|
r.objects
|
||||||
|
.insert(TypedObjectId::Measure(m_a), ObjectState::Live);
|
||||||
|
r.objects
|
||||||
|
.insert(TypedObjectId::Measure(m_b), ObjectState::Live);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_a),
|
||||||
|
OperationId::new(ReplicaId(1), 2),
|
||||||
|
);
|
||||||
|
r.minted_by.insert(
|
||||||
|
TypedObjectId::Measure(m_b),
|
||||||
|
OperationId::new(ReplicaId(1), 1),
|
||||||
|
);
|
||||||
|
r.measure_values.insert(m_a, (instance, measure_a));
|
||||||
|
r.measure_values.insert(m_b, (instance, measure_b));
|
||||||
|
|
||||||
|
let c3_a = TimeAnchor::Measure {
|
||||||
|
id: m_a,
|
||||||
|
position: MeasurePosition::Start,
|
||||||
|
offset: AnchorOffset::Zero,
|
||||||
|
};
|
||||||
|
let c3_b = TimeAnchor::Measure {
|
||||||
|
id: m_b,
|
||||||
|
position: MeasurePosition::Start,
|
||||||
|
offset: AnchorOffset::Zero,
|
||||||
|
};
|
||||||
|
// OperationId order says m_b (counter 1) < m_a (counter 2); stamp
|
||||||
|
// order says m_a (physical 100) < m_b (physical 200). The correct
|
||||||
|
// answer follows the stamp: c3_a (m_a) is Less than c3_b (m_b).
|
||||||
|
assert_eq!(
|
||||||
|
r.anchors_comparable_order(&c3_a, &c3_b),
|
||||||
|
Some(Ordering::Less),
|
||||||
|
"c3 ordering must follow the minters' stamp reduction tuple, not OperationId::cmp"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn g3b_region_anchor(region: RegionId, n: i32) -> TimeAnchor {
|
fn g3b_region_anchor(region: RegionId, n: i32) -> TimeAnchor {
|
||||||
TimeAnchor::Region {
|
TimeAnchor::Region {
|
||||||
id: region,
|
id: region,
|
||||||
|
|
@ -17858,6 +18114,190 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Repair 3 (spec/CONTRACT_GENESIS_G3B_MEASURE.md pin 6c, review fix): a
|
||||||
|
/// SINGLE undo restoring both a whole-grid write and a per-key
|
||||||
|
/// meter-change write, at the SAME position, under the SAME undo
|
||||||
|
/// operation id — so both restorations tie under `Recency::Prospective`
|
||||||
|
/// when evaluated in flight (pin 9c's aggregate safety check, before the
|
||||||
|
/// undo commits). The ratified tie-break — per-key governs on a tie, but
|
||||||
|
/// ONLY within the prospective family — must make that PROSPECTIVE
|
||||||
|
/// prediction agree with what restoration ACTUALLY installs once it runs
|
||||||
|
/// for real: production code collects and applies every
|
||||||
|
/// `metric_grid_chain` restoration before it begins `meter_change_chain`
|
||||||
|
/// restorations (`reduce.rs` ~6419-6457), so the per-key predecessor is
|
||||||
|
/// written to the graph strictly AFTER the whole-grid predecessor, and
|
||||||
|
/// must be what's left standing.
|
||||||
|
#[test]
|
||||||
|
fn g3b_grid_oracle_aggregate_prospective_restoration_matches_real_undo() {
|
||||||
|
let region = RegionId::new(ReplicaId(1), 1);
|
||||||
|
let instance = StaffInstanceId::new(ReplicaId(1), 2);
|
||||||
|
let staff = StaffId::new(ReplicaId(1), 3);
|
||||||
|
let sig_whole_baseline = TimeSignatureId::new(ReplicaId(1), 10);
|
||||||
|
let sig_perkey_baseline = TimeSignatureId::new(ReplicaId(1), 11);
|
||||||
|
let sig_whole_new = TimeSignatureId::new(ReplicaId(1), 12);
|
||||||
|
let sig_perkey_new = TimeSignatureId::new(ReplicaId(1), 13);
|
||||||
|
let pos0 = g3b_region_anchor(region, 0);
|
||||||
|
let key = MusicalPosition(RationalTime::from_int(0));
|
||||||
|
let tx = TransactionId::new(ReplicaId(1), 900);
|
||||||
|
|
||||||
|
let create_region = prim_env(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CausalContext::new(),
|
||||||
|
OperationKind::CreateRegion(CreateRegionOp {
|
||||||
|
region: crate::valuegen::region(region),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let create_instance = prim_env(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
seen_r1(0),
|
||||||
|
OperationKind::CreateStaffInstance(CreateStaffInstanceOp {
|
||||||
|
region,
|
||||||
|
instance: crate::valuegen::staff_instance(instance, staff),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Pre-transaction baseline: a whole-grid write, then a per-key write
|
||||||
|
// that overlays it at pos0 — these establish the PREDECESSORS the
|
||||||
|
// undo below will restore to.
|
||||||
|
let set_grid_baseline = prim_env(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
seen_r1(1),
|
||||||
|
OperationKind::SetMetricGrid(SetMetricGridOp {
|
||||||
|
region,
|
||||||
|
grid: Some(MetricGrid {
|
||||||
|
meter_sequence: vec![MeterChange {
|
||||||
|
anchor: pos0.clone(),
|
||||||
|
time_signature: sig_whole_baseline,
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let set_time_sig_baseline = prim_env(
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
seen_r1(2),
|
||||||
|
OperationKind::SetTimeSignature(SetTimeSignatureOp {
|
||||||
|
region,
|
||||||
|
anchor: pos0.clone(),
|
||||||
|
time_signature: Some(crate::valuegen::time_signature(sig_perkey_baseline, 3)),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The transaction: overwrites BOTH the whole grid and the per-key
|
||||||
|
// change, at the SAME position, in one undoable unit.
|
||||||
|
let decl = declare_transaction(1, 10, 10, seen_r1(3), tx);
|
||||||
|
let tx_set_grid = tx_member(
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
11,
|
||||||
|
seen_r1(10),
|
||||||
|
tx,
|
||||||
|
OperationKind::SetMetricGrid(SetMetricGridOp {
|
||||||
|
region,
|
||||||
|
grid: Some(MetricGrid {
|
||||||
|
meter_sequence: vec![MeterChange {
|
||||||
|
anchor: pos0.clone(),
|
||||||
|
time_signature: sig_whole_new,
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let tx_set_time_sig = tx_member(
|
||||||
|
1,
|
||||||
|
12,
|
||||||
|
12,
|
||||||
|
seen_r1(11),
|
||||||
|
tx,
|
||||||
|
OperationKind::SetTimeSignature(SetTimeSignatureOp {
|
||||||
|
region,
|
||||||
|
anchor: pos0.clone(),
|
||||||
|
time_signature: Some(crate::valuegen::time_signature(sig_perkey_new, 3)),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let undo = undo_env(1, 20, 20, seen_r1(12), tx, UndoPolicy::StrictInverse);
|
||||||
|
|
||||||
|
let mut set = OperationSet::new();
|
||||||
|
set.accept_all(vec![
|
||||||
|
create_region,
|
||||||
|
create_instance,
|
||||||
|
set_grid_baseline,
|
||||||
|
set_time_sig_baseline,
|
||||||
|
decl,
|
||||||
|
tx_set_grid,
|
||||||
|
tx_set_time_sig,
|
||||||
|
undo,
|
||||||
|
]);
|
||||||
|
let identity = IdentityContext::new(ReplicaId(1));
|
||||||
|
let out = reduce_operation_set_onto(&set, &Score::empty(identity));
|
||||||
|
|
||||||
|
// What restoration ACTUALLY installs: read the materialized graph.
|
||||||
|
let region_value = out
|
||||||
|
.score
|
||||||
|
.canvas
|
||||||
|
.regions
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.id == region)
|
||||||
|
.expect("region present");
|
||||||
|
let installed = region_value
|
||||||
|
.content
|
||||||
|
.staff_based()
|
||||||
|
.and_then(|c| c.default_metric_grid.as_ref())
|
||||||
|
.expect("a default metric grid after restoration")
|
||||||
|
.meter_sequence
|
||||||
|
.clone();
|
||||||
|
assert_eq!(
|
||||||
|
installed,
|
||||||
|
vec![MeterChange {
|
||||||
|
anchor: pos0.clone(),
|
||||||
|
time_signature: sig_perkey_baseline
|
||||||
|
}],
|
||||||
|
"actual restoration installs the whole-grid predecessor, then \
|
||||||
|
overlays the per-key predecessor at pos0 (applied strictly \
|
||||||
|
after) — the per-key value must be what the graph shows"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The oracle's PROSPECTIVE prediction, evaluated as pin 9c's
|
||||||
|
// aggregate safety check would (BEFORE the undo commits): both the
|
||||||
|
// whole-grid and per-key restorations are simultaneously "in
|
||||||
|
// flight" under the SAME undo, so both tie under
|
||||||
|
// `Recency::Prospective`.
|
||||||
|
let op_set = OperationSet::new();
|
||||||
|
let r = Reducer::new(&op_set);
|
||||||
|
let prospective_grid = Some(MetricGrid {
|
||||||
|
meter_sequence: vec![MeterChange {
|
||||||
|
anchor: pos0.clone(),
|
||||||
|
time_signature: sig_whole_baseline,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
let mut meter_change_overrides = BTreeMap::new();
|
||||||
|
meter_change_overrides.insert(
|
||||||
|
key,
|
||||||
|
Some(MeterChange {
|
||||||
|
anchor: pos0.clone(),
|
||||||
|
time_signature: sig_perkey_baseline,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let predicted = r.effective_grid(
|
||||||
|
instance,
|
||||||
|
Some(region),
|
||||||
|
Some(&prospective_grid),
|
||||||
|
&meter_change_overrides,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
predicted, installed,
|
||||||
|
"the aggregate prospective evaluation must agree with what \
|
||||||
|
restoration actually installs (pin 6c's ratified tie-break: on \
|
||||||
|
equal recency, the per-key write governs — but ONLY for the \
|
||||||
|
prospective family)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// (M30b) The effective-grid oracle consults ONLY the ledgers
|
/// (M30b) The effective-grid oracle consults ONLY the ledgers
|
||||||
/// (`instance_grid`, `metric_grid_chain`, `meter_change_chain`) — never
|
/// (`instance_grid`, `metric_grid_chain`, `meter_change_chain`) — never
|
||||||
/// `self.graph` — so graph-aware and base-free reduction run the
|
/// `self.graph` — so graph-aware and base-free reduction run the
|
||||||
|
|
@ -18380,4 +18820,94 @@ mod tests {
|
||||||
immediate-violation gap"
|
immediate-violation gap"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Repair 2 (spec/CONTRACT_GENESIS_G3B_MEASURE.md pin 6c case 1 / pin 7):
|
||||||
|
/// creating a measure against a WHOLLY EMPTY effective grid — no
|
||||||
|
/// `local_metric_grid`, no region `default_metric_grid`, and no
|
||||||
|
/// `SetMetricGrid`/`SetTimeSignature` writes anywhere — must Applied. No
|
||||||
|
/// active signature means agreement is vacuous and the boundary clause
|
||||||
|
/// abstains; neither is a violation.
|
||||||
|
///
|
||||||
|
/// The original defect wrongly grouped `GoverningElement::None` with a
|
||||||
|
/// disagreeing `Unique` in the agreement match (`MeasureMeterMismatch`),
|
||||||
|
/// and collapsed `None` into `Indeterminate` for the boundary clause's
|
||||||
|
/// `prev_duration`, so the final `_ => unverifiable()` arm fired
|
||||||
|
/// (`MeasureOrderUnverifiable`) once a computable delta met a `None`
|
||||||
|
/// duration.
|
||||||
|
///
|
||||||
|
/// This is deliberately END-TO-END: the white-box selector test
|
||||||
|
/// (`g3b_grid_oracle_governing_selection`) already exercises
|
||||||
|
/// `governing_by_anchor`'s `None` outcome correctly in isolation — the
|
||||||
|
/// bug was entirely in how `create_measure`'s caller handled that
|
||||||
|
/// `None`, so only a real mint through the caller proves the fix.
|
||||||
|
#[test]
|
||||||
|
fn g3b_create_measure_applies_against_an_empty_effective_grid_end_to_end() {
|
||||||
|
let region = RegionId::new(ReplicaId(1), 60);
|
||||||
|
let instance = StaffInstanceId::new(ReplicaId(1), 61);
|
||||||
|
let staff = StaffId::new(ReplicaId(1), 62);
|
||||||
|
// No SetMetricGrid, no SetTimeSignature: the effective grid is
|
||||||
|
// empty for the whole life of this test.
|
||||||
|
let mut envs = g3b_region_and_instance_envs(1, region, instance, staff);
|
||||||
|
|
||||||
|
fn measure(id: u64, start: TimeAnchor, sig: Option<TimeSignatureId>) -> Measure {
|
||||||
|
Measure {
|
||||||
|
id: MeasureId::new(ReplicaId(1), id),
|
||||||
|
start,
|
||||||
|
time_signature: sig,
|
||||||
|
explicit_number: None,
|
||||||
|
number_visibility: epiphany_core::MeasureNumberVisibility::Auto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first measure of the instance: clauses 1 & 3 are vacuous for
|
||||||
|
// it regardless of Repair 2 (pin 9's pickup exemption), so it does
|
||||||
|
// not by itself distinguish the bug from the fix.
|
||||||
|
let first = g3b_measure_env(
|
||||||
|
1,
|
||||||
|
70,
|
||||||
|
70,
|
||||||
|
instance,
|
||||||
|
measure(300, g3b_region_anchor(region, 0), None),
|
||||||
|
);
|
||||||
|
// The SECOND measure: comparable to, and a nonzero Musical delta
|
||||||
|
// from, the first (c4 — same Region id and edge), AND declaring a
|
||||||
|
// time signature — even though nothing ever wrote the grid. Under
|
||||||
|
// the original bug, the agreement clause alone already refused
|
||||||
|
// (`MeasureMeterMismatch`) before the boundary clause was reached;
|
||||||
|
// dropping `time_signature` to `None` would additionally exercise
|
||||||
|
// the boundary-only half of the same defect.
|
||||||
|
let second = g3b_measure_env(
|
||||||
|
1,
|
||||||
|
71,
|
||||||
|
71,
|
||||||
|
instance,
|
||||||
|
measure(
|
||||||
|
301,
|
||||||
|
TimeAnchor::Region {
|
||||||
|
id: region,
|
||||||
|
edge: RegionEdge::Start,
|
||||||
|
offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(4))),
|
||||||
|
},
|
||||||
|
Some(TimeSignatureId::new(ReplicaId(1), 999)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
envs.extend([first.clone(), second.clone()]);
|
||||||
|
let mut set = OperationSet::new();
|
||||||
|
set.accept_all(envs);
|
||||||
|
let state = set.reduce();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
g3b_effect_of(&state, first.id),
|
||||||
|
Some(OperationEffect::Applied)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
g3b_effect_of(&state, second.id),
|
||||||
|
Some(OperationEffect::Applied),
|
||||||
|
"an empty effective grid (no active signature anywhere) must be \
|
||||||
|
vacuous agreement and an abstaining boundary — NOT \
|
||||||
|
MeasureMeterMismatch and NOT MeasureOrderUnverifiable \
|
||||||
|
(pin 6c case 1 / pin 7)"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue