Skip to content

fix(engine): bind "cast the exiled cards" to the cards actually exiled - #7034

Merged
matthewevans merged 2 commits into
mainfrom
fix/foreach-category-exile-overreach
Aug 5, 2026
Merged

fix(engine): bind "cast the exiled cards" to the cards actually exiled#7034
matthewevans merged 2 commits into
mainfrom
fix/foreach-category-exile-overreach

Conversation

@matthewevans

@matthewevans matthewevans commented Aug 5, 2026

Copy link
Copy Markdown
Member

Fixes a user-reported bug: "exiling more than one card at a time isn't working", reported with a turn-10 game state dump.

The bug

Sanar, the Vivid Blade exiles one card per color, then grants "you may cast the exiled cards this turn". In the reported state, all 75 revealed cards were exiled and granted, not the per-color picks.

Two defects compose:

  1. Parser registry gap. publishes_tracked_set_from_resolution's helper is_exile_effect doesn't recognise ForEachCategory { action: ExileFromPool }, so the cast anaphor never got rewritten off ParentTarget.
  2. cast_from_zone::resolve read ability.targets unconditionally, so that ParentTarget resolved to the whole reveal window stamped by inject_last_revealed_targets' untyped tail.

Ablation over the real card: base exiles 5/5/8 → 3 cards; parser fix alone 5/5/8 → 3; both together 2/2/8 → 6. Only the pair fixes it.

The fix

cast_from_zone routes tracked-set casts through targeting::resolve_tracked_set_sentinel — the same binding authority change_zone::resolve already uses — and the anaphor binds to TrackedSetFiltered { caused_by: Exiled } rather than a bare set.

The cause filter is load-bearing, not decorative: publish_tracked_set extends the chain set, so a later PutCounterAll clause merges a battlefield creature into the same set the exile published. Without the filter, grant_lingering_permissions would rip that creature off the battlefield into exile. non_exile_publisher_members_are_excluded_from_the_cast_anaphor is the fixture for exactly that.

Second bug fixed on the way: Praetor's Grasp was silently broken at base — its grant landed nowhere, because the chain's inherited targets are [Player(P1)] and filter_map(Object) yielded nothing.

Second commit — review follow-up

publishes_exiled_cause_at_resolution delegated to is_exile_effect, which recurses into CreateDelayedTrigger. That makes it answer yes for a clause that stamps nothing when it resolves, so an anaphor bound after one would match nothing.

It can't simply be excluded — the same predicate is the base term of publishes_tracked_set_from_resolution, where the delayed wrapper's yes is correct (strip_temporal_suffix folds a previous clause's real exile into it). Six scopes have a tracked-set consumer preceded only by a delayed wrapper — conqueror's galleon, end-blaze epiphany, fire giant's fury, priority boarding, storm herald, waltz of rage — and would lose their binding entirely.

Two questions, two predicates. Both directions pinned in the invariant test. Revert probe run rather than assumed: collapsing the split fails the six-card guard.

Fixture refresh

The 11 integration_cards.json entries this change invalidates are refreshed, field-scoped to abilities / static_abilities / triggers.

This matters because add_real_card loads the AST from that fixture instead of parsing live — so urza_lord_high_artificer_shuffle_exile_free_cast was asserting the pre-fix shape and passing for the wrong reason. It passes before and after; only now does it assert what the engine produces.

62 entries stale for unrelated reasons are deliberately left alone — a separate refresh PR is owed. Note gen-test-fixture.py --check verifies key presence only and cannot detect any of this.

Blast radius

51 cards bind a tracked-set cast; all 51 resolve to TrackedSetFiltered { filter: Any, caused_by: Exiled }, and all 51 have a strict non-delayed Exiled-stamping producer earlier in the chain.

Honest residual: 30 of those 51 are named nowhere in crates/engine — no parser shape test, no runtime test. They had no coverage before this change either. The whole-population invariant proves the filter matches something, not that each chain publishes the right members.

CR annotations

CR 614.6CR 607.2a on the anaphor-binding sites added here. 614.6 is "if an event is replaced, it never happens" and says nothing about which objects a later reference resolves to. 607.2a is the linked-ability rule for an activated or triggered ability that instructs a player to exile; 607.2b and 614.14 are the replacement-effect variants, which these cards are not. CR 105.1 dropped from a test comment — it names the five colors and mandates no iteration order.

The existing 608.2c + 614.6 pairing elsewhere in the parser is pre-existing and left alone.

Deferred, recorded

  • Shape GThe Key to the Vault, Black Cat, Cunning Thief, Djeru and Hazoret, Keldon Flamesage exile their entire looked-at pile. Verified byte-identical before and after this change; different seam. Its pinned test is green on stale oracle text and never resolves the trigger.
  • OQ2 — the sibling GrantCastingPermission { PlayFromExile } anaphor keeps the bare binding. Inert today (no exile delivery).

Verification

  • --lib 18523 passed / 0 failed; --test integration 4562 passed / 0 failed
  • 9 targeted integration tests + 338 snapshot tests green
  • Revert probe on the predicate split: red on the intended assertion

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of cards tracked through exile effects, including category-specific exile.
    • Added more precise filtering so only eligible exiled cards can be selected.
    • Preserved selection order while preventing duplicate cards.
    • Improved compatibility with delayed effects and related card interactions.
  • Tests

    • Expanded coverage for accepting or declining exiled cards, cause and type filtering, and excluding unrelated tracked cards.
    • Added regression coverage for Sanar Vivid and Praetor’s Grasp scenarios.

A user reported that exiling more than one card at a time did not work. On
Sanar, Innovative First-Year, the trigger exiled all 76 revealed library cards
and granted every one a cast permission, instead of exiling only the two
per-colour picks and shuffling the rest back. Their game ended with 74 basic
lands in exile and an 11-card library.

Two independent defects compose into that symptom.

**Parser — a registry gap.** `publishes_tracked_set_from_resolution` gates the
`ParentTarget` -> tracked-set widening, and its helper `is_exile_effect` does
not know `Effect::ForEachCategory { action: ExileFromPool }` — even though
`complete_per_category_exile` publishes exactly that set at runtime with
`ThisWayCause::Exiled`. The sibling predicate `chain_clause_is_exile_producer`
already lists that producer and names Sanar. So "You may cast the exiled cards
this turn" stayed on the `ParentTarget` sentinel.

**Engine — an unconditional target read.** `cast_from_zone::resolve` collected
`ability.targets` regardless of what its own `target_filter` denoted, so
`ParentTarget` resolved to whatever the chain had propagated — here the whole
reveal window, stamped by `inject_last_revealed_targets`' untyped tail and
carried through `Shuffle` by chain inheritance. `grant_lingering_permissions`
then routes every target not already in exile through an exile-delivery batch,
which is what moved 74 lands out of the library.

Ablation confirms both are load-bearing: the parser fix alone corrects the
parsed shape and changes nothing at runtime; the engine fix alone cannot help
a card whose anaphor never became a tracked-set reference.

The binding is cause-filtered, not a bare `TrackedSet{0}`, and that is
load-bearing rather than defensive. `publish_tracked_set` EXTENDS the chain
set, so an intervening publisher merges its own objects in. A constructed
`ExileTop -> PutCounterAll -> CastFromZone` fixture shows the bare binding
grants a cast permission to a battlefield creature and moves it to exile;
`caused_by: Exiled` reads only members whose producer action was an exile.
Portent of Calamity already ships that filter shape for the same reason.

`cast_from_zone` now binds tracked-set filters through
`targeting::resolve_tracked_set_sentinel` — the same authority
`change_zone::resolve` already uses — deduplicated, since the extend semantics
let an id appear twice.

Also fixed, found while testing: Praetor's Grasp was silently broken. It exiled
the card but the "you may play that card" grant landed nowhere, because the
anaphor bound to an empty target list. It now works.

Scope: 50 existing cards carry a `CastFromZone` tracked-set target and change
binding source from inherited chain targets to the published set; Sanar makes
51. Every one of those 51 scopes was verified to contain an in-scope producer
that stamps `Exiled` at runtime, with zero exceptions, and a new invariant test
keeps that correspondence true. Coverage is unchanged: 31645/35657 supported
before and after, identical `Unimplemented` counts.
…tion

Review follow-up to the ForEachCategory exile fix.

`publishes_exiled_cause_at_resolution` delegated to `is_exile_effect`, which
recurses into `CreateDelayedTrigger`. That made it answer yes to a clause
that stamps nothing when it resolves, so a same-chain "the exiled cards"
anaphor bound after one would match nothing at runtime.

It cannot simply be excluded: the same predicate is the base term of
`publishes_tracked_set_from_resolution`, where the delayed wrapper's yes is
correct — `strip_temporal_suffix` folds a previous clause's real exile into
the wrapper, so the chain did publish a set. Six scopes have a tracked-set
consumer preceded only by a delayed wrapper (conqueror's galleon, end-blaze
epiphany, fire giant's fury, priority boarding, storm herald, waltz of rage)
and would lose their binding entirely.

So the two questions get two predicates: the narrow one spells its shapes out
and drops the recursion; the wide one lists `is_exile_effect` separately.
Both directions are pinned. Revert probe run, not assumed: collapsing the
split fails the six-card guard.

Also:
- Refresh the 11 integration_cards.json entries this change invalidates, field
  scoped (abilities/static_abilities/triggers only). The Urza tests load their
  AST from that fixture, so they were asserting the pre-fix shape and passing
  for the wrong reason. The 62 entries stale for unrelated reasons are left
  alone; --check only verifies presence and cannot see any of this.
- CR 614.6 -> CR 607.2a on the anaphor-binding sites added here. 614.6 is
  "if an event is replaced, it never happens" and says nothing about which
  objects a later reference resolves to. 607.2a is the linked-ability rule for
  an activated or triggered ability that instructs a player to exile; 607.2b
  and 614.14 are the replacement-effect variants, which these cards are not.
- Drop CR 105.1 from a test comment: it names the five colors and mandates no
  iteration order.
- Correct the rung-2 doc to name the And shape all 51 cards produce.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The parser now distinguishes exile-caused tracked-set members and rewrites cast targets accordingly. Runtime casting resolves tracked-set candidates directly, filters them, and removes duplicates. Parser and Sanar Vivid tests cover cause stamping, filtering, optional selections, and multi-publisher cases.

Changes

Tracked-set casting

Layer / File(s) Summary
Parser cause binding
crates/engine/src/parser/oracle_effect/{mod.rs,assembly.rs}, crates/engine/src/game/effects/mod.rs
The parser detects direct and category-based exile publishers. Cast parent targets use TrackedSetFiltered with ThisWayCause::Exiled when prior clauses publish exile-caused members.
Runtime candidate resolution
crates/engine/src/game/effects/cast_from_zone.rs
CastFromZone resolves tracked-set candidates directly, applies filters and ability context, preserves publication order, and deduplicates object IDs.
Parser and integration validation
crates/engine/src/parser/oracle_effect/tests.rs, crates/engine/tests/integration/issue_4253_sanar_vivid.rs
Tests cover cause stamping, filtered bindings, optional per-color exile selections, type filtering, Praetor’s Grasp, and multi-publisher behavior.

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

Possibly related PRs

  • phase-rs/phase#6545: Both changes handle exiled cards through TrackedSetFiltered and ThisWayCause::Exiled.
  • phase-rs/phase#6661: Both changes update tracked-set exile and cause handling in the parser and effects system.

Suggested labels: bug

Suggested reviewers: andriypolanski, lgray

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: binding casts to the cards that were actually exiled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/foreach-category-exile-overreach

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.45.0)
crates/engine/src/parser/oracle_effect/mod.rs

ast-grep timed out on this file


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.

❤️ Share

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

@matthewevans
matthewevans enabled auto-merge August 5, 2026 14:54
@matthewevans

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@matthewevans
matthewevans added this pull request to the merge queue Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Generated for head 457bad0e44d18e2fa40b00951c817135e17e889f.

Parse changes introduced by this PR · 51 card(s), 2 signature(s) (baseline: main fa5fbdfd7059)

🟡 Modified fields (2 signatures)

  • 50 cards · 🔄 ability/CastFromZone · changed field target: tracked set #0tracked set #0 matching any target
    • Affected (first 3): A-Nashi, Moon Sage's Scion, A-Visions of Phyrexia, Abstruse Appropriation (+47 more)
  • 1 card · 🔄 ability/CastFromZone · changed field target: parent targettracked set #0 matching any target
    • Affected (first 3): Sanar, Innovative First-Year

@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/parser/oracle_effect/mod.rs (1)

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

Consider carrying the cause as a typed value instead of a bool.

cast_anaphor_is_exiled: bool encodes which ThisWayCause the cast anaphor may narrow to. The value the rewrite actually needs is Some(ThisWayCause::Exiled) or None. An Option<ThisWayCause> parameter carries the same information with more meaning, removes the two-arm branch in the CastFromZone arm, and extends without a second boolean when a further cause (for example a sacrifice or reveal cause) becomes narrowable. CLAUDE.md requires typed carriers over booleans for distinguishable cases.

♻️ Sketch of the typed signature
-fn rewrite_parent_targets_to_tracked_set(effect: &mut Effect, cast_anaphor_is_exiled: bool) {
+fn rewrite_parent_targets_to_tracked_set(effect: &mut Effect, cast_cause: Option<ThisWayCause>) {
         Effect::CastFromZone { target, .. } => {
-            if cast_anaphor_is_exiled {
-                rewrite_filter_parent_to_exiled_tracked_set(target)
-            } else {
-                rewrite_filter_parent_to_tracked_set(target)
-            }
+            match cast_cause {
+                Some(cause) => rewrite_filter_parent_to_caused_tracked_set(target, cause),
+                None => rewrite_filter_parent_to_tracked_set(target),
+            }
         }

Also applies to: 26455-26461

🤖 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_effect/mod.rs` at line 26411, Update
rewrite_parent_targets_to_tracked_set to carry the cast-anaphor narrowing cause
as an Option<ThisWayCause> instead of cast_anaphor_is_exiled: bool, and pass
through Some(ThisWayCause::Exiled) or None at the call sites. In the
CastFromZone handling, remove the two-arm boolean branch and use the typed
option directly when deciding the tracked-set cause. Preserve the existing
behavior for the exiled and non-exiled cases, and keep the change scoped to the
rewrite_parent_targets_to_tracked_set path and its immediate callers.

Source: Coding guidelines

🤖 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/effects/cast_from_zone.rs`:
- Around line 96-103: Update the tracked-set filtering in cast_from_zone so the
FilterContext is built from an ability clone whose targets are replaced with the
tracked-set members, matching the existing sibling patterns in this resolver.
Keep the deduped members list as the candidate set, but do not pass the original
ability directly into FilterContext::from_ability; instead, bind the context to
the same exact ids being filtered so ParentTarget-relative and name/type
comparisons evaluate against the published set.

In `@crates/engine/src/parser/oracle_effect/assembly.rs`:
- Around line 2434-2446: Update the comment in the exiled-cause narrowing block
that sets cast_anaphor_is_exiled and calls rewrite_parent_targets_to_tracked_set
so it cites CR 607.2a instead of CR 614.6. Keep the existing behavior unchanged,
and align the wording with publishes_exiled_cause_at_resolution, which already
uses CR 607.2a and excludes the replacement-effect case.

In `@crates/engine/src/parser/oracle_effect/tests.rs`:
- Around line 22560-22588: Update the `uncaused_exilers` test fixture in
`oracle_effect/tests.rs` so it includes the three missing uncaused exile shapes
alongside `Effect::HeistExile` and `Effect::RevealUntil`: add `Effect::Dig {
destination: Some(Zone::Exile), .. }`, `Effect::ExileHaunting { .. }`, and
`Effect::ExileResolvingSpellInsteadOfGraveyard { .. }`. Keep the existing loop
and assertions in place so these shapes are covered by the same positive
`chain_clause_is_exile_producer` check and the negative
`publishes_exiled_cause_at_resolution` / `this_way_cause_for_effect` checks.

---

Nitpick comments:
In `@crates/engine/src/parser/oracle_effect/mod.rs`:
- Line 26411: Update rewrite_parent_targets_to_tracked_set to carry the
cast-anaphor narrowing cause as an Option<ThisWayCause> instead of
cast_anaphor_is_exiled: bool, and pass through Some(ThisWayCause::Exiled) or
None at the call sites. In the CastFromZone handling, remove the two-arm boolean
branch and use the typed option directly when deciding the tracked-set cause.
Preserve the existing behavior for the exiled and non-exiled cases, and keep the
change scoped to the rewrite_parent_targets_to_tracked_set path and its
immediate callers.
🪄 Autofix

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: 38035f1d-8f33-42f6-a48a-0c3b302f843a

📥 Commits

Reviewing files that changed from the base of the PR and between fa5fbdf and 457bad0.

⛔ Files ignored due to path filters (1)
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__nashi_moon_sages_scion_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
📒 Files selected for processing (7)
  • crates/engine/src/game/effects/cast_from_zone.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/tests/fixtures/integration_cards.json
  • crates/engine/tests/integration/issue_4253_sanar_vivid.rs

Comment on lines +96 to +103
let ctx = crate::game::filter::FilterContext::from_ability(ability);
let mut seen = HashSet::new();
members
.iter()
.copied()
.filter(|obj_id| seen.insert(*obj_id))
.filter(|obj_id| crate::game::filter::matches_target_filter(state, *obj_id, &bound, &ctx))
.collect()

@coderabbitai coderabbitai Bot Aug 5, 2026

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

Bind the filter context to the tracked-set members, not to ability.targets.

The helper stops reading ability.targets for candidate ids, then builds the FilterContext from that same polluted ability. FilterContext::from_ability(ability) carries ability.targets, which for Sanar is the whole reveal window the chain seam injected. A residual leg in the tracked-set filter that performs an object-scope read (a ParentTarget-relative comparison, a same-name or shares-a-type leg) therefore evaluates against the injected window rather than against the published set. The two sibling sites in this same resolver already avoid that: lines 429-435 and lines 502-504 both clone the ability and replace targets with the exact candidate set before constructing the context.

Mirror that pattern so the context and the candidates describe the same set.

🛡️ Proposed fix
-    let ctx = crate::game::filter::FilterContext::from_ability(ability);
     let mut seen = HashSet::new();
-    members
+    let deduped: Vec<ObjectId> = members
         .iter()
         .copied()
         .filter(|obj_id| seen.insert(*obj_id))
+        .collect();
+    // Bind the filter's object-scope reads to exactly the published set,
+    // mirroring the scoped contexts used by the `ExiledBySource` paths below.
+    let mut scoped_ability = ability.clone();
+    scoped_ability.targets = deduped.iter().copied().map(TargetRef::Object).collect();
+    let ctx = crate::game::filter::FilterContext::from_ability(&scoped_ability);
+    deduped
+        .into_iter()
         .filter(|obj_id| crate::game::filter::matches_target_filter(state, *obj_id, &bound, &ctx))
         .collect()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let ctx = crate::game::filter::FilterContext::from_ability(ability);
let mut seen = HashSet::new();
members
.iter()
.copied()
.filter(|obj_id| seen.insert(*obj_id))
.filter(|obj_id| crate::game::filter::matches_target_filter(state, *obj_id, &bound, &ctx))
.collect()
let mut seen = HashSet::new();
let deduped: Vec<ObjectId> = members
.iter()
.copied()
.filter(|obj_id| seen.insert(*obj_id))
.collect();
// Bind the filter's object-scope reads to exactly the published set,
// mirroring the scoped contexts used by the `ExiledBySource` paths below.
let mut scoped_ability = ability.clone();
scoped_ability.targets = deduped.iter().copied().map(TargetRef::Object).collect();
let ctx = crate::game::filter::FilterContext::from_ability(&scoped_ability);
deduped
.into_iter()
.filter(|obj_id| crate::game::filter::matches_target_filter(state, *obj_id, &bound, &ctx))
.collect()
🤖 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/cast_from_zone.rs` around lines 96 - 103,
Update the tracked-set filtering in cast_from_zone so the FilterContext is built
from an ability clone whose targets are replaced with the tracked-set members,
matching the existing sibling patterns in this resolver. Keep the deduped
members list as the candidate set, but do not pass the original ability directly
into FilterContext::from_ability; instead, bind the context to the same exact
ids being filtered so ParentTarget-relative and name/type comparisons evaluate
against the published set.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed — but not on this PR. The merge queue landed #7034 before the fix could be pushed (queued branches reject updates, and by the time I dequeued it had already merged), so all three findings ship in follow-up #7035.

Verified rather than accepted: both sibling sites in cast_from_zone already clone-and-rescope the ability before building the context, one of them under the same CR 607.2a — so the third site was genuinely inconsistent. It is latent today (all 51 cards bind filter: Any, which reads no object scope), closed so it stays that way. The assembly.rs citation was a real miss on my side: my sweep used a hand-listed file set that omitted it. And the three added exile shapes each pass both the positive and negative assertions, which is what establishes they are genuinely uncaused.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="phase-rs/phase"
gh pr view 7035 --repo "$repo" --json number,state,mergedAt,url,title,files \
  --jq '{number, state, mergedAt, url, title, files: [.files[].path]}'

Length of output: 689


@matthewevans, I verified that #7035 is open and targets this exact correction in crates/engine/src/game/effects/cast_from_zone.rs.

The follow-up also includes crates/engine/src/parser/oracle_effect/assembly.rs and crates/engine/src/parser/oracle_effect/tests.rs. Since #7035 is not merged yet, I will leave this finding unresolved on #7034 until the fix lands.


✏️ Learnings added
Learnt from: matthewevans
Repo: phase-rs/phase PR: 7034
File: crates/engine/src/game/effects/cast_from_zone.rs:96-103
Timestamp: 2026-08-05T15:38:05.074Z
Learning: In `crates/engine/src/game/effects/cast_from_zone.rs`, tracked-set cast filters must build `FilterContext` from a `ResolvedAbility` whose `targets` are replaced with the deduplicated tracked-set members. This keeps object-scope filter evaluation bound to the published set rather than chain-injected targets.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Comment thread crates/engine/src/parser/oracle_effect/assembly.rs
Comment thread crates/engine/src/parser/oracle_effect/tests.rs
Merged via the queue into main with commit 14b11cf Aug 5, 2026
15 checks passed
@matthewevans
matthewevans deleted the fix/foreach-category-exile-overreach branch August 5, 2026 15:35
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