fix(engine): preserve cast-payment stamps for CR 603.4 re-checks; never leak them across identities (Emptiness evoke riders) - #6352
Conversation
…er leak them across identities (Emptiness evoke riders)
The per-color payment tally (colors_spent_to_cast) was zeroed for every
object by clear_post_collection_transients after each trigger batch, so
ManaColorSpent intervening-ifs ("if {W}{W} was spent to cast it" — the
Emptiness/Incarnation cycle, Adamant, the Shadowmoor spent-color family)
silently failed their CR 603.4 resolution re-check whenever the source
was still ExactLive. Preserve the tally on Battlefield|Stack under the
same guard that already protects cast_from_zone.
Preservation alone would leak payment provenance across object
identities, so the cast-payment stamps now share one complete lifecycle
via a single authority, GameObject::clear_cast_payment_stamps: cleared
for objects outside Battlefield|Stack at the next collection pass
(countered/fizzled spells lose their record, mirroring cast_from_zone),
cleared at battlefield exit (CR 400.7 — a re-entering permanent is a new
object; also fixes the live Satoru-class leak where
mana_spent_to_cast_amount survived blink AND counter-then-reanimate),
and reset at every spell-copy birth site (CR 707.10: a copy is not cast
and mana is not an object; CR 707.12 for cast copies). The
allow-raw-zone spell-copy-birth markers enumerate all four sites
(copy_spell, epic, cast_copy_of_card, paradigm); prepare.rs is out of
class (its copy is later cast and re-stamped).
Closes phase-rs#5943
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 34 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughCast-payment tracking is centralized in ChangesCast-payment stamp lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CastSpell
participant GameObject
participant TriggerCollection
participant TriggerResolver
participant EffectResolution
CastSpell->>GameObject: record cast-payment stamps
TriggerCollection->>GameObject: preserve Battlefield/Stack stamps
TriggerResolver->>GameObject: read live or latched payment provenance
EffectResolution->>GameObject: evaluate ManaColorSpent
EffectResolution->>GameObject: clear stamps on copies and exited objects
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
matthewevans
left a comment
There was a problem hiding this comment.
[MED] Battlefield exit clears source-qualified payment provenance needed by a pending trigger. Evidence: game_object.rs line 2340 now calls clear_cast_payment_stamps(), which clears mana_spent_source_snapshots; snapshot_for_zone_change copies only bool/color/total fields into TriggerSourceContext at game_object.rs lines 1839-1841; and QuantityRef::ManaSpentToCast with FromSource reads only the live object vector in game/quantity.rs lines 1555-1572. Why it matters: if a permanent with an ETB rider that asks how much mana from a Treasure/Cave/artifact source was spent to cast it leaves before its intervening-if/effect resolves, the exact source context is latched but has no source-payment vector, and the newly cleared object reads as zero. CR 400.7d expressly permits a permanent ability to reference mana spent to cast the spell that became it. This avoids stale reanimation identity only by losing legitimate resolving-trigger provenance. Suggested fix: preserve the source-payment snapshots in the exact trigger-source/latch representation (and route FromSource resolution through that representation), while continuing to clear the new incarnation at the zone-change boundary; add a runtime fixture that sacrifices/blinks the ETB source before the source-qualified rider resolves.
Maintainer hold: this review is changes-requested. Current-head CI still has Rust lint/tests/card-data pending; CodeRabbit is still processing and has posted no findings; the required coverage-parse-diff artifact is absent, so the engine-only no-parse-change claim cannot yet be admitted. Security scan was clean. I also found open #6340 for the same issue/root cause; these alternatives must not both be enqueued.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/engine/src/game/game_object.rs`:
- Around line 2401-2406: Reset phyrexian_life_paid in
GameObject::clear_cast_payment_stamps and extend its default-state test. In
crates/engine/src/game/effects/cast_copy_of_card.rs:194-199,
copy_spell.rs:83-86, epic.rs:138-141, and paradigm.rs:196-198, extend each
regression test to verify copied objects or spells clear inherited
Phyrexian-life payment, preserving Comprehensive Rules behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: db98c503-b03e-485e-90d3-63e8f7efca41
📒 Files selected for processing (9)
crates/engine/src/game/effects/cast_copy_of_card.rscrates/engine/src/game/effects/copy_spell.rscrates/engine/src/game/effects/epic.rscrates/engine/src/game/effects/mod.rscrates/engine/src/game/effects/paradigm.rscrates/engine/src/game/game_object.rscrates/engine/src/game/triggers.rscrates/engine/tests/integration/emptiness_mana_spent_evoke_5943.rscrates/engine/tests/integration/main.rs
Parse changes introduced by this PR✓ No card-parse changes detected. |
…re-checks (review response)
Per review: battlefield exit correctly clears the new incarnation's
cast-payment stamps (CR 400.7), but a permanent whose ETB rider reads a
source-qualified spent amount (Marut: 'a Treasure token for each mana
from a Treasure spent to cast it') could leave before that rider
resolves. TriggerSourceContext latched bool/color/total but not
mana_spent_source_snapshots, and QuantityRef::ManaSpentToCast read the
live object only, so a blinked/sacrificed source read zero — losing
provenance CR 400.7d expressly permits the ability to reference.
Adds mana_spent_source_snapshots to the trigger-source latch (stamped in
snapshot_for_zone_change beside the other three) and routes
ManaSpentToCast{Total,DistinctColors,FromSource} through source_read at
the same authority as the existing ManaColorSpent/ManaSpentCondition
re-checks, gated on the trigger source's object id == the read's cast
object so listener/target scopes stay live. The new incarnation is still
cleared at the zone boundary. Regression: a source-qualified ETB rider
whose source is blinked before it resolves reads the latch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Fixed on head The fix. Regression. Verification on Model: claude-opus-4-8 |
Reset the full cast-payment lifecycle in the shared helper and cover every stack-copy birth.\n\nAdds a production stack-resolution regression for a Compleated planeswalker copy.
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — the current head loses triggering-spell payment provenance after a countered spell changes zones.
🔴 Blocker
[HIGH] Preserve payment facts for distinct sources that trigger on a spell. clear_post_collection_transients now clears mana_spent_to_cast_amount and the source snapshots for every object outside the battlefield or stack (crates/engine/src/game/triggers.rs:4652-4664). But TriggeringSpell quantity resolution reads the event object's live record without a latch (crates/engine/src/game/quantity.rs:1562-1576); the new latch only applies when the trigger source and cast object are identical (:1602-1622). Countering a spell before an Aetherflux Conduit-style trigger resolves therefore changes the recorded amount to zero. origin/main deliberately preserved this amount in the same cleanup path.
Why it matters: the trigger refers to the specific spell cast, so its amount must survive that spell leaving its expected zone. CR 608.2h requires use of last-known information when the specified object is no longer in the expected public zone.
Suggested fix: snapshot the triggering spell's payment facts in the trigger event/pending context and route TriggeringSpell through that event authority (or retain those facts until all event consumers resolve). Add a runtime Aetherflux Conduit regression that counters the spell before its trigger resolves.
✅ Clean
- The source-qualified latch and the copied-spell cleanup address the earlier scoped feedback; this remaining issue is the distinct-source event path.
Recommendation: request changes; retain the latch and repair event-scoped triggering-spell provenance.
matthewevans
left a comment
There was a problem hiding this comment.
Approved after the current-head review.
- Payment provenance remains source-qualified and is cleared for every spell-copy birth path.
- The Compleated regression covers the runtime resolution path; the copy enters with five loyalty rather than inheriting paid-life history.
- The merge refresh preserves the reviewed 12-file PR diff; the current CodeRabbit status has no actionable finding.
matthewevans
left a comment
There was a problem hiding this comment.
Approved after the current-head fixture correction.
- The copy-payment reset path is unchanged and remains covered by all five stamp assertions.
- The runtime assertion now uses the established untargeted permanent-spell fixture, so it reaches stack resolution and battlefield entry instead of correctly fizzling an untargeted damage spell.
…er leak them across identities (Emptiness evoke riders) (phase-rs#6352) * fix(engine): preserve cast-payment stamps for CR 603.4 re-checks; never leak them across identities (Emptiness evoke riders) The per-color payment tally (colors_spent_to_cast) was zeroed for every object by clear_post_collection_transients after each trigger batch, so ManaColorSpent intervening-ifs ("if {W}{W} was spent to cast it" — the Emptiness/Incarnation cycle, Adamant, the Shadowmoor spent-color family) silently failed their CR 603.4 resolution re-check whenever the source was still ExactLive. Preserve the tally on Battlefield|Stack under the same guard that already protects cast_from_zone. Preservation alone would leak payment provenance across object identities, so the cast-payment stamps now share one complete lifecycle via a single authority, GameObject::clear_cast_payment_stamps: cleared for objects outside Battlefield|Stack at the next collection pass (countered/fizzled spells lose their record, mirroring cast_from_zone), cleared at battlefield exit (CR 400.7 — a re-entering permanent is a new object; also fixes the live Satoru-class leak where mana_spent_to_cast_amount survived blink AND counter-then-reanimate), and reset at every spell-copy birth site (CR 707.10: a copy is not cast and mana is not an object; CR 707.12 for cast copies). The allow-raw-zone spell-copy-birth markers enumerate all four sites (copy_spell, epic, cast_copy_of_card, paradigm); prepare.rs is out of class (its copy is later cast and re-stamped). Closes phase-rs#5943 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(engine): latch source-qualified payment provenance for CR 400.7d re-checks (review response) Per review: battlefield exit correctly clears the new incarnation's cast-payment stamps (CR 400.7), but a permanent whose ETB rider reads a source-qualified spent amount (Marut: 'a Treasure token for each mana from a Treasure spent to cast it') could leave before that rider resolves. TriggerSourceContext latched bool/color/total but not mana_spent_source_snapshots, and QuantityRef::ManaSpentToCast read the live object only, so a blinked/sacrificed source read zero — losing provenance CR 400.7d expressly permits the ability to reference. Adds mana_spent_source_snapshots to the trigger-source latch (stamped in snapshot_for_zone_change beside the other three) and routes ManaSpentToCast{Total,DistinctColors,FromSource} through source_read at the same authority as the existing ManaColorSpent/ManaSpentCondition re-checks, gated on the trigger source's object id == the read's cast object so listener/target scopes stay live. The new incarnation is still cleared at the zone boundary. Regression: a source-qualified ETB rider whose source is blinked before it resolves reads the latch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(PR-6352): clear phyrexian payment on spell copies Reset the full cast-payment lifecycle in the shared helper and cover every stack-copy birth.\n\nAdds a production stack-resolution regression for a Compleated planeswalker copy. * test(PR-6352): make copy reset fixture a permanent spell --------- Co-authored-by: michiot05 <281539540+michiot05@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
Tier: Frontier
Model: claude-fable-5
Thinking: high
Closes #5943.
Summary
Emptiness (ECL,
{4}{W/B}{W/B}, Evoke{W/B}{W/B}) — "When this creature enters, if {W}{W} was spent to cast it, …" / "…if {B}{B} was spent to cast it, …". Discord report: evoking with two white or two black mana never fires the matching rider.Root cause (runtime, not parser — the AST is faithful):
colors_spent_to_castis stamped correctly at payment finalization (including hybrid halves per CR 107.4e and the evoke alternative-cost path per CR 702.74a), butclear_post_collection_transientszeroed it on every object in every zone after each non-empty trigger batch. The CR 603.4 intervening-if re-check at resolution reads the live object while the source is stillExactLive— by then the tally is gone, soManaColorSpentconditions silently failed. This kills the whole class: the Incarnation hybrid-ETB cycle, all Adamant cards (CR 207.2c), the Shadowmoor "if {C} was spent" family, and converge-style stack reads across intermediate batches.The fix preserves the tally on
Battlefield | Stackunder the same guard that already protectscast_from_zone— the function's own documented treatment for cast facts that CR 603.4 re-checks. But preservation alone would leak payment provenance across object identities, so this PR gives all four cast-payment stamps (mana_spent_to_cast,colors_spent_to_cast,mana_spent_to_cast_amount,mana_spent_source_snapshots) one complete lifecycle through a new single authority,GameObject::clear_cast_payment_stamps():cast_from_zone).mana_spent_to_cast_amountsurvived blink/reanimation, so Satoru-class "no mana was spent to cast it" read stale values.allow-raw-zone: …spell-copy birthmarkers enumerate all four stack-copy birth sites —copy_spell.rs,epic.rs,cast_copy_of_card.rs,paradigm.rs— and each now calls the shared authority;prepare.rs's exile-copy is out of class (later cast for real, re-stamped at payment). Without this, preserving the tally would have made every Fork/Twincast/Epic/paradigm copy wrongly satisfy "was spent" conditions the moment the original paid matching mana.Relationship to #6340
#6340 (opened while this fix was in its mandated review pipeline) identifies the same root cause and takes the approach of never clearing
colors_spent_to_castat all, aligning it withmana_spent_to_cast_amount's current always-durable treatment. Respectfully submitting this PR for comparison because the unconditioned-durability approach inherits the exact identity leaks this PR closes and tests:copy_of_emptiness_fires_neitherintegration row plus per-site resolver tests at all four copy-birth sites.blink_reanimate_clears_cast_payment_stampsandcountered_spell_clears_cast_payment_stamps_before_reanimation, both revert-proven (left: 2, right: 0).mana_spent_to_cast_amountprecedent it aligns to is itself the source of the pre-existing Satoru-class blink leak that this PR fixes.Implementation method (required)
Method: /engine-implementer — plan → /review-engine-plan (6 rounds to certification; the loop successively surfaced: the copy-inheritance regression, the epic/cast_copy_of_card sites, the paradigm site via the marker-sweep completeness proof, and a consolidation pass) → implement → /review-impl (clean; Maintainer-Simulation Gate PASS) → final read-only /review-impl on the committed head (returned one finding — the countered-spell path — fixed with the prescribed lifecycle alignment + regression, delta re-certified). Each step in a fresh agent context. Final head: 441c22b.
Gate A
./scripts/check-parser-combinators.sh→ Gate A PASS head=441c22bfc333874df8b1e06c048c51b11910f5b9 base=094d4e5630de0659ee3d62b959cb851b12c3e632 (Gate G PASS). No files undercrates/engine/src/parser/are touched.Anchored on
crates/engine/src/game/triggers.rsclear_post_collection_transients— the existing!matches!(obj.zone, Battlefield | Stack)guard preservingcast_from_zonefor exactly this CR 603.4 re-check reason; the fix extends the same guard to the payment tally.crates/engine/src/game/game_object.rs:2288reset_for_battlefield_exit— the established CR 400.7 exit authority (cast_from_zone = Noneprecedent) that now delegates stamp clearing to the shared helper.crates/engine/src/game/effects/copy_spell.rscopy-normalization block (CR 707.10, issue Sevinne's Reclamation — Sevinne's Reclamation copies itself when a copy of a flashback Sevinne's Reclamation resolves. #3283cast_from_zoneprecedent) — the copy-birth reset idiom now applied uniformly via the single authority at all four marker-enumerated sites.Verification
cargo fmt --allclean;cargo clippy -p engine --tests0 warnings (final head).emptiness_mana_spent_evoke_5943.rs, registered inmain.rs) — 11 passed, all through the real cast pipeline with verbatim Oracle text: evoke{W}{W}with explicit trigger ordering (ETB-first, mid-drive reach guard proving the source is still on the battlefield at the re-check — the ExactLive path the bug killed), sac-first sibling (latched-snapshot path), evoke{B}{B}, hostile mixed{W}+{B}(neither fires; entered+sacrificed reach guards), hard cast, hostile intermediate-batch row, hostile copy row, blink row, counter-then-reanimate row, convoke boundary pin (CR 702.51a), and a Dawnglow-class resolution-sideAbilityConditionrow.cast_from_zonepin, helper contract pin, and per-site copy-birth resets incopy_spell.rs,epic.rs(file's first test module),cast_copy_of_card.rs,paradigm.rs— each revert-fails its own site.colors_spent_to_cast_persists_through_zone_change_replacement, fullmana_spentfilter (61), all copy/paradigm/epic/cast-copy suites.left: 2, right: 0. All green on the final head.docs/MagicCompRules.txtbefore writing — CR 601.2h (:2472), 603.4 (:2592), 603.3b (:2586), 107.4e (:504), 400.7 (:1950), 702.74a (:4556), 707.10 (:5666), 707.10f (:5682), 707.12 (:5689), 608.3f (:2836), 702.50a (:4391), 702.51a (:4397), 207.2c (:1497). Zero UNVERIFIED.Claimed parse impact
None. Engine-only: no parser files, no serialized AST shapes, no coverage change (Emptiness's parse was already faithful; its unrelated
has_gapsfacet is untouched). The parse-diff artifact should report no card-parse changes — consistent with a pure runtime-lifecycle fix. (A local regen diff against the pre-change baseline showed only upstream MTGJSON corpus drift — 13 new cards + set-code additions — with zero entries touching any payment-related field.)Scope Expansion
None. Deliberately excluded (noted as optional follow-ups): deduplicating
casting.rs's two payment-finalization clear blocks through the new helper; the adjacentreset_for_battlefield_entryhygiene gap documented inl02_bb6.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests