🤖 AI text below 🤖
A parked CR 616.1 replacement choice is destroyed by the spell's own CR 608.2n graveyard move
pending_replacement is a single slot. When a spell's effect parks a CR 616.1 replacement choice mid-resolution, the spell's own "final part of resolution" move to its owner's graveyard is proposed while that choice is still parked. If that move also raises an optional replacement, it overwrites the parked choice — and the card the effect had already selected is silently dropped. It never leaves the hand, and nothing records it.
This is a data-loss defect. It is pre-existing and independent of the paused-discard-batch repair (PR #7494); it was found while instrumenting a fixture for that work.
Measured signature
Instrumenting a random-discard fixture to dump pending_replacement.proposed, the discard batch, and per-card zones at every prompt. At prompt #0:
pending = ZoneChange { object_id: ObjectId(2), from: Stack, to: Graveyard, cause: Some(ObjectId(2)), … } <- the spell itself
batch = (PlayerId(1), paused_card ObjectId(6), Random { pool: [3,4,5], remaining: 1 })
zones = [(3,Hand),(4,Hand),(5,Hand),(6,Hand)]
end = spell_zone=Graveyard p1_gy=1 p1_hand=3 zones=[(3,Hand),(4,Graveyard),(5,Hand),(6,Hand)]
ObjectId(6) — the victim the effect actually selected first — never left the hand, for the rest of the game. ObjectId(4), selected after the resume, did.
Rules frame
CR 608.2n: "As the final part of an instant or sorcery spell's resolution, the spell is put into its owner's graveyard." That move is proposed while a CR 616.1 choice for a different object is already parked. CR 616.1 gives the affected player the choice among applicable replacement effects; the engine can hold only one such pending choice at a time.
Independence from PR #7494 — the load-bearing claim
The overwrite is already in place at prompt #0, before any resume code from that PR runs. Its drain executes only on a ChooseReplacement action, and prompt #0 is the first prompt. The only code from that PR on the path to this dump is the park, which writes the batch field and never touches pending_replacement. When the drain did run, it behaved correctly — it made the second pick, and that card reached the graveyard.
Smallest known repro
Two-player GameScenario. Give P1 a permanent hosting ReplacementDefinition::new(ReplacementEvent::Moved).destination_zone(Zone::Graveyard).mode(ReplacementMode::Optional { decline: None }) with valid_card: None. Give P0 a sorcery reading "Target player discards two cards at random." and P1 four cards in hand. Cast it targeting P1 and decline every prompt. Two prompts are raised; only one card leaves P1's hand.
Why PR #7494's own fixtures do not hit it
Its Windfall arm gates on ReplacementEvent::Discard (Library of Leng class). A spell going to its owner's graveyard is not a discard, so the spell's own move raises no choice there and there is nothing to clobber. The random-discard fixture that exposed this has since been narrowed with Not { SpecificObject { id: … } }, with the measurement recorded in its doc comment so a future reader sees why the narrowing exists.
Suggested repair shape
The parked-choice slot needs to either queue rather than overwrite, or refuse a second park while one is outstanding and defer the spell's own move until the first choice resolves. Note that the sacrifice family already solves an adjacent ordering problem with per-batch completion bookkeeping (PendingPlayerScopeSacrificeCompletion), which documents surviving "every replacement-choice pause" — that is the nearest precedent to trace before designing.
🤖 AI text below 🤖
A parked CR 616.1 replacement choice is destroyed by the spell's own CR 608.2n graveyard move
pending_replacementis a single slot. When a spell's effect parks a CR 616.1 replacement choice mid-resolution, the spell's own "final part of resolution" move to its owner's graveyard is proposed while that choice is still parked. If that move also raises an optional replacement, it overwrites the parked choice — and the card the effect had already selected is silently dropped. It never leaves the hand, and nothing records it.This is a data-loss defect. It is pre-existing and independent of the paused-discard-batch repair (PR #7494); it was found while instrumenting a fixture for that work.
Measured signature
Instrumenting a random-discard fixture to dump
pending_replacement.proposed, the discard batch, and per-card zones at every prompt. At prompt #0:ObjectId(6)— the victim the effect actually selected first — never left the hand, for the rest of the game.ObjectId(4), selected after the resume, did.Rules frame
CR 608.2n: "As the final part of an instant or sorcery spell's resolution, the spell is put into its owner's graveyard." That move is proposed while a CR 616.1 choice for a different object is already parked. CR 616.1 gives the affected player the choice among applicable replacement effects; the engine can hold only one such pending choice at a time.
Independence from PR #7494 — the load-bearing claim
The overwrite is already in place at prompt #0, before any resume code from that PR runs. Its drain executes only on a
ChooseReplacementaction, and prompt #0 is the first prompt. The only code from that PR on the path to this dump is the park, which writes the batch field and never touchespending_replacement. When the drain did run, it behaved correctly — it made the second pick, and that card reached the graveyard.Smallest known repro
Two-player
GameScenario. Give P1 a permanent hostingReplacementDefinition::new(ReplacementEvent::Moved).destination_zone(Zone::Graveyard).mode(ReplacementMode::Optional { decline: None })withvalid_card: None. Give P0 a sorcery reading "Target player discards two cards at random." and P1 four cards in hand. Cast it targeting P1 and decline every prompt. Two prompts are raised; only one card leaves P1's hand.Why PR #7494's own fixtures do not hit it
Its Windfall arm gates on
ReplacementEvent::Discard(Library of Leng class). A spell going to its owner's graveyard is not a discard, so the spell's own move raises no choice there and there is nothing to clobber. The random-discard fixture that exposed this has since been narrowed withNot { SpecificObject { id: … } }, with the measurement recorded in its doc comment so a future reader sees why the narrowing exists.Suggested repair shape
The parked-choice slot needs to either queue rather than overwrite, or refuse a second park while one is outstanding and defer the spell's own move until the first choice resolves. Note that the sacrifice family already solves an adjacent ordering problem with per-batch completion bookkeeping (
PendingPlayerScopeSacrificeCompletion), which documents surviving "every replacement-choice pause" — that is the nearest precedent to trace before designing.