fix(engine): keep prepared-copy spells stack-resident through targeting - #7017
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe change makes token and card-copy lifecycle checks state-aware. Only live pending spell entries preserve stack residency. Regression tests cover cleanup, zone movement, prepared-copy resolution, and countering. ChangesToken and copy residency
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Generated for head Parse changes introduced by this PR✓ No card-parse changes detected. |
matthewevans
left a comment
There was a problem hiding this comment.
[MED] Scope prepared-copy stack residency to the production casting/SBA boundary rather than globally exempting same-ID placeholders. Evidence: crates/engine/src/game/zones.rs:25 now treats any matching StackItem::Spell as residency, while the new regression test manually calls check_state_based_actions during TargetSelection at crates/engine/tests/integration/issue_1312_prepared_spell_cast_triggers.rs:105; normal cast flow reaches state-based actions at the priority boundary (crates/engine/src/game/engine.rs:11051, crates/engine/src/game/engine_priority.rs:180). Why it matters: this global predicate lets an Exile token/copy survive whenever an unrelated same-ID spell placeholder exists, even outside the actual cast lifecycle; the test manufactures that condition before target finalization turns the object into Zone::Stack.
Please remove or rework the global exemption and add a production-path reproduction that advances through the real legal-action/cast flow to the normal SBA boundary. If debug/replay handling needs a special policy, keep it at that explicit API rather than changing the shared zone-residency rule. The revised test should demonstrate both that the prepared copy survives the real targeting path and that the synthetic non-stack case is still cleaned up.
|
Both points addressed in 798fefe:
Verification on head Model: gpt-5.6-sol |
Co-authored-by: nishu-builder <nishu-builder@users.noreply.github.com> # Conflicts: # crates/engine/data/mtgjson-vintage
Co-authored-by: nishu-builder <nishu-builder@users.noreply.github.com>
|
I re-reviewed the current head Holding approval briefly for the current-head CI evidence: Rust lint, Rust tests, and Card data are still running. The existing parse-diff receipt is for stale head |
|
Maintainer hold for current head 7c8fb95. The branch was brought current with main and the implementation review remains clean, but this new head is awaiting its required CI plus a parse-diff receipt bound to this exact SHA. After those are green/current, re-check the live head and proceed with fresh approval and merge-queue enqueue. |
matthewevans
left a comment
There was a problem hiding this comment.
Maintainer review is clean at current head 7c8fb95; required checks and the exact-head parse-diff receipt are green. Approving for merge queue.
…#6826 cost (phase-rs#7053) phase-rs#7044 refreshed the baseline onto the phase-rs#6826 regression itself (layers_full_eval 3495 -> 15877). Once phase-rs#7049 fixed that regression the gate carried ~3x slack -- real cost 5464 against a 15877 baseline under a ~6% band -- so it could no longer catch a new decision-cost regression. Re-measured on a clean worktree against an immutable card-data snapshot (scoped card_data_hash 670a4a14, covering 46/46 scenario deck cards): counter | old | new | threshold layers_full_eval | 15877 | 5464 | 5801 state_clone_for_legality | 19342 | 11099 | 11717 restriction_static_mode_gate_scans | 155747 | 110891 | 116499 sba_battlefield_snapshot_builds | 27462 | 19626 | 20671 scripts/validate-ai-perf-reproducibility.sh: PASSED (margin+band). 25/25 band runs clean, "0 OVER-MARGIN of 29 counters", and worst_current == baseline on every counter across 125 cold processes. CI budget: T_run_max 90s * 2.5 + T_build ~630s = ~14.3 min < 25 min. (The script reported T_build=1s from a warm cache; the 630s figure is the cold isolated server-release build measured separately, which is the conservative ceiling the check intends.) git_sha stamps 7f5f0b4, where the 25-run validation ran. Re-confirmed against f26f4e3 (current main, including phase-rs#7051 and phase-rs#7017): 0 FAIL, 29 PASS, +0 on every counter. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
Summary
Keeps a prepared-copy spell alive while its cast is paused for targeting.
CastPreparedCopycreated the copy object in Exile withis_token = trueand then paused for target selection; a state-based-action pass during the pause removed the token (outside battlefield/stack), and completingChooseTargetemittedSpellCastagainst a destroyed object — leaving a stack entry referencing an absent object. Found by an external deterministic-replay harness (11 distinct seeds; repro: a prepared back-face Regrowth copy targeting a graveyard card), with both actions drawn from the engine's own exact legal-action set. The fix aligns the prepared-copy path with ordinary announcement: the copy is stack-resident (same-idStackEntryKind::Spell) from cast start, so CR 704.5d/e token cleanup correctly ignores it, and it ceases to exist through the normal post-resolution route. The residency check requires the same-id entry to beStackEntryKind::Spellspecifically — same-id non-spell projections (virtual activated-ability overlays) neither shield an off-battlefield token from SBAs nor bypass the movement guards, with a hostile fixture proving both.Files changed
CR references
Implementation method (required)
Method: /engine-implementer
Track
Developer
LLM
Model: gpt-5.6-sol
Thinking: high
Tier: Frontier
Verification
Required checks ran clean, or the exact CI-owned alternative is stated below.
Gate A output below is for the current committed head.
Final review-impl below is clean for the current committed head.
Both anchors cite existing analogous code at the same seam.
tilt get uiresource clippy— Tilt unavailable in this worktree; used the documented direct fallback.cargo fmt --all/cargo fmt --all -- --check— passed.git diff --check— passed.cargo clippy -p phase-engine --all-targets -- -D warnings— passed on headf569c3753289f763d95fc7ea5644bdf8ff7b6a5b.cargo test -p phase-engine— passed on headf569c3753289f763d95fc7ea5644bdf8ff7b6a5b(full suites, 0 failed).Plan verification matrix — passed individually: SBA-pass survival regression (production
CastPreparedCopy→ explicitsba::check_state_based_actionspass mid-targeting →ChooseTargetcompletes → stack object exists → normal resolution), countered-copy sibling (copy ceases correctly), hostile same-id non-spell fixture (virtualActivatedAbilityoverlay does not shield SBA removal nor pass movement guards), raw-movement and zone-pipeline hostile fixtures, three direct-to-stack sibling tests.Revert probe — with the raw-zone-only residency check restored, the survival regression fails; with the fix, passes.
./scripts/gen-card-data.sh— passed on headf569c3753289f763d95fc7ea5644bdf8ff7b6a5b.cargo coverage— passed on headf569c3753289f763d95fc7ea5644bdf8ff7b6a5b: timeless legal 15124/16180 fully supported (93.5%); vintage legal 29841/32268 fully supported (92.5%).cargo semantic-audit— passed on headf569c3753289f763d95fc7ea5644bdf8ff7b6a5b: 32732 cards audited, 297 existing findings.Gate A
Gate A PASS head=f569c3753289f763d95fc7ea5644bdf8ff7b6a5b base=6d7821dced9623609edea342b47dd9c704ff0b36
Anchored on
announce_spell_on_stackbinds the spell object's exact id toStackEntryKind::Spellat announcement; the prepared-copy path now establishes the same residency at the same point in its cast.cast_single_target_during_resolution's direct-to-stack copy casting is the analogous copy-cast seam whose object lifetime the fix mirrors.Final review-impl
Final review-impl PASS head=f569c3753289f763d95fc7ea5644bdf8ff7b6a5b
Claimed parse impact
None.
Validation Failures
Contributor-environment note per the engine-implementer skill: pipeline steps ran as isolated fresh contexts (Codex CLI sessions) with artifact-only handoffs rather than spawned Claude subagents. Plan review: 3 rounds to clean (1 → 1 → 0; round one tightened the residency contract to
StackEntryKind::Spellagainst same-id virtual ability overlays, round two corrected the rules rationale to CR 109.1/602.2a/603.3 after an invalid CR 111.1b citation was caught). Implementation review: 1 round, clean.CI Failures
None.
Related
Same contributor as #6997 (merged), #7007, #7008; independent concerns.
Summary by CodeRabbit