Skip to content

engine: delayed-trigger ParentTarget pin misses 33 tracked-set cards (Eerie Interlude, Ghostway, Yorion) — CR 400.7 / 603.7c #7100

Description

@matthewevans

Follow-up to #7099, which pinned delayed-trigger ParentTarget referents to their
ObjectIncarnationRef so a blinked object is no longer affected by a trigger that
snapshotted its previous incarnation (CR 400.7 / CR 603.7c).

That PR fixed the reported Goryo's Vengeance + Ephemerate interaction and a second
bug (a referent that died was being exiled out of the graveyard). It did not
close the whole class. Three residuals, in priority order.

1. 33 tracked-set cards still have the bug

Measured runtime coverage: 267 of 317 card/CDT pairs (84.2%). The 33-pair gap is
a distinct mechanism, not a missed handler.

bind_tracked_set_to_ability_chain (crates/engine/src/game/effects/delayed_trigger.rs)
recurses into bind_tracked_set_to_effect, whose Effect::ChangeZone arm ends in a
_ => TargetFilter::TrackedSet { id } catch-all and also replaces the variant with
ChangeZoneAll. That runs before the pin gate, so ParentTarget is erased and
the gate never sees an anaphor to pin. Every one of the 33 is a single-node chain
whose entire target field is replaced wholesale — none has a nested or sibling node
that could survive.

Affected cards include Eerie Interlude, Ghostway, Yorion Sky Nomad, Venser the
Sojourner, Touch the Spirit Realm
.

Measurement, from the #7099 run:

uses_tracked_set: true 46 of 317 pairs
already inside the 17-pair exclusion 13
inside "retained", so genuinely lost 33
of those 33, surviving the binder 0

Two honesty caveats on that number. It was computed by simulating the binder over
the AST, not by observing the engine. And the loss is conditional on a tracked set
being live: tracked_set_id resolves via chain_tracked_set_id or the game-wide
latest_tracked_set_id fallback, so an unrelated earlier tracked set can also
trigger the rewrite. Treat 267 as a lower bound on coverage and 33 as an upper
bound on the loss
.

The naive fix — hoisting the pin gate above the binder — was rejected during review:
it stamps pins on chains that then resolve by live scan, so pinned_object_targets_all_stale
can go true and suppress a working resolution. A real fix has to make the binder
preserve the anaphor rather than move the gate.

2. ParentTargetSlot slot-index validation (latent)

Raised by CodeRabbit on #7099. When slot 0 is stale but another pinned target is still
current, the all-stale guard does not fire, and a raw slot-indexed read can resolve
slot 0 to the re-entered object with the same ObjectId — which is exactly the CR 603.7c
violation the PR set out to fix.

Currently latent: the measured population of slot-indexed ParentTarget consumers is
zero,
so no shipping card reaches it. It becomes live the moment one does. The
structural fix is per-slot incarnation validation instead of the all-or-nothing guard.

3. Test residual: flip_permanent.rs / transform_effect.rs

Also from CodeRabbit. Both handlers took early-return-only treatment rather than
target substitution, because substituting on an emptied list would fall through to
[] => ability.source_id and make the effect target its own source. The behavior
change (a declared non-SelfRef stale target now resolves as a no-op instead of
flipping the re-entered object) has no dedicated test; it rests on full-suite
regression only.

The same "regression-only" caveat applies to the six Tier C handlers in #7099
(discard.rs, effect.rs, phase_out.rs, deal_damage.rs, tap_untap.rs,
cast_from_zone.rs). cast_from_zone.rs was flagged highest-risk of that set.

Not in scope here

Two pre-existing clusters were deliberately left alone in #7099 so each lands as one
auditable pass, and are not tracked by this issue:

  • A parser misparse: "sacrifice this creature" parses to ParentTarget where it
    should be SelfRef.
  • A CR mis-citation cluster: sacrifice.rs cites CR 701.17a for sacrifice in several
    places; 701.17a is mill, and Sacrifice is CR 701.21.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions