Skip to content

fix(engine): auto-reattach Gift of Immortality at next end step - #6808

Merged
matthewevans merged 12 commits into
phase-rs:mainfrom
andriypolanski:fix/4956-gift-of-immortality-reattach
Jul 30, 2026
Merged

fix(engine): auto-reattach Gift of Immortality at next end step#6808
matthewevans merged 12 commits into
phase-rs:mainfrom
andriypolanski:fix/4956-gift-of-immortality-reattach

Conversation

@andriypolanski

@andriypolanski andriypolanski commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #4956

Summary

Discord report: Gift of Immortality returns at the next end step but prompts to choose a creature to attach to, instead of auto-attaching to “that creature.”

Oracle: Enchant creature. When enchanted creature dies, return that card to the battlefield under its owner's control. Return this card to the battlefield attached to that creature at the beginning of the next end step.

Peers covered in the same building block: Next of Kin (attach to the put creature) and Lynde, Cheerful Tormentor (return Curse attached to you).

Root cause

The delayed body was a bare ChangeZone{SelfRef→BF} with no nested Attach and no AbilityDefinition.forward_result, so Aura entry fell through to CR 303.4f host choice (WaitingFor::ReturnAsAuraTarget).

Two follow-on runtime bugs also blocked the happy path once the AST was correct:

  1. Delayed SelfRef currency kept the parent trigger’s battlefield stamp after SBAs moved the Aura to the GY (incarnation bump) — self_ref_is_current failed and the end-step ChangeZone no-oped.
  2. Next of Kin’s “If you do” OptionalEffectPerformed was cloned onto both the CreateDelayedTrigger wrapper and the delayed payload; at end step the creation-time signal was gone, so the return was skipped.

Changes

  • Parser / IRReturnToBattlefield.attach_host; construct + lower emit ChangeZone + nested Attach; assembly nest-before-wrap sets forward_result before delayed wrapping (one CDT, not two). Skip short-name rewrite of temporal "next" (Next of Kin).
  • RuntimeController attach host; ParentTarget walk through sub/else; refresh delayed trigger source zone+incarnation at creation; lift (don’t clone) condition/optional onto the CDT wrapper; keep CDT source_id as the Aura (not the returned host).
  • Testsissue_4956_gift_of_immortality_reattach.rs: shape + Gift/Next/Lynde runtime reattach; Gift stays in GY when host is gone (no Aura choice).

CR references

  • CR 303.4f / CR 303.4g / CR 303.4i — Aura entry attach / no legal host / specified host
  • CR 603.7a / CR 603.7c — delayed trigger creation and creation-time bindings
  • CR 608.2c — “that creature” / ParentTarget anaphora
  • CR 701.3a — attach
  • CR 400.7 — SelfRef / source currency across zones
  • CR 201.5 — “this card” SelfRef on delayed return

Test plan

  • cargo fmt --all
  • cargo test -p engine --test integration issue_4956
    • 7 passed (shape ×3, Gift reattach, Gift host-gone, Next of Kin, Lynde)

Summary by CodeRabbit

  • New Features
    • Added support for returning Auras to the battlefield attached to a specified host, with improved forwarding so the chosen host is honored across nested “return/attach” scenarios.
  • Bug Fixes
    • Fixed delayed Aura reattachment by correcting parent-target snapshot timing and refreshing source zone/incarnation for delayed triggers.
    • Improved parsing for “return … to the battlefield attached to ”, including better remainder handling.
    • Ensured effect-zone prompts publish tracked-set data at the right time.
    • Prevented incorrect “next …” phrase rewriting during card-name normalization.
  • Tests
    • Expanded integration coverage for Gift of Immortality and related Aura reattachment (including Necrotic Plague).

@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The parser preserves explicit Aura attachment hosts for battlefield returns, while engine resolution propagates parent targets and tracked sets through delayed triggers. Integration tests exercise normal trigger processing and verify deterministic Aura and Equipment reattachment.

Changes

Delayed Aura reattachment

Layer / File(s) Summary
Parse and lower attachment hosts
crates/engine/src/parser/oracle_effect/..., crates/engine/src/parser/oracle_ir/ast.rs, crates/engine/src/parser/oracle_util.rs
Return-to-battlefield clauses preserve attachment hosts and continuation text, then lower hosts into nested Attach effects and tracked-set riders.
Assemble delayed attachment effects
crates/engine/src/parser/oracle_effect/assembly.rs
Battlefield changes with Attach sub-abilities forward results and move delayed-trigger fields into the wrapper.
Resolve snapshots and attachment targets
crates/engine/src/game/effects/..., crates/engine/src/game/engine_resolution_choices.rs, crates/engine/src/game/stack.rs, crates/engine/src/game/zone_pipeline.rs
Delayed resolution detects nested ParentTarget references, refreshes source identity, hydrates event-context targets, publishes tracked sets during pauses, and resolves forwarded attachment chains.
Validate delayed reattachment behavior
crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs, crates/engine/tests/integration/main.rs
Integration tests validate oracle shapes, normal trigger processing, target propagation, and reattachment without Aura host prompts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OracleParser
  participant AbilityAssembly
  participant DelayedTrigger
  participant AbilityResolver
  participant ZonePipeline
  OracleParser->>AbilityAssembly: parse battlefield return with attach_host
  AbilityAssembly->>DelayedTrigger: create forwarded delayed ChangeZone and Attach
  DelayedTrigger->>AbilityResolver: snapshot ParentTarget and resolve chain
  AbilityResolver->>ZonePipeline: return Aura and apply attachment
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: matthewevans, claytonlin1110

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several parser/runtime changes and tests generalize behavior for Next of Kin, Lynde, and delayed-trigger plumbing beyond the Gift-only issue scope. If intentional, link the broader follow-up issues; otherwise split unrelated generalizations and non-Gift refactors into separate PRs.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly names the main engine behavior change: auto-reattaching Gift of Immortality at the next end step.
Linked Issues check ✅ Passed The PR adds attach-host handling and tests that Gift of Immortality reattaches automatically instead of prompting, satisfying #4956.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer hold for current head ffd0b154ac0d8b8b5cbe3c953e6650c9758bcc04: this generic parser change has completed implementation review, but its required current-head coverage-parse-diff artifact is still absent. The card-data/coverage job is now complete; approval/enqueue will resume only after the required parse-diff artifact is published and verified for this head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/parser/oracle_effect/mod.rs`:
- Around line 15778-15779: Update the attach-host parsing flow around
parse_search_attach_host so it preserves and threads the parser’s unconsumed
remainder instead of replacing rem with an empty string. Change the parser
result or surrounding logic to distinguish the consumed host rider from trailing
instructions, while still requiring or handling full consumption when
appropriate.

