Skip to content

fix(parser): guard an existing player_scope + pin the reason in #6965's fail-closed tests - #7009

Merged
matthewevans merged 1 commit into
mainfrom
ship/6965-review-fixes
Aug 5, 2026
Merged

fix(parser): guard an existing player_scope + pin the reason in #6965's fail-closed tests#7009
matthewevans merged 1 commit into
mainfrom
ship/6965-review-fixes

Conversation

@matthewevans

Copy link
Copy Markdown
Member

Follow-up to #7003 (merged), addressing the CodeRabbit review it did not wait for.

Correctness (the one that isn't cosmetic)

bind_recipient_without_recipient_slot assigned def.player_scope = Some(scope) unconditionally while its doc promised a "Total and FAIL-CLOSED" contract. That contract covered recipients no PlayerFilter can name — it did not cover a body that already carries its own iteration scope.

Both halves are rewritten from clones of one parsed body, so an unguarded stamp would replace a printed per-player fan-out with a single recipient, and with a different one on each half, silently resolving for the wrong set of players. The binder now refuses when a scope is already present; the caller already converts false into Unimplemented, so this falls through to an honest gap.

CR citations

site was now why
Wand of Orcus ×2 CR 301.5f CR 301.5a (+ 301.5f) 301.5f defines what an ability means by "equipped creature"; 301.5a is the attachment relation
Infernal Offering test CR 109.4 CR 608.2d only 109.4 is about which objects have controllers; "that player" is a player chosen while applying the effect, no object controller involved
targeted-player fail-closed CR 109.4 CR 115.1 115.1 is the rule that makes those conjuncts targets in the first place

Two kept deliberately. CR 608.2c stays on the Life at Stake test — it asserts chain order mirroring printed order, which is exactly what that rule governs. CR 109.4 stays on that test's ParentObjectTargetController binding, where the player genuinely is named through a battlefield object's controller.

Vacuous negative assertions

Two tests accepted any Effect::Unimplemented, so they would have passed on a clause that died earlier — or, in the Wand case, on a dropped execute effect that also grants no deathtouch. Both now assert the gap is named unbound_subject and quotes the conjunct that caused it.

recipient_less_body_with_a_targeted_player_conjunct_fails_closed gains a positive reach guard, because a dead distributor fails closed on everything, including what it should bind. Worth noting how that guard was built: my first attempt used a bare "You and that player each sacrifice a creature." and went red — "that player" without its preceding "Choose an opponent." is itself an unbound subject. The guard now carries the full Infernal Offering text.

Docs

SubjectPhraseAst::affected claimed one consumer reads it. sync_subject_into_nested_shuffle_sub and inject_subject_target read it too via target.or(affected). Narrowed to the invariant actually intended: ImperativeFallback is the only consumer treating None as a coverage gap; the others treat it as "nothing to rebind". None is reachable in all three — stated so a future edit doesn't assume otherwise.

Verification

  • parser tests 1583 passed, 0 failed
  • engine lib 18487 passed, 0 failed
  • integration 4487 passed, 0 failed
  • cargo clippy -p phase-engine --all-targets -- -D warnings clean

…e fail-closed tests (#6965)

Six findings from the CodeRabbit review of #7003, which merged before these
landed.

**Correctness.** `bind_recipient_without_recipient_slot` assigned
`def.player_scope = Some(scope)` unconditionally while documenting a
"Total and FAIL-CLOSED" contract. That contract only covered recipients no
`PlayerFilter` can name — not a body that already carries its own iteration
scope. Both halves are rewritten from clones of ONE parsed body, so an
unguarded stamp would replace a printed per-player fan-out with a single
recipient, and with a different one on each half. Refuse when a scope is
already present; the caller already turns `false` into `Unimplemented`.

**CR citations.** CR 301.5f defines what an ability means by "equipped
creature"; it does not define attachment, which is CR 301.5a. Both sites now
cite the rule whose body describes the code. CR 109.4 is about which objects
have controllers, so it does not describe Infernal Offering's "that player" —
a player chosen while applying the effect, CR 608.2d, which the doc already
cited. Dropped it there. The targeted-player fail-closed contract now cites
CR 115.1, which is the rule that actually makes those conjuncts targets.

**Vacuous negative assertions.** Two tests accepted any `Effect::Unimplemented`
and so would have passed on a clause that died earlier, or on a dropped effect.
Both now assert the gap is named `unbound_subject` and quotes the conjunct that
caused it. `recipient_less_body_with_a_targeted_player_conjunct_fails_closed`
gains a positive reach guard, since a dead distributor fails closed on
everything including what it should bind — the guard carries the full Infernal
Offering text, because "that player" without its preceding "Choose an opponent."
is itself an unbound subject. The Wand of Orcus test proved only that a trigger
reached the stack, which a dropped execute effect would also satisfy while
granting no deathtouch; it now inspects the trigger's chain before resolution.

**Docs.** `SubjectPhraseAst::affected` claimed one consumer reads it.
`sync_subject_into_nested_shuffle_sub` and `inject_subject_target` read it too,
via `target.or(affected)`. Narrowed to the invariant actually intended:
`ImperativeFallback` is the only consumer treating `None` as a coverage gap;
the others treat it as "nothing to rebind". `None` is reachable in all three.

Two citations kept deliberately. CR 608.2c stays on the Life at Stake test: it
asserts chain order mirroring printed order, which is what that rule governs.
CR 109.4 stays on the same test's `ParentObjectTargetController` binding, where
the player IS named through a battlefield object's controller.

Verified: parser tests 1583 passed; engine lib 18487 passed; integration 4487
passed; clippy -D warnings clean. The new reach guard was watched go red first
(it caught a wrong positive example, which is how the antecedent requirement
above was found).
@matthewevans
matthewevans enabled auto-merge August 4, 2026 23:23
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@matthewevans, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d0f9c097-1416-439b-952f-64f1e264989e

📥 Commits

Reviewing files that changed from the base of the PR and between 644c713 and af4a2d9.

📒 Files selected for processing (4)
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/tests/integration/wand_of_orcus_compound_subject_6965.rs

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.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Generated for head af4a2d9dacc77c73e7561ba5a08452be13aeac7b.

Parse changes introduced by this PR

✓ No card-parse changes detected.

Merged via the queue into main with commit d7241ef Aug 5, 2026
14 checks passed
@matthewevans
matthewevans deleted the ship/6965-review-fixes branch August 5, 2026 00:00
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