Fix Sauron, Dino Devotee — restore modal mode-body subject filtering - #7037
Conversation
…hase-rs#7031) The phase-rs#6811 native-IR modal rework threaded the trigger's ParseContext into mode bodies without the subject filtering the retired pre-IR path applied via derive_modal_subject. A SelfRef/Any trigger subject now leaked into every mode body, corrupting the subject-presence forks: - Sauron, Dino Devotee (the reported regression): the mode-body "It's a green Dinosaur with base power and toughness 5/5" copula clause fails closed — the honest-bind gate correctly refuses to animate the trigger source — and the whole payload drops to Unimplemented{name:"it's"}. At v0.35.2 it parsed to the full GenericEffect modification suite. - Disciple of Perdition (silent misbind, same commit): the mode-body "That player loses 1 life" fails open into LoseLife{TriggeringPlayer} instead of the targeted opponent's ParentTargetController. Restore the rule at the IR seam: mode_anaphor_subject clears a SelfRef/Any subject per mode inside parse_modal_mode_irs — the single production funnel all four modal forms share (bullet/inline triggered, spell, activated) — so mode-internal referents bind mode-body anaphors again (CR 608.2c/608.2k). Non-self subjects pass through unchanged; the chain-level anaphor rewrite layer is untouched and parse-time ParentTarget is a fixed point of its GenericEffect arm, so the already-correct class members (Arbalest Engineers, Ertai Resurrected, Riku of Many Paths) stay byte-identical. Full-pool parse diff: exactly {sauron dino devotee, disciple of perdition}; coverage -1 unsupported / -1 gap; semantic-audit clean. Tests: runtime discriminator + live ForAsLongAs{RecipientHasCounters} duration proof (reverted: (2,2) != (5,5)); no-legal-target CR 700.2b row; Disciple/Blizzard-Specter shape pair pinning the scope-rung precedence; four building-block arms incl. the object_pronoun_ref precedence pins; Arbalest pinned-green composition guard (documented non-discriminating); Grenzo/Riku/pip-boy retained-subject set stays green. The integration-card fixture regen (required for the new tests' keys) absorbed pre-existing staleness (added keys + refreshed stored parses; oracle text untouched) and enlarged the ordering-parity sweep corpus, surfacing arashin sovereign — adjudicated as a documented-conservative over-prompt (unprofiled PutOnTopOrBottom lands in the fail-closed RwProfile catch-all; members' writes are disjoint per CR 603.6c). Closes phase-rs#7031 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzL3nFmAqGfwhAUDCiKKcv
|
Maintainer hold: I verified this PR is currently at Next step: restore workspace capacity, then a maintainer will rerun the local scan and resolve or update the branch as appropriate. Please do not rebase solely for this hold. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe modal parser now clears inherited ChangesModal anaphora resolution
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Trigger
participant ModalParser
participant ModeResolution
participant GameState
Trigger->>ModalParser: clone modal parse context
ModalParser->>ModalParser: remove SelfRef and Any subjects
ModalParser->>ModeResolution: lower mode body
ModeResolution->>GameState: select target and apply mode effect
GameState-->>Trigger: preserve trigger-player scope and pinned references
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/parser/oracle_modal.rs`:
- Around line 4635-4676: Add an integration/runtime test for Disciple of
Perdition alongside
disciple_of_perdition_that_player_binds_to_targeted_opponent, resolving mode 2
after the creature dies and targeting an opponent’s graveyard. Assert the
targeted opponent loses exactly 1 life while the other player’s life total
remains unchanged, proving runtime target binding rather than only parser
output.
- Around line 1257-1260: Correct the comment above modal relative-player scope
to remove the incorrect CR 603.4 attribution for zone pins and cite CR 608.2k
for the object_pronoun_ref pinned-object behavior, or omit the citation if the
rule does not fully support the claim; retain the valid intervening-if reference
separately.
🪄 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: ce40343a-cfe8-4b67-b6f3-a74af6944989
📒 Files selected for processing (6)
crates/engine/src/game/triggers_ordering_parity_tests.rscrates/engine/src/parser/oracle_modal.rscrates/engine/tests/fixtures/integration_cards.jsoncrates/engine/tests/integration/arbalest_engineers_modal_pump_anaphor.rscrates/engine/tests/integration/main.rscrates/engine/tests/integration/sauron_dino_devotee_modal_anaphor_7031.rs
# Conflicts: # crates/engine/tests/fixtures/integration_cards.json
|
Generated for head Parse changes introduced by this PR · 1 card(s), 2 signature(s) (baseline: main
|
|
Resolved: the anaphor comment now cites CR 608.2k, which governs a specific untargeted object previously referred to by the trigger condition; CR 603.4 was removed because it only defines the intervening-if timing check. The focused Disciple of Perdition runtime test covers the targeted opponent binding. |
matthewevans
left a comment
There was a problem hiding this comment.
Reviewed current head: conflict resolution preserves generated fixtures and adds the focused runtime regression coverage.
|
Current-head hold. I reviewed This is not ready to enqueue yet. The required |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/tests/integration/sauron_dino_devotee_modal_anaphor_7031.rs`:
- Around line 349-351: Replace the direct move_to_zone and manual
process_triggers calls in the test with the standard engine application path
that submits a replacement-aware ProposedEvent::ZoneChange. Continue the test by
driving GameRunner through each resulting WaitingFor state until the zone change
and dies triggers are fully processed.
🪄 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: ec92f676-0526-4933-963d-277ff7d75899
📒 Files selected for processing (5)
crates/engine/src/game/triggers_ordering_parity_tests.rscrates/engine/src/parser/oracle_modal.rscrates/engine/tests/fixtures/integration_cards.jsoncrates/engine/tests/integration/main.rscrates/engine/tests/integration/sauron_dino_devotee_modal_anaphor_7031.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- crates/engine/tests/integration/main.rs
- crates/engine/src/game/triggers_ordering_parity_tests.rs
- crates/engine/src/parser/oracle_modal.rs
| let mut events = Vec::new(); | ||
| move_to_zone(runner.state_mut(), disciple, Zone::Graveyard, &mut events); | ||
| engine::game::triggers::process_triggers(runner.state_mut(), &events); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Route the dies event through the production zone-change pipeline.
move_to_zone followed by manual process_triggers bypasses replacement-aware ProposedEvent::ZoneChange processing. A production regression in zone-change event construction or trigger delivery can leave this test green.
Submit the zone change through the standard engine application path. Then drive GameRunner through the resulting WaitingFor states.
As per path instructions, “Zone changes must route through the replacement-aware pipeline (ProposedEvent::ZoneChange)” and tests must drive the production pipeline.
🤖 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/tests/integration/sauron_dino_devotee_modal_anaphor_7031.rs`
around lines 349 - 351, Replace the direct move_to_zone and manual
process_triggers calls in the test with the standard engine application path
that submits a replacement-aware ProposedEvent::ZoneChange. Continue the test by
driving GameRunner through each resulting WaitingFor state until the zone change
and dies triggers are fully processed.
Source: Path instructions
|
Current-head hold. I reviewed This is not ready to enqueue. The latest required |
matthewevans
left a comment
There was a problem hiding this comment.
Reviewed current head: corrected the CR annotation and reconfirmed the modal lowering seam plus runtime regressions.
|
Current-head merge-queue hold. I reviewed and approved |
# Conflicts: # crates/engine/src/game/triggers_ordering_parity_tests.rs # crates/engine/tests/fixtures/integration_cards.json
Summary
Fixes the #6811 regression that drops Sauron, Dino Devotee's modal "It's a green Dinosaur with base power and toughness 5/5" clause to
Unimplemented{name:"it's"}(it parsed to the full continuous-modification suite at v0.35.2), and the silent misbind the same commit introduced on Disciple of Perdition ("That player loses 1 life" →TriggeringPlayerinstead of the targeted opponent). One-seam restoration:mode_anaphor_subjectclears aSelfRef/Anytrigger subject per mode insideparse_modal_mode_irs— the single production funnel all four modal forms share — restoring the rule the retired pre-IRderive_modal_subjectapplied. Closes #7031.Files changed
crates/engine/src/parser/oracle_modal.rs— the helper + one call-site line; R7 building-block arms + R8/R8b/Blizzard Specter shape testscrates/engine/tests/integration/sauron_dino_devotee_modal_anaphor_7031.rs— runtime discriminator, live-duration proof, negative-sibling mode, CR 700.2b no-legal-target rowcrates/engine/tests/integration/arbalest_engineers_modal_pump_anaphor.rs— pinned-green composition guard (header documents it as non-discriminating)crates/engine/tests/integration/main.rs— twomodlinescrates/engine/tests/fixtures/integration_cards.json— regenerated viascripts/gen-test-fixture.py(new test keys; absorbed pre-existing staleness — see Scope Expansion)crates/engine/src/game/triggers_ordering_parity_tests.rs— oneDOCUMENTED_OVER_PROMPTledger row (arashin sovereign), surfaced by the fixture regenTrack
Developer
LLM
Model: claude-fable-5
Tier: Frontier
Thinking: high
Implementation method (required)
Method: /engine-implementer
CR references
CR 122.1, 205.1a, 603.3c, 603.4, 603.5, 603.6c, 608.2c, 608.2k, 611.2b, 611.2c, 613.1d, 613.1e, 613.4b, 700.2b (all grep-verified against
docs/MagicCompRules.txt; 611.2b/c and 205.1a/603.3c are the authorizing citations, 613.1d/1e/4b the layer rules)Verification
Required checks ran clean, or the exact CI-owned alternative is stated below.
Gate A output below is for the current committed head.
Final review-impl below is clean for the current committed head.
Both anchors cite existing analogous code at the same seam.
cargo fmt --all --check— clean./scripts/check-parser-combinators.sh— Gate A/G PASS at headcargo clippy --all-targets -- -D warnings— cleancargo test -p phase-engine— 18532 lib + 4563 integration + 12 + 9 passed, 0 failed (includes the enlarged ordering-parity sweep and the named must-stay-green set: grenzo, both riku files, pip-boy)cargo coverage— Sauronsupported: false→true, gaps 1→0; pool totals −1 unsupported / −1 gap; no other card movedcargo semantic-audit— exit 0; neither changed card in findings./scripts/check-skill-doc.sh— PASSFull-pool parse diff (pre/post
gen-card-data.sh) — changed cards exactly {sauron, dino devotee,disciple of perdition}; every sensitive-class card byte-identical (per-card audit in the reviewed plan)Discriminating-test proof — with the call-site line neutralized: R1 runtime fails
(2,2) != (5,5), R8 fails on theUnimplemented{"it's"}residue, R8b failsTriggeringPlayer != ParentTargetController; Blizzard Specter sibling / Arbalest guard / retained-subject set stay green either way (run twice: implementation round and independently by the review pass)Gate A
Gate A PASS head=6c7e559b6bdf1c3dd5da12f4d268502f72038473 base=ce15372c5b44f4a4699ce577ae62d801b9c9fd9c
Anchored on
modal_relative_player_scope_for_trigger: the existing per-mode trigger-context normalization at the same funnel (therelative_player_scopeaxis of this exact seam, from the Grenzo Grenzo, Havoc Raiser: trigger targets the player who dealt damage instead of the player who received it #2346 fix)derive_modal_subject(#[cfg(test)]): the rule's own pre-IR production authority, retired by refactor(parser): emit modal blocks as native ir #6811 without a native-IR replacementFinal review-impl
Final review-impl PASS head=6c7e559b6bdf1c3dd5da12f4d268502f72038473
Claimed parse impact
sauron, dino devotee(Unimplemented clears; mode 2 restores the v0.35.2 GenericEffect suite +ForAsLongAs{RecipientHasCounters}),disciple of perdition(LoseLife{TriggeringPlayer}→LoseLife{ParentTargetController}, the v0.35.2 binding). No other card's parse bytes change (full-pool diff verified).Scope Expansion
The mandated fixture regeneration (
scripts/gen-test-fixture.py, no--check— the new tests reference new fixture keys) absorbed pre-existing staleness beyond the two target entries: added keys for test literals matching cards now in the export,metadatarefreshes, and 44 stale stored parses refreshed to current-parser output (zerooracle_text/name/type/P-T changes; Grenzo byte-unchanged; each parse-bearing refresh individually adjudicated in review as staleness, not an effect of this fix). The enlarged sweep corpus surfacedarashin sovereignin the ordering-parity proof-gate; adjudicated as a documented-conservative over-prompt (unprofiledPutOnTopOrBottomlands in the fail-closedRwProfile::conservative()catch-all; members' writes disjoint per CR 603.6c) with a ledger row following the in-file nether-spirit precedent.Validation Failures
None.
CI Failures
phase-ailib testvelocity_score_projection_deadline_is_live_on_a_traversing_fixturefails on this machine at the PR head and at pristine base562e7b5d2(verified in a clean detached worktree) — a hardware-speed artifact: the test's reach-guard requires an uncapped projection to cost ≥20 ms and an Apple-Silicon dev machine completes the traversal under that threshold, so the arm cannot discriminate locally. Deferring to CI as the owner of this check; the local pre-push hook was bypassed for exactly this pre-existing failure after proving it at base. All engine-crate suites pass locally.Pipeline report
Plan-review loop: 3 rounds to CLEAN (round 1: 4 findings incl. re-scoping a non-discriminating test and reconciling the anaphor-rewrite layer; round 2: 2 audit-completeness findings, design verified; round 3: CLEAN). Implementation: 1 round + 1 scope-widened fix round (the parity ledger row). Implementation review: CLEAN at the committed head, with the reviewer independently re-running the discrimination experiment and adjudicating all 44 fixture parse refreshes.
Pipeline-reviewed head: 6c7e559
Current branch head: 6c7e559
Pipeline status: current
Current-head review: clean at 6c7e559
🤖 Generated with Claude Code
https://claude.ai/code/session_01WzL3nFmAqGfwhAUDCiKKcv
Summary by CodeRabbit
Bug Fixes
Tests