In `@crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs`:
- Around line 447-493: Add a reach-guard assertion immediately after the first
drain_priority in gift_of_immortality_stays_in_graveyard_when_host_gone,
verifying that exactly one delayed trigger was installed before exiling the
host. Keep the existing graveyard and waiting_for assertions unchanged.
🪄 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: 804b67b9-3c37-47c4-b63f-3947105d16f9

📥 Commits

Reviewing files that changed from the base of the PR and between 77e0a75 and ffd0b15.

📒 Files selected for processing (13)
  • crates/engine/src/game/effects/change_zone.rs
  • crates/engine/src/game/effects/delayed_trigger.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/zone_pipeline.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_util.rs
  • crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs
  • crates/engine/tests/integration/main.rs

Comment thread crates/engine/src/parser/oracle_effect/mod.rs Outdated
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 43 card(s), 24 signature(s) (baseline: main a3d6b10c52d1)

🟢 Added (11 signatures)

  • 13 cards · ➕ ability/Attach · added: Attach (target=parent target)
    • Affected (first 3): Dragon Breath, Dragon Fangs, Dragon Scales (+10 more)
  • 3 cards · ➕ ability/Attach · added: Attach (target=you control creature)
    • Affected (first 3): Craving of Yeenoghu, One Last Job, Storm Herald
  • 2 cards · ➕ ability/Attach · added: Attach (target=opponent)
    • Affected (first 3): Accursed Witch, Radiant Grace
  • 2 cards · ➕ ability/Attach · added: Attach (target=self)
    • Affected (first 3): Hakim, Loreweaver, Magnetic Snuffler
  • 1 card · ➕ ability/Attach · added: Attach (target=creature)
    • Affected (first 3): Gryff's Boon
  • 1 card · ➕ ability/Attach · added: Attach (target=enchanted by self creature)
    • Affected (first 3): Mantle of the Ancients
  • 1 card · ➕ ability/Attach · added: Attach (target=last created)
    • Affected (first 3): Forum Filibuster
  • 1 card · ➕ ability/Attach · added: Attach (target=opponent controls creature or opponent controls planeswalker)
    • Affected (first 3): Vengeful Strangler
  • 1 card · ➕ ability/Attach · added: Attach (target=parent target, targets=0+)
    • Affected (first 3): Cass, Hand of Vengeance
  • 1 card · ➕ ability/Attach · added: Attach (target=shares creature type with reference creature)
    • Affected (first 3): Reins of the Vinesteed
  • 1 card · ➕ trigger/ChangesZone · added: ChangesZone (active in=battlefield, from=battlefield, optional=yes, to=graveyard, watches=attached permanent)
    • Affected (first 3): Next of Kin

🔴 Removed (5 signatures)

  • 1 card · ➖ ability/ChangeZone · removed: ChangeZone (conditional=not (previous effect outcome), enters_under=You, target=self, to=battlefield)
    • Affected (first 3): Takklemaggot
  • 1 card · ➖ trigger/ChangesZone · removed: ChangesZone (active in=battlefield, from=battlefield, optional=yes, to=graveyard, watches=attached permanent)
    • Affected (first 3): Next of Kin
  • 1 card · ➖ ability/CreateDelayedTrigger · removed: CreateDelayedTrigger (when=at your next end step)
    • Affected (first 3): Storm Herald
  • 1 card · ➖ ability/activate · removed: activate
    • Affected (first 3): Hakim, Loreweaver
  • 1 card · ➖ ability/~ · removed: ~
    • Affected (first 3): Craving of Yeenoghu

🟡 Modified fields (8 signatures)

  • 9 cards · 🔄 trigger/ChangesZone · changed field active in: graveyardbattlefield
    • Affected (first 3): Genju of the Cedars, Genju of the Falls, Genju of the Fens (+6 more)
  • 3 cards · 🔄 ability/ChangeZone · changed field target: parent targettracked set #0
    • Affected (first 3): Bison Whistle, From Father to Son, The Five Doctors
  • 3 cards · 🔄 ability/CreateDelayedTrigger · changed field tracked: yes
    • Affected (first 3): Rally the Ancestors, Random Encounter, Wake the Dead
  • 1 card · 🔄 ability/Attach · changed field target: triggering sourcecreature
    • Affected (first 3): Cass, Hand of Vengeance
  • 1 card · 🔄 ability/ChangeZone · changed field target: triggering sourcetracked set #0
    • Affected (first 3): Rootweaver Druid
  • 1 card · 🔄 ability/ChangeZoneAll · changed field target: parent targettracked set #0
    • Affected (first 3): The Five Doctors
  • 1 card · 🔄 trigger/LeavesBattlefield · changed field active in: graveyardbattlefield
    • Affected (first 3): Traveling Plague
  • 1 card · 🔄 ability/PutCounter · changed field target: parent targettracked set #0
    • Affected (first 3): Soul Exchange

