Skip to content

ship/t73 purged source lki#5779

Merged
matthewevans merged 3 commits into
mainfrom
ship/t73-purged-source-lki
Jul 14, 2026
Merged

ship/t73 purged source lki#5779
matthewevans merged 3 commits into
mainfrom
ship/t73-purged-source-lki

Conversation

@matthewevans

Copy link
Copy Markdown
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.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@matthewevans
matthewevans enabled auto-merge July 14, 2026 03:06
@matthewevans
matthewevans added this pull request to the merge queue Jul 14, 2026
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

Merged via the queue into main with commit be7bd75 Jul 14, 2026
13 checks passed
@matthewevans
matthewevans deleted the ship/t73-purged-source-lki branch July 14, 2026 03:27
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant