ship/t73 purged source lki#5779
Merged
Merged
Conversation
matthewevans
commented
Jul 14, 2026
Member
- fix(engine): resolve a PURGED source's controller from LKI (CR 608.2h)
- test(engine): make the exploit LKI guard discriminating (ceased-to-exist self-exploiter)
- test(engine): strengthen the purged-source false-condition control
`FilterContext::from_source` derived the context's `source_controller` from
live state only:
state.objects.get(&source_id).map(|o| o.controller)
A token that leaves the battlefield ceases to exist (CR 111.7 / CR 704.5d) and
is purged from `state.objects` outright, so this answered `None` and every
`ControllerRef::You` predicate built on the context became unanswerable and
failed closed.
The live vector is the CR 603.4 intervening-if re-check at RESOLUTION. Trigger
collection enumerates sources out of `state.objects`, so the trigger is
collected while the token still exists, goes on the stack, and the CR 111.7 SBA
then purges the source. `check_trigger_condition` re-checks the condition via
`FilterContext::from_source` (`ControlsType`, `ControlsNone`,
`ZoneChangeObjectMatchesFilter`, ...), reads `source_controller: None`, decides
the condition is FALSE, and silently removes the ability from the stack. A
token copy of Delta Bloodflies that dies with its own attack trigger on the
stack never drains the opponent.
CR 608.2h is explicit: "If the effect requires information from a specific
object, INCLUDING THE SOURCE OF THE ABILITY ITSELF, the effect uses the current
information of that object if it's in the public zone it was expected to be in;
if it's no longer in that zone ... the effect uses the object's last known
information." CR 113.7a: the ability on the stack exists independently of its
source, so the source's removal must not make "you" unanswerable.
`state.lki_cache` already holds the at-exit controller (captured by
`apply_zone_exit_cleanup`). Route both source-deriving constructors through one
`source_controller_or_lki` authority: live state wins, LKI answers only when the
object is gone — a strict no-op for every source that still exists. This mirrors
the live-then-LKI fallback `ability_utils::parent_target_controller` /
`parent_target_owner` already use for CR 608.2h.
Population (computed, not estimated): 153 faces in the card pool carry a
`from_source`-routed intervening-if whose filter reads `source_controller`
(127 ControlsType + 28 ZoneChangeObjectMatchesFilter + 23 ControlsNone,
deduped); 131 of them are creatures, whose source can be a token via copy /
embalm / eternalize.
Witness (`purged_source_intervening_if_lki`) drives the real pipeline: the card
is synthesized from verbatim Oracle text, the token is killed through the real
zone-change pipeline and purged by the real SBA, and the trigger resolves off
the real stack. Watched RED (P1 at 20, expected 19) before this change and GREEN
after. Two controls hold in both directions: a NONTOKEN source that dies the same
way is unaffected (it never leaves `state.objects`), and a purged token whose
condition is genuinely false stays false — the fallback restores the ability to
ANSWER the question, it does not fabricate a match.
…ist self-exploiter)
`exploited_typed_filter_matches_self_sacrificed_exploiter_via_lki` was a VACUOUS
guard: a printed creature card keeps its `core_types` and `controller` across a
zone change and `filter_inner` has no zone gate, so the graveyard object still
satisfied `Typed { Creature, controller: You }` on the live path. It passed with
the LKI fallback compiled out.
The vector that discriminates is a ceased-to-exist token (CR 111.7), which is
purged from `state.objects` and which `filter_inner` cannot see at all. m35b
could not write that test for exploit, because a token that exploits ITSELF is
also its own trigger SOURCE, and `FilterContext::from_source` could not resolve a
purged source — the subject resolved via LKI but the CONTEXT did not.
With the CR 608.2h source fallback in place, add the missing sibling:
`exploited_typed_filter_matches_ceased_to_exist_token_self_exploiter_via_lki`
drives the real zone-change pipeline, purges the token per CR 111.7, and asserts
the self-exploiting token still matches "a creature you control" — with a negative
control that an opponent-controlled subject filter still does NOT match, so the
fallback cannot be passing by fabricating a match.
Watched RED against a reverted `from_source` and GREEN with the fix. The stale
doc comment on the printed-card sibling, which recorded the gap as open and
untracked, is corrected to point at the new test.
The `CounterBearer::Absent` arm left the controller with NO creatures at all once the token source was purged, so "no life lost" could have meant "the battlefield was empty" rather than "the counter predicate was evaluated and was false". A control that cannot tell those two apart is not a control. Give BOTH arms a second creature and vary ONLY the counter. The false-condition control now hands the filter a live candidate to reject, so its green means the predicate ran and returned false — which is what makes the primary witness's green non-vacuous.
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
matthewevans
enabled auto-merge
July 14, 2026 03:06
Parse changes introduced by this PR✓ No card-parse changes detected. |
github-merge-queue Bot
pushed a commit
that referenced
this pull request
Jul 14, 2026
…-if from LKI (CR 608.2h) (#5792) `TriggerCondition::SourceMatchesFilter` asks the filter about the SOURCE ITSELF as the subject, and CR 603.4 re-checks the intervening-if at RESOLUTION — by which time the source may be gone. A token that has left the battlefield ceased to exist (CR 111.7 / CR 704.5d) and was purged from `state.objects`, so the live-only lookup could not see the subject at all and failed closed for every property. t73 (#5779) repaired the CONTEXT half of this class (the source's controller now answers from LKI); this is the SUBJECT half. CR 608.2h: "If the effect requires information from a specific object, INCLUDING THE SOURCE OF THE ABILITY ITSELF, the effect uses the current information of that object if it's in the public zone it was expected to be in; if it's no longer in that zone ... the effect uses the object's LAST KNOWN INFORMATION." CR 113.7a keeps the ability resolving regardless of its source's fate. Two halves, each independently necessary (proven by ablation — neither alone turns the witness green): 1. The trigger arm now routes through `subject_filter_matches_with_lki`, the existing single authority for this fallback (shared with match_sacrificed / match_connives / the exploit matcher). It matches the LIVE object first and consults `lki_cache` only when the object no longer carries its battlefield appearance — a strict no-op for any source still on the battlefield. 2. `LKISnapshot` now carries the exit-time attachment set. Attachment is a battlefield-only relationship — SBA unattaches everything the instant the host leaves (CR 704.5m/n) — so "if this creature is enchanted" had nothing live to read even once the subject was visible, and the LKI→ZoneChangeRecord synthesizer hardcoded `attachments: vec![]`. `apply_zone_exit_cleanup` cannot capture this itself: in `move_to_zone` the sever runs first, so the attachment list is already empty by then. The caller — the only code that still holds it — now supplies it, reusing the one shared `capture_attachment_snapshot` authority. Soulbond (50 defs / 25 faces, the largest slice of the class) is structurally unaffected and must stay FALSE for a dead source (CR 702.95b: pairing exists only between battlefield creatures): `InZone` reads `record.from_zone`, which the LKI synthesizer leaves `None`, and `Unpaired` reads live `state.objects`. Witness: `purged_source_matches_filter_lki` — Dreampod Druid ("At the beginning of each upkeep, if this creature is enchanted, create a 1/1 green Saproling creature token"), the one face in the 65-face class whose gate is source-referential while its effect acts on the game rather than on the vanished source. Drives the real pipeline end to end; the observable is the Saproling. Carries a harness positive control (a living Druid must create the token — the control that proves the probe can reach the observable at all), a CR 603.4 first-check control, and a negative control asserting a purged token that was never enchanted still answers FALSE. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.