2 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — current-head parser correctness defects.

🔴 Blocker

  1. crates/engine/src/parser/oracle_effect/mod.rs:15778-15792 parses an attached-return host then replaces rem with "". parse_search_attach_host accepts continuation delimiters, so rules-bearing suffixes are silently discarded. Current parse-diff includes Cass, Hand of Vengeance (…, then attach any number of Equipment …) and shows lost Storm Herald/Takklemaggot signatures. Preserve the unconsumed remainder and let normal continuation parsing lower it; add regression coverage for the continued and lost-semantic forms.

  2. crates/engine/src/game/effects/mod.rs:6491-6508 hydrates only the root ChangeZone.target; its matcher at :6546-6565 never reaches nested Attach { target: ParentTarget }. crates/engine/src/game/effects/change_zone.rs:69-87 consequently resolves ParentTarget only from existing ability targets or the Aura's prior attachment. Event-subject return Auras (the 13-card ParentTarget parse-diff cluster, including Dragon Breath and Smoke Shroud) therefore cannot bind to the creature that entered and can fall back to the ordinary Aura choice at zone_pipeline.rs:2924-2950. Carry/bind the trigger event referent to the nested attach host and add a multiple-legal-host runtime regression that proves no prompt and the correct attachment.

🟡 Non-blocking

crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs:469-492 lacks a reach guard after the first drain_priority; assert the delayed reattach was installed before exiling the host, or the negative assertion can pass without exercising the hostile path.

Recommendation: fix the two parser/runtime blockers, preserve coverage honesty for the full parse-diff class, and strengthen the hostile-path test before re-review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/engine/src/game/effects/mod.rs (1)

10353-10394: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Skip ParentTarget in the attach rebind path
In crates/engine/src/game/effects/mod.rs, the generic Attach branch still appends ability.source_id on the success path. Since effect_object_targets treats ParentTarget as the full fallback target list, that rebased source is the returned Aura itself, so Attach { target: ParentTarget } can resolve against the wrong object. Exempt ParentTarget here the same way LastCreated is exempted.

🤖 Prompt for 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.

In `@crates/engine/src/game/effects/mod.rs` around lines 10353 - 10394, The
generic Attach rebind path must exempt ParentTarget from appending
ability.source_id, matching the existing LastCreated exemption. Update the
Attach handling in the relevant effect resolution logic so ParentTarget
preserves the parent target list and does not rebase to the returned Aura, while
leaving other target types unchanged.

Source: Coding guidelines

🧹 Nitpick comments (1)
crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs (1)

751-792: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Cass and Storm Herald continuation-preservation are only validated at the parsed-AST level, not through the real resolution pipeline.

cass_preserves_equipment_reattach_continuation and storm_herald_preserves_delayed_exile_continuation only assert on the shape of parsed.triggers[0].execute (via ability_chain_contains_equipment_attach / ability_chain_contains_delayed_exile). Neither drives a GameRunner to confirm the equipment reattach or delayed-exile continuation actually executes correctly after the new attach-host lowering (e.g., that the trailing Attach{Equipment} sub-ability actually fires post-assembly, or that the delayed exile CDT installs and fires at end step). smoke_shroud_attaches_to_entering_ninja_among_multiple_hosts shows this is achievable for this PR's card classes — Cass/Storm Herald are left without equivalent runtime coverage even though they exercise the same new attach-host parsing/lowering/assembly code this PR touches.

As per path instructions, "a parser AST shape test does NOT prove runtime semantics or a coverage-support claim" for crates/engine/tests/**.

🤖 Prompt for 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.

In `@crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs`
around lines 751 - 792, Extend cass_preserves_equipment_reattach_continuation
and storm_herald_preserves_delayed_exile_continuation with GameRunner-based
integration scenarios that resolve each card’s ability through the real
pipeline. Assert Cass’s trailing Equipment attach continuation executes after
assembly, and Storm Herald’s delayed exile continuation installs and fires at
end step, using the existing runtime setup pattern from
smoke_shroud_attaches_to_entering_ninja_among_multiple_hosts while retaining the
AST checks.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@crates/engine/src/game/effects/mod.rs`:
- Around line 10353-10394: The generic Attach rebind path must exempt
ParentTarget from appending ability.source_id, matching the existing LastCreated
exemption. Update the Attach handling in the relevant effect resolution logic so
ParentTarget preserves the parent target list and does not rebase to the
returned Aura, while leaving other target types unchanged.

---

Nitpick comments:
In `@crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs`:
- Around line 751-792: Extend cass_preserves_equipment_reattach_continuation and
storm_herald_preserves_delayed_exile_continuation with GameRunner-based
integration scenarios that resolve each card’s ability through the real
pipeline. Assert Cass’s trailing Equipment attach continuation executes after
assembly, and Storm Herald’s delayed exile continuation installs and fires at
end step, using the existing runtime setup pattern from
smoke_shroud_attaches_to_entering_ninja_among_multiple_hosts while retaining the
AST checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ee445e2f-90e7-4195-a67c-f07fa058b1f6

📥 Commits

Reviewing files that changed from the base of the PR and between ffd0b15 and 07b08eb.

📒 Files selected for processing (6)
  • crates/engine/src/game/effects/change_zone.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — current-head attachment regression and failing parser gate.

🔴 Blocker

crates/engine/src/game/effects/mod.rs:6501-6522 always calculates nested_host_ref even when the root ability already has a chosen target. nested_forward_result_attach_parent_target_ref at :6531-6550 then injects the trigger-event referent into the Attach sub; change_zone.rs:37-46 gives that sub target precedence. This overwrites an explicit chosen attachment host. The current ParentTarget parse-diff class includes Necrotic Plague: it must return to the controller's chosen creature, not the creature that died. Hydrate an event referent only when the chain has no bound parent target, and add a runtime regression with distinct dying/chosen creatures.

🟡 Non-blocking

The current Rust lint job is red because sequence.rs:12980 and :12995 use starts_with in parser-gate-covered code. Rewrite those assertions using the approved parser/string convention so the required gate is green.

Recommendation: preserve explicit parent targets, add the discriminating Necrotic Plague test, and clear the parser-gate failure before re-review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/tests/integration/issue_4956_gift_of_immortality_reattach.rs`:
- Around line 892-980: Strengthen the test around the parsed execute chain:
assert that the return clause’s relevant sub-ability, including the
Attach→ParentTarget path found by find_attach_parent, has forward_result
enabled, rather than relying on manually setting return_aura.forward_result.
Drive the parsed ability through the production resolution pipeline instead of
reconstructing return_aura, so a dropped parser-to-resolution flag fails the
test. Replace the vacuous WaitingFor::ReturnAsAuraTarget check with the sibling
tests’ drain_priority flow to observe the actual prompt.
🪄 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: 38034d5b-165a-4d7f-ad8e-28bb3b350633

📥 Commits

Reviewing files that changed from the base of the PR and between 07b08eb and 08a6a14.

📒 Files selected for processing (3)
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/src/parser/oracle_effect/sequence.rs

Comment thread crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs Outdated

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested

[P2] Exercise the parsed Necrotic Plague chain

The new regression test proves only that the parsed tree contains an Attach { target: ParentTarget }; it then constructs a separate ResolvedAbility and manually sets forward_result before resolving it. That leaves both parser-to-runtime contracts changed here untested: the parsed ChangeZone.forward_result wiring and propagation of the resolution-time TargetOnly choice into that ChangeZone. Resolve the parsed trigger through the normal target-selection/priority flow with distinct dying and chosen creatures, then assert it returns attached to the chosen one and never opens ReturnAsAuraTarget (or at minimum structurally assert the exact parsed TargetOnly → forward-result ChangeZone → Attach chain).

@matthewevans matthewevans self-assigned this Jul 30, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — current-head parser gate and broad parse-proof gap

🔴 Blocker

  1. crates/engine/src/parser/oracle_effect/sequence.rs:12980-12982,12995-12997 still uses added strip_prefix parsing dispatch in the parser-combinator-gated surface. The current-head Rust-lint job rejects those exact lines, so the required gate is red. Make the remainder check an exact expected-string assertion or express it through nom tag(...).parse(...).

  2. The current-head coverage-parse-diff affects 27 cards / 17 signatures, including removed Storm Herald delayed-trigger and changed Cass attachment signatures, but crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs:761-800 only inspects parsed ASTs for those continuations. Resolve representative Cass/Storm chains through the production pipeline and reconcile each removed/changed signature against intended semantics; if a clause remains unimplemented, preserve strict failure rather than expanding supported coverage.

✅ Already covered

The existing current-head review’s Necrotic Plague parser-to-runtime test finding remains valid; no need to repost it.

Recommendation: fix the parser gate and provide runtime/coverage evidence for the actual 27-card parser blast radius, then re-review.

@matthewevans matthewevans removed their assignment Jul 30, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — current head still lacks required proof for this parser/runtime change.

🔴 Blocker

  1. crates/engine/src/parser/oracle_effect/imperative.rs:14732-14733 currently fails the required Rust-lint job: Clippy reports field_reassign_with_default for let mut ctx = ParseContext::default(); ctx.subject = …. Initialize ParseContext with the subject field in its struct literal so the mandatory lint gate can proceed.

  2. crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs:1060-1079 (Cass) and :1371-1386 (Necrotic Plague) preselect a root target and then manually stamp it into every nested Attach. That bypasses the parser-to-resolution contract this PR changes; Effect::TargetOnly itself only consumes already selected targets at crates/engine/src/game/effects/mod.rs:4158-4196. A broken forward-result/target-propagation path can therefore pass both tests. Drive the parsed triggers through real trigger target selection and priority resolution with distinct dying and chosen hosts; assert the chosen attachment and that no ReturnAsAuraTarget prompt occurs, without a stamp_host helper.

  3. The required parse-diff sticky is not current-head evidence: comment 5127806035 was last updated at 2026-07-30T12:10:12Z, before current head 18c79f… was created at 14:36Z. Its prior 27-card/17-signature result cannot establish this head’s parser blast radius. Publish the current-head full sticky and reconcile the changed/removed signatures before re-review; preserve strict failure for any clause that remains unresolved.

Recommendation: clear the lint failure, replace the manual-host tests with real selection-to-resolution coverage, and publish/reconcile the current-head parse diff before re-review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
crates/engine/src/game/engine_resolution_choices.rs (1)

6615-6667: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bottom-narrowing logic is now duplicated with finish_effect_zone_put_at_library_position.

Lines 6969-6991 contain the same chain_tracked_set_id → filter-out-chosen → library-zone-filter → insert sequence for LibraryPosition::Bottom. Since this helper is meant to be the single publication authority for EffectZoneChoice selections, have that completion call it instead so the two cannot drift (they already differ on the empty-set behavior).

🤖 Prompt for 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.

In `@crates/engine/src/game/engine_resolution_choices.rs` around lines 6615 -
6667, Remove the duplicated bottom-narrowing and tracked-set insertion logic
from finish_effect_zone_put_at_library_position, and invoke
publish_effect_zone_choice_tracked_set there with the completed choice, effect
kind, and library position. Preserve the helper’s existing empty-set behavior
and make it the sole publication authority for these EffectZoneChoice
selections.
🤖 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/engine_resolution_choices.rs`:
- Around line 6660-6662: Update the empty-result handling around the narrowing
logic near the tracked-set publication so a genuinely empty filtered set still
updates chain_tracked_set_id to the empty result. Preserve the early return only
for the mid-pause guard case, and ensure PutAtLibraryPosition with
LibraryPosition::Bottom cannot retain the previous wider set for chained
TargetFilter::TrackedSet resolution.

In `@crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs`:
- Around line 1375-1386: Remove the unconditional local stamp_host helper and
its invocation at
crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs:1375-1386,
allowing build_resolved_from_def_with_targets and resolve_ability_chain to
propagate the selected target through TargetOnly, ChangeZone, and Attach. Also
remove the Attach-node stamping at
crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs:1064-1079,
relying on the parent ChangeZone result to bind ParentTarget.
- Around line 966-992: Update find_leave_rider to return Some(e) when def.effect
is Effect::Unimplemented, making the existing Some(Effect::Unimplemented { .. })
match arm reachable. Preserve the current AddTargetReplacement and ChangeZone
matching and recursive sub-ability/else-ability traversal.

---

Nitpick comments:
In `@crates/engine/src/game/engine_resolution_choices.rs`:
- Around line 6615-6667: Remove the duplicated bottom-narrowing and tracked-set
insertion logic from finish_effect_zone_put_at_library_position, and invoke
publish_effect_zone_choice_tracked_set there with the completed choice, effect
kind, and library position. Preserve the helper’s existing empty-set behavior
and make it the sole publication authority for these EffectZoneChoice
selections.
🪄 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: 17d83fc4-9993-432b-ba13-0e857a7a9bab

📥 Commits

Reviewing files that changed from the base of the PR and between 08a6a14 and 18c79f0.

📒 Files selected for processing (8)
  • crates/engine/src/game/effects/delayed_trigger.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/engine_resolution_choices.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs

Comment thread crates/engine/src/game/engine_resolution_choices.rs Outdated
Comment thread crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs Outdated
…pipelines

Propagate the chosen Attach host when a forward_result ChangeZone returns
zero cards so Cass Equipment ParentTarget still resolves, refresh SelfRef
at stack resolve after Aura SBAs, keep AttachedTo dies triggers on the
battlefield, and drive Cass/Necrotic through real target selection without
stamp_host. Also clear the Clippy field_reassign_with_default lint.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/engine/src/game/stack.rs (1)

4019-4037: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid cloning the whole ResolvedAbility when the trigger-source refresh doesn't apply.

ability.clone() runs unconditionally on every call, even when trigger_source is None or its identity doesn't match source_id — the common case. Since ResolvedAbility can carry a deep sub_ability/else_ability Effect chain plus targets, this is an avoidable allocation on a per-stack-resolution path.

♻️ Proposed refactor
-    let mut ability = ability.clone();
-    if let Some(mut source_context) = ability.trigger_source.clone() {
-        if source_context.identity.reference.object_id == ability.source_id {
-            if let Some(obj) = state.objects.get(&ability.source_id) {
-                source_context.identity.expected_zone = obj.zone;
-                source_context.identity.reference.incarnation = obj.incarnation;
-                ability.set_trigger_source_recursive(source_context);
-            }
-        }
-    }
-    // Use resolve_ability_chain to support SubAbility/Execute chaining
-    let _ = effects::resolve_ability_chain(state, &ability, events, 0);
+    let needs_refresh = ability.trigger_source.as_ref().is_some_and(|source_context| {
+        source_context.identity.reference.object_id == ability.source_id
+    }) && state.objects.contains_key(&ability.source_id);
+    if needs_refresh {
+        let mut ability = ability.clone();
+        if let Some(mut source_context) = ability.trigger_source.clone() {
+            if let Some(obj) = state.objects.get(&ability.source_id) {
+                source_context.identity.expected_zone = obj.zone;
+                source_context.identity.reference.incarnation = obj.incarnation;
+                ability.set_trigger_source_recursive(source_context);
+            }
+        }
+        // Use resolve_ability_chain to support SubAbility/Execute chaining
+        let _ = effects::resolve_ability_chain(state, &ability, events, 0);
+    } else {
+        let _ = effects::resolve_ability_chain(state, ability, events, 0);
+    }
🤖 Prompt for 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.

In `@crates/engine/src/game/stack.rs` around lines 4019 - 4037, Refactor the
trigger-source refresh in the resolving ability flow to avoid unconditionally
cloning the entire ResolvedAbility. Only clone when trigger_source exists, its
object_id matches source_id, and the corresponding object is present; otherwise
pass the original ability unchanged to resolve_ability_chain. Preserve the
existing zone/incarnation refresh and recursive source update when applicable.
🤖 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/parser/oracle_trigger.rs`:
- Around line 1882-1889: Update the annotation above the AttachedTo dies-trigger
handling to cite CR 603.6e and briefly describe that an Aura trigger for its
enchanted permanent leaving can find the Aura’s new graveyard object after
state-based actions; preserve the existing CR 603.6c, CR 113.6, and CR 704.5m
references and behavior.

---

Nitpick comments:
In `@crates/engine/src/game/stack.rs`:
- Around line 4019-4037: Refactor the trigger-source refresh in the resolving
ability flow to avoid unconditionally cloning the entire ResolvedAbility. Only
clone when trigger_source exists, its object_id matches source_id, and the
corresponding object is present; otherwise pass the original ability unchanged
to resolve_ability_chain. Preserve the existing zone/incarnation refresh and
recursive source update when applicable.
🪄 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: 28761e81-3e41-467e-bda3-30f3516ddd8d

📥 Commits

Reviewing files that changed from the base of the PR and between 18c79f0 and 11e3d8f.

📒 Files selected for processing (6)
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/engine_resolution_choices.rs
  • crates/engine/src/game/stack.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_trigger.rs
  • crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs

Comment on lines +1882 to +1889
// CR 603.6c + CR 113.6 + CR 704.5m: A SelfRef return "from your
// graveyard" in the effect body implies the source is already in that
// zone when the trigger fires — EXCEPT for AttachedTo dies triggers
// (Necrotic Plague: "When enchanted creature dies, … Return this card
// from its owner's graveyard"). Those fire while the Aura is still on
// the battlefield; SBAs move it to the GY before the effect resolves.
// Applying self-recursion here would park the trigger in the GY only,
// so the enchanted-creature death never matches.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cite the Aura-specific rule.

This behavior is specifically covered by CR 603.6e: an Aura trigger for its enchanted permanent leaving can find the Aura’s new graveyard object after SBAs. Add it to this annotation. (media.wizards.com)

As per path instructions, rules-touching code requires a verified CR <number>: <description> annotation.

🤖 Prompt for 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.

In `@crates/engine/src/parser/oracle_trigger.rs` around lines 1882 - 1889, Update
the annotation above the AttachedTo dies-trigger handling to cite CR 603.6e and
briefly describe that an Aura trigger for its enchanted permanent leaving can
find the Aura’s new graveyard object after state-based actions; preserve the
existing CR 603.6c, CR 113.6, and CR 704.5m references and behavior.

Source: Path instructions

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested

[P1] Publish the empty tracked set instead of retaining a stale chain set

crates/engine/src/game/effects/mod.rs:5301-5309 now returns from publish_tracked_set when affected_ids is empty. That contradicts the downstream contract exercised by the existing empty-set chain tests at mod.rs:17431-17493 and engine_resolution_choices.rs:2093-2173: an empty producer must replace the chain set, rather than leaving a previous non-empty set available to a following TargetFilter::TrackedSet. As written, a chained no-op can reuse stale objects (and the empty chain set is never published).

The current CI run 30558607178 has failures in both required Rust test shards, including game::effects::tests::tracked_set_sentinel_does_not_reuse_prior_non_empty_set_when_current_move_is_empty. Restore generic empty-set publication, and isolate the Storm Herald pause handling narrowly rather than changing the shared publication primitive.

Recommendation: fix the generic tracked-set contract and rerun the required CI before re-review.

Reverting the blanket stack SelfRef refresh — rewriting trigger_source to the
post-SBA GY object flipped source_read to ExactLive and broke Persist, Modular,
attachment-count LKI, and related dies triggers. Necrotic Plague instead gains a
narrow self_ref_is_current path for battlefield-latched SelfRef returns from the
graveyard. Completed empty zone moves also publish a fresh empty tracked set
(without mid-pause Storm Herald binding).

Co-authored-by: Cursor <cursoragent@cursor.com>
@matthewevans matthewevans self-assigned this Jul 30, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked — the current head still has two stale-identity/state leaks and a parser blast radius far beyond the stated Gift scope.

🔴 Blocker

  1. crates/engine/src/game/effects/mod.rs:8569-8570 calls publish_tracked_set_with_causes even when affected_with_causes is empty. The shared publisher returns without rebinding on empty input at :5301-5308, while the generic completed-empty path explicitly creates a fresh empty set at :9637-9644. A following tracked-set consumer can therefore observe a prior non-empty set. Mirror the completed-empty rebind here, while preserving the existing resolution-choice pause behavior, and add a regression that proves an empty player-scope completion cannot inherit a prior tracked set.

  2. crates/engine/src/types/ability.rs:23339-23351 allows the post-SBA SelfRef exception solely from the source id, expected battlefield zone, return-from-graveyard shape, and current graveyard zone. It carries no captured incarnation/provenance check. The normal source authority validates the captured incarnation at :23156-23166, and the own-departure successor validates the recorded zone-change identity and no later same-id move at :23214-23264. Capture and validate the relevant incarnation/provenance for this exception as well, with a stale same-id/second-zone-change negative regression.

  3. crates/engine/src/parser/oracle_effect/assembly.rs:1923-1954 is a generic parser assembly change, but the current <!-- coverage-parse-diff --> artifact reports 43 cards across 24 signatures beyond the stated Gift peers: it changes Genju and Traveling Plague and removes behavior from Takklemaggot, Hakim, Loreweaver, and Storm Herald. The selected tests in crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs:832-1356 do not reconcile that card-level surface, and both required Rust test shards are red on this head. Reconcile every gained/lost/changed parse signature (restore unintended deltas or expand the declared, tested class) and make the current-head Rust shards pass before resubmission.

✅ Clean

The current parse-diff artifact is present and gives the necessary card-level evidence for the requested reconciliation.

Recommendation: request changes — fix the empty tracked-set rebind and SelfRef provenance checks, then reconcile the complete parse-diff surface with discriminating regressions and a green current-head Rust run.

@matthewevans matthewevans removed their assignment Jul 30, 2026
Remove the empty early-return from publish_tracked_set so completed no-op
producers still allocate a fresh empty chain set, while mid-chain empty
extends leave prior members intact (Beseech). ParentTarget Attach under
forward_result falls back to the ability source when no explicit host was
chosen (Iron Man / Armored Skyhunter) without overwriting a bound choice.

Co-authored-by: Cursor <cursoragent@cursor.com>

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — the current head has stale-identity/state leaks and lacks current-head parser coverage evidence.

🔴 Blocker

  1. crates/engine/src/types/ability.rs:23332-23351 permits the post-SBA SelfRef exception using only the raw source id, expected battlefield zone, return-from-graveyard shape, and current graveyard zone. It neither validates the captured incarnation nor proves this is the source's own immediate departure successor. The normal guards at :23156-23166 and :23216-23265 carry that identity/provenance authority. A same-id object that has changed zones again can therefore be rebound as the old source. Extend this exception with the captured incarnation and recorded zone-change provenance, and add a negative regression for a stale same-id/second-zone-change case.

  2. crates/engine/src/game/engine_resolution_choices.rs:5450-5468 publishes the tracked set on terminal completion. However, the up_to selection path accepts zero choices at :4455-4469, while publish_effect_zone_choice_tracked_set returns early for an empty set at :6663-6667. The next chained TargetFilter::TrackedSet can then retain a prior non-empty set instead of observing this completed empty result. Publish a fresh empty tracked set on terminal completion; keep any pause-only early return narrowly scoped.

  3. The required <!-- coverage-parse-diff --> comment is not current-head evidence: its latest update is 2026-07-30T18:38:06Z, before current head 51442f4c57e88dfd0d803ae9dc686bdd1cb0569c (PR update 2026-07-30T18:56:49Z). Its 43-card/24-signature result cannot establish this head's parser blast radius. Publish the current-head full artifact and reconcile every gained, removed, and changed signature against the intended, tested scope before re-review.

Recommendation: request changes — restore source-incarnation/provenance safety, publish completed empty tracked sets, then provide and reconcile the current-head parse-diff artifact.

…acked sets

Require a matching BF→GY zone-change identity with no later same-id move for
post-SBA SelfRef graveyard returns, and publish a fresh empty chain set on
terminal EffectZoneChoice completion while keeping mid-pause empty skips.

Co-authored-by: Cursor <cursoragent@cursor.com>
@andriypolanski

Copy link
Copy Markdown
Contributor Author

Addressed — 2787aec0f (CI green; sticky current)

Blocker 1 — SelfRef post-SBA provenance

self_ref_post_sba_graveyard_return now requires a BF→GY ZoneChangeRecord whose captured trigger_source identity matches the ability latch, and rejects any later same-id zone change (mirrors self_ref_own_departure_successor).

Regressions:

  • self_ref_post_sba_gy_return_binds_matching_bf_departure
  • self_ref_post_sba_gy_return_rejects_stale_after_second_zone_change

Blocker 2 — Terminal empty EffectZoneChoice tracked set

publish_effect_zone_choice_tracked_set(..., mid_pause):

  • mid_pause=true (NeedsAura / NeedsChoice): empty still skipped (Storm Herald pause)
  • mid_pause=false (terminal completion): empty rebinds a fresh empty chain set

Regression: terminal_empty_up_to_effect_zone_choice_rebinds_empty_tracked_set

Blocker 3 — Current-head parse-diff + reconciliation

Sticky <!-- coverage-parse-diff --> updated 2026-07-30T20:37:20Z for head 2787aec0f (baseline main a3d6b10c52d1): 43 cards / 24 signatures.

Cluster Verdict
+ Attach (parent target) · Dragon Breath / Smoke Shroud class (13) Intended. Event-subject return Auras bind host via nested Attach { ParentTarget }. Covered by smoke_shroud_* / Dragon Breath pipeline tests.
+ Attach (you control creature) · Storm Herald, Craving, One Last Job Intended. Rem-preserving attach-host parse. Storm Herald pipeline: return → attach → delayed exile TrackedSet.
+ Attach (parent target, 0+) / Aura host triggering sourcecreature · Cass Intended. Chosen Aura host + Equipment ParentTarget. Covered by cass_* production pipeline (no stamp_host).
active in GY→BF · Genju×6, Necrotic Plague, Reins, Traveling Plague Intended. AttachedTo dies / leave latch while Aura still on BF (CR 704.5m / Necrotic class). Necrotic Plague pipeline proves chosen host ≠ dying creature.
Next of Kin trigger add+remove same signature Fingerprint churn around delayed reattach wrap; net AttachedTo dies shape preserved. Covered by next_of_kin_*.
− CreateDelayedTrigger (when=at your next end step) · Storm Herald Not a semantic loss. CDT remains nested after Attach host (uses_tracked_set + exile TrackedSet); sticky fingerprint moved when rem preservation inserted the Attach rider. Covered by storm_herald_exiles_returned_auras_at_end_step_via_pipeline.
**− activate / − ~** · Hakim, Craving Intended cleanup. Leftover Unimplemented tails from discarded rem; activation/Attach hosts now consumed. Attach (self) / Attach (you control) added for those cards.
− ChangeZone (conditional=not previous outcome, …) · Takklemaggot Class of attach-host rem parse. Return+attach now consumes the attached-to host; choose-gate remains Unimplemented (choose a creature…) — strict failure on the choice clause, not a newly supported Takklemaggot.
ParentTarget → TrackedSet #0 · Bison Whistle, From Father to Son, Five Doctors, Rootweaver, Soul Exchange Intentional building-block. Battlefield returns now publish chain tracked sets (is_battlefield_return_effect) so plural/those anaphors (Storm Herald) and singular “that …” follow-ups bind TrackedSet instead of bare ParentTarget.
CDT tracked ∅→yes · Rally / Random Encounter / Wake the Dead Same TrackedSet publish path for delayed “those” follow-ups.
Other Attach adds · Accursed Witch, Radiant Grace, Gryff's Boon, Mantle, Forum Filibuster, Vengeful Strangler, Magnetic Snuffler, Reins Same attach-host class unlocked by rem preservation — not Gift-only, but the general “return … attached to …” building block this PR owns.

Verification (local + CI)

  • issue_4956 ×14, SelfRef provenance ×2, terminal empty tracked set, Beseech, tracked_set_sentinel — pass
  • CI run 30578785148 on 2787aec0f — all required checks green

@matthewevans matthewevans self-assigned this Jul 30, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — the prior identity and parse-diff blockers are addressed, but terminal empty EffectZoneChoice still lacks a discriminating production-pipeline regression.

🔴 Blocker

crates/engine/src/game/engine_resolution_choices.rs:9600-9662 calls the private publish_effect_zone_choice_tracked_set helper directly. That proves the helper can rebind an empty set, but it does not exercise the real WaitingFor::EffectZoneChoice completion path that calls it at :5465-5471. A production-path wiring error can therefore leave a prior non-empty chain_tracked_set_id visible to the next TargetFilter::TrackedSet while this test remains green.

Replace or supplement it with a real terminal zero-choice up_to selection driven through handle_resolution_choice; seed a prior non-empty tracked set and assert the following tracked-set consumer observes the new empty set. Preserve the separate pause-path assertion.

✅ Clean

The current head restores SelfRef provenance checks, and the refreshed parse-diff artifact (2026-07-30T20:37:20Z) reconciles the 43-card / 24-signature surface. Required CI checks for this head are green.

Recommendation: add the production-pipeline zero-choice regression, then request re-review.

@matthewevans matthewevans added the bug Bug fix label Jul 30, 2026
@matthewevans matthewevans removed their assignment Jul 30, 2026
…e path

The empty SelectCards early-return skipped terminal tracked-set publish, so
up_to zero-choice could leave a prior non-empty chain set for TrackedSet
consumers. Rebind before resume and cover it via handle_resolution_choice.

Co-authored-by: Cursor <cursoragent@cursor.com>
@andriypolanski

Copy link
Copy Markdown
Contributor Author

Addressed — production empty EffectZoneChoice path

The empty SelectCards early-return at engine_resolution_choices.rs never reached the terminal publish_effect_zone_choice_tracked_set call. It now rebinds a fresh empty chain set (when a continuation is active) before resume.

Regression terminal_empty_up_to_effect_zone_choice_rebinds_empty_tracked_set now drives WaitingFor::EffectZoneChoice + handle_resolution_choice with a zero-choice up_to selection, seeds a prior non-empty tracked set, asserts the TrackedSet continuation does not grant against the stale members, and keeps the mid-pause empty skip assertion.

Ready for re-review on the tip commit after CI.

@matthewevans matthewevans self-assigned this Jul 30, 2026
@matthewevans

Copy link
Copy Markdown
Member

Maintainer hold for current head 6cbb74d57d2c08f4bcbd6b65933c33608022e36e: the newly added production-path empty-choice regression has completed source review, but required Rust lint, both Rust test shards, and card-data/coverage are still in progress. The required <!-- coverage-parse-diff --> artifact currently reflects prior head 2787aec0f (updated 2026-07-30T20:37:20Z), so it is not evidence for this head. Approval/enqueue will resume after those checks are terminal and a current-head parse-diff artifact is published and verified.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer hold for current head b66baa3121e847a7ae6f0a9feb74292d3cfc54a8: I applied the narrow CR 603.6e annotation fix identified by the independent review. The required current-head checks and <!-- coverage-parse-diff --> artifact are now pending; approval/enqueue will resume after they are terminal and the parse-diff is verified for this head.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved for merge queue on b66baa3: the final delta is the verified CR 603.6e annotation correction; current parse-diff and required checks are reconciled.

@matthewevans
matthewevans added this pull request to the merge queue Jul 30, 2026
@matthewevans matthewevans removed their assignment Jul 30, 2026
Merged via the queue into phase-rs:main with commit e3fc63b Jul 30, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gift of Immortality — At the beginning of the next end step it comeback but it let you choose the creature while it sho…

2 participants