Pass 13 — P13-D2: tombstone before the graph delete, closing the cue-cascade trail
delete_event tombstoned the event in objects AFTER materialize_graph_delete —
but that graph pass cascades a cue among the event's referents, running
reanchor_for_tombstone over the cue's referents while the source event is still
Live. A slur bridging {X, cue-of-X} therefore re-anchored onto X
(Reanchored{to: X}) and then cascade-deleted when X's tombstone landed a line
later: a contradictory same-effect trail (candidate was "plausible by code
trace, unexecuted" — now executed; reverting the fix reproduces exactly that
two-record trail).
Fix: tombstone the event in objects BEFORE the graph delete, matching the
conventions cascade_cue and tombstone_undo_targets already follow (both
tombstone before their graph delete — which is why the undo path never had this
bug). The bridging slur now sees X already dead during the cue cascade and
cascades once.
Regression: deleting_a_cue_source_does_not_leave_a_contradictory_repair_for_a_
bridging_slur_p13_d2 (built on a cue-bearing base with a bridging slur; asserts
exactly one CascadeDeleted, no Reanchored). Verified to fail without the fix.
940 tests, convergence/conformance green.
PASS13_CANDIDATES.md: P13-D2 resolved — batch CLOSED (all four candidates done).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
79c8e1da2e
commit
430214d619
|
|
@ -1098,6 +1098,21 @@ simply never exercised — no existing test broke). Locked by
|
|||
`undo_orphaning_a_pre_existing_slur_cascades_it_in_the_ledger_p13_d1`
|
||||
(cascade + recorded repair + order-independent convergence).
|
||||
|
||||
**P13-D2 resolved (Pass 13, 2026-07-08).** `delete_event` tombstoned the event
|
||||
in `objects` *after* `materialize_graph_delete` — but that graph pass cascades
|
||||
a cue among the event's referents, running `reanchor_for_tombstone` over the
|
||||
cue's own referents while the source event is still `Live`. A slur bridging
|
||||
{X, cue-of-X} therefore re-anchored onto X (recording `Reanchored{to: X}`) and
|
||||
then cascade-deleted when X's tombstone landed a line later — a contradictory
|
||||
same-effect trail (candidate: "plausible by code trace, unexecuted"; now
|
||||
executed — reverting the fix reproduces exactly that two-record trail). Fixed
|
||||
by tombstoning the event in `objects` **before** the graph delete, matching the
|
||||
conventions `cascade_cue` and `tombstone_undo_targets` already follow (both
|
||||
tombstone before their graph delete — which is why the undo path never had this
|
||||
bug). The bridging slur now sees X already dead during the cue cascade and
|
||||
cascades once. Locked by
|
||||
`deleting_a_cue_source_does_not_leave_a_contradictory_repair_for_a_bridging_slur_p13_d2`.
|
||||
|
||||
**Noted, not implemented:** no writer path derives a chunk schema *minor*
|
||||
from appended kind discriminants (a major-0 block carrying discriminant 29
|
||||
stamps the same fixed minor as always) — pre-existing for the Phase-3
|
||||
|
|
|
|||
|
|
@ -2897,8 +2897,15 @@ impl<'a> Reducer<'a> {
|
|||
.and_then(|score| score.events.get(op.event))
|
||||
.map(Event::voice)
|
||||
});
|
||||
let graph_repairs = self.materialize_graph_delete(env, op);
|
||||
|
||||
// Tombstone the event in `objects` BEFORE the graph delete (P13-D2).
|
||||
// `materialize_graph_delete` re-anchors the deleted event's referents,
|
||||
// and a cue among them cascades — running `reanchor_for_tombstone` over
|
||||
// the cue's own referents. A structure anchored on {this event,
|
||||
// cue-of-this-event} must see this event as already dead there, or it
|
||||
// would re-anchor onto it (recording `Reanchored{to: X}`) only to
|
||||
// cascade when X's tombstone lands a line later — a contradictory
|
||||
// same-effect trail. Tombstoning first matches `cascade_cue` and the
|
||||
// undo path, which already tombstone before their graph delete.
|
||||
let minter = self.minted_by.get(&ev_obj).copied().unwrap_or(env.id);
|
||||
self.objects.insert(
|
||||
ev_obj,
|
||||
|
|
@ -2907,6 +2914,7 @@ impl<'a> Reducer<'a> {
|
|||
minted_by: minter,
|
||||
},
|
||||
);
|
||||
let graph_repairs = self.materialize_graph_delete(env, op);
|
||||
for events in self.voice_occupancy.values_mut() {
|
||||
events.retain(|(_, _, event)| *event != op.event);
|
||||
}
|
||||
|
|
@ -9705,6 +9713,104 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_a_cue_source_does_not_leave_a_contradictory_repair_for_a_bridging_slur_p13_d2() {
|
||||
// P13-D2: a slur Z anchored on {X, cue-of-X}. Deleting X cascades the
|
||||
// cue (a cue depends on its source event). The cue-cascade's ledger
|
||||
// re-anchor must NOT re-anchor Z onto X — X is itself being deleted —
|
||||
// which would record `Reanchored{to: X}` for Z and then `CascadeDeleted`
|
||||
// in the same effect (a contradictory trail). With X tombstoned before
|
||||
// the graph delete, Z sees no live survivor and cascades once.
|
||||
use epiphany_core::generators::valid_score;
|
||||
use epiphany_core::{
|
||||
check_invariants, CueEvent, CueRendering, Event, EventDuration, EventPosition,
|
||||
MusicalDuration, MusicalPosition, RationalTime, SlurId,
|
||||
};
|
||||
let mut base = valid_score(0x5EED);
|
||||
let (voice_id, x, count) = {
|
||||
let v = &base.canvas.regions[0].staff_instances()[0].voices[0];
|
||||
(v.id, v.events[0], v.events.len() as i64)
|
||||
};
|
||||
let cue = EventId::new(ReplicaId(9), 9_001);
|
||||
let slur_id = SlurId::new(ReplicaId(9), 9_002);
|
||||
let sid = TypedObjectId::Slur(slur_id);
|
||||
// A cue sourced on X, placed just past the voice's last event, and a
|
||||
// slur bridging X and its cue.
|
||||
base.events
|
||||
.insert(Event::Cue(CueEvent {
|
||||
id: cue,
|
||||
voice: voice_id,
|
||||
position: EventPosition::Musical(MusicalPosition(
|
||||
RationalTime::new(count, 4).unwrap(),
|
||||
)),
|
||||
duration: EventDuration::Musical(MusicalDuration(RationalTime::new(1, 4).unwrap())),
|
||||
source: vec![x],
|
||||
rendering: CueRendering,
|
||||
}))
|
||||
.expect("fresh cue id");
|
||||
base.canvas.regions[0]
|
||||
.content
|
||||
.staff_instances_mut()
|
||||
.expect("staff-based content")[0]
|
||||
.voices[0]
|
||||
.events
|
||||
.push(cue);
|
||||
base.cross_cutting
|
||||
.slurs
|
||||
.push(crate::valuegen::slur(slur_id, x, cue));
|
||||
assert!(
|
||||
check_invariants(&base).is_empty(),
|
||||
"the cue + bridging-slur base is well-formed"
|
||||
);
|
||||
|
||||
let del = prim_env(
|
||||
2,
|
||||
0,
|
||||
20,
|
||||
CausalContext::new(),
|
||||
OperationKind::DeleteEvent(DeleteEventOp {
|
||||
event: x,
|
||||
tuplet_compensation: TupletCompensation::NotInTuplet,
|
||||
}),
|
||||
);
|
||||
let mut set = OperationSet::new();
|
||||
set.accept_all(vec![del.clone()]);
|
||||
let result = set.reduce_onto(&base);
|
||||
|
||||
// The bridging slur cascaded (both its anchors — X and cue-of-X — died).
|
||||
assert!(
|
||||
matches!(
|
||||
result.state.objects.get(&sid),
|
||||
Some(ObjectState::Tombstoned { .. })
|
||||
),
|
||||
"the bridging slur cascades"
|
||||
);
|
||||
// …recorded exactly ONCE as a cascade, never a contradictory
|
||||
// `Reanchored{to: X}` onto the event being deleted.
|
||||
let repairs = match result
|
||||
.state
|
||||
.effects
|
||||
.iter()
|
||||
.find(|(id, _)| *id == del.id)
|
||||
.map(|(_, e)| e)
|
||||
{
|
||||
Some(OperationEffect::AppliedWithRepair { repairs }) => repairs.clone(),
|
||||
other => panic!("expected AppliedWithRepair, got {other:?}"),
|
||||
};
|
||||
let slur_repairs: Vec<_> = repairs.iter().filter(|r| r.target == sid).collect();
|
||||
assert_eq!(
|
||||
slur_repairs.len(),
|
||||
1,
|
||||
"the bridging slur is repaired exactly once: {slur_repairs:?}"
|
||||
);
|
||||
assert!(
|
||||
matches!(slur_repairs[0].kind, RepairKind::CascadeDeleted),
|
||||
"the one record is a cascade, not a contradictory reanchor: {:?}",
|
||||
slur_repairs[0].kind
|
||||
);
|
||||
assert!(check_invariants(&result.score).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rank_four_reanchor_records_same_canvas_nearer_p12_c4() {
|
||||
// Pass 12 (P12-C4): the rank-4 (same-canvas) proximity survivor has
|
||||
|
|
|
|||
|
|
@ -5,9 +5,15 @@ per the house rule (a batch pass opens at ≥3 candidates; this file opened
|
|||
when P13-D1/D2 joined P13-K1). Each entry names the owning DECISIONS record;
|
||||
this file is the index, not the analysis.
|
||||
|
||||
**Batch CLOSED (2026-07-08).** All four candidates are resolved (worked down in
|
||||
order): P13-D3 and P13-K1 by the user's ratified calls ("fix the mint only" /
|
||||
"reject the introduction"), P13-D1 and P13-D2 as correctness fixes with
|
||||
convergence-locked / execute-then-fix regressions. No open Pass-13 candidates
|
||||
remain; a future ≥3-candidate batch reopens the pass.
|
||||
|
||||
| Id | One-line statement | Filed in | Status |
|
||||
|---|---|---|---|
|
||||
| P13-K1 | The K3 verdict for a system pitch introduced by a ModifyEvent replacement value differs across a snapshot cut (in-session `TargetMissing` vs post-snapshot `SystemDerivedContentImmutable`); really "may ModifyEvent introduce never-minted pitch ids" | `crates/epiphany-ops/DECISIONS.md` (Pass-12 G-pass code tranche) | **resolved** (Pass 13: reject the introduction — modify_event refuses a never-minted system-derived pitch, verdict now snapshot-cut-invariant; user "reject the introduction") |
|
||||
| P13-D1 | Undo-driven event tombstones run graph-side re-anchor/cascade but never ledger-side `reanchor_for_tombstone`: structures leave the graph while staying `Live`, no `RepairRecord` — Ch6's same-step recording MUST is unmet for undo-driven tombstones (pre-existing class: slurs/spanners; repeats now too) | `crates/epiphany-ops/DECISIONS.md` (Schema major 2, Phase D) | **resolved** (Pass 13: `tombstone_undo_targets` runs the ledger re-anchor per event target; liveness guard; convergence-locked) |
|
||||
| P13-D2 | Cue-cascade recursion re-anchors against the triggering event before its tombstone lands in `objects`: a structure anchored on {X, cue-of-X} can record `Reanchored{to: X}` then `CascadeDeleted` in one effect (contradictory repair trail; plausible by code trace, unexecuted) | `crates/epiphany-ops/DECISIONS.md` (Schema major 2, Phase D) | open |
|
||||
| P13-D2 | Cue-cascade recursion re-anchors against the triggering event before its tombstone lands in `objects`: a structure anchored on {X, cue-of-X} can record `Reanchored{to: X}` then `CascadeDeleted` in one effect (contradictory repair trail; plausible by code trace, unexecuted) | `crates/epiphany-ops/DECISIONS.md` (Schema major 2, Phase D) | **resolved** (Pass 13: `delete_event` tombstones before the graph delete, matching `cascade_cue`/undo; repro executed then fixed) |
|
||||
| P13-D3 | `CreateCrossCutting` validates only event endpoints (`CrossCuttingValue::endpoints()`), so a SPANNER anchored to a missing region/measure mints dangling past `anchor_target_exists`; and non-event referent tombstones (`DeleteRegion` under a region-anchored spanner/repeat) re-anchor nothing — "every referenced endpoint is live" is events-only as implemented | `crates/epiphany-ops/DECISIONS.md` (Phase D follow-up) | **resolved** (Pass 13: mint fixed via `anchor_object_refs`; non-event referent re-anchoring ratified events-only, user "fix the mint only") |
|
||||
|
|
|
|||
Loading…
Reference in New Issue