Skip to content

feat(engine): implement CR 710 Kamigawa flip cards#6569

Open
real-venus wants to merge 4 commits into
phase-rs:mainfrom
real-venus:feat/kamigawa-flip-cards
Open

feat(engine): implement CR 710 Kamigawa flip cards#6569
real-venus wants to merge 4 commits into
phase-rs:mainfrom
real-venus:feat/kamigawa-flip-cards

Conversation

@real-venus

@real-venus real-venus commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Effect::FlipPermanent, implementing the CR 710 flip-card mechanic. 19 of the 21 printed flip cards now lower their flip instruction to a real effect instead of Unimplemented (Bushi Tenderfoot, Budoka Gardener, Nezumi Shortfang, Kitsune Mystic, Akki Lavarunner, the five Saviors "Ascendant" cards, …). Their alternative faces already parsed, so for most of them this was the only gap.

Why a separate slot from Transform

Flipping is deliberately its own engine path, per the CR:

  • CR 710.1b — the alternative name / type line / P&T / text box apply only while the permanent is on the battlefield and flipped.
  • CR 710.1c — a flip card's color and mana cost do not change. game/flip.rs therefore has its own apply_flipped_face_to_object rather than reusing the double-faced applicator (which swaps them). This is enforced on the flip path, the save/load rehydrate path, and by a guard that stops Effect::Transform / GameAction::Transform from ever running the DFC applicator on a flip card.
  • CR 710.2 / 710.4 — flipping is one-way, and a flipped permanent that leaves the battlefield reverts to its normal half (CR 110.5 new object).
  • CR 707.2 — a copy of a flipped permanent copies the unflipped normal half.

It also handles the interactions that giving flip cards a back face exposes: turning a flipped permanent face down no longer clobbers its stashed normal half, and the double-faced-card indicator (engine DTO + client) no longer fires for flip cards.

GameEvent::Flipped is emitted for the log/UI but intentionally dispatches no trigger key — no printed card triggers on flipping.

Scope

Out of scope, documented in flip.rs: CR 710.5 (choosing the alternative name for "choose a card name"), Homura's enters-flipped rider, and two second-order copy/face-down cases that would need a second face-stash slot on GameObject.

Testing

18 tests: per-rule coverage of 710.1b/1c/2/4, the transform and face-down interactions, copy semantics, save/load rehydrate, the "flip it" pronoun form end to end, and a corpus test pinning all 19 cards. The CR 710.1c assertion is mutation-verified (swapping in the DFC applicator fails exactly the right tests). CR 705.1 ("flip a coin") is unaffected.

cargo clippy --workspace --all-targets -D warnings clean; cargo test -p engine green (17489 lib + 3692 integration, 0 failed); 34 CR numbers verified against the Comprehensive Rules.

Summary by CodeRabbit

  • New Features
    • Added full Kamigawa “flip” permanent support, including oracle parsing/lowering, dedicated resolution, and accurate flip events and log/public-state rendering.
  • Bug Fixes
    • Flip permanents now apply alternate characteristics correctly (one-way), preserve mana cost and color, avoid transform/double-faced behavior, don’t generate trigger/indexing keys, and restore normal state when leaving the battlefield or during copy/reload.
  • UI Improvements
    • Updated the alternative-face (DFC) button logic to use flip-specific layout rules so the UI reflects the correct face behavior.
  • Tests
    • Added comprehensive end-to-end integration coverage for flip cards and pronoun targeting.

Adds `Effect::FlipPermanent`, implementing the flip-card mechanic. 19 of the 21
printed CR 710 cards now lower their flip instruction to a real effect instead
of `Unimplemented` (Bushi Tenderfoot, Budoka Gardener, Nezumi Shortfang, Kitsune
Mystic, Akki Lavarunner, the five Saviors "Ascendant" cards, ...). Their
alternative faces already parsed, so for most of them this was the only gap.

Flipping is deliberately a separate engine slot from transforming, per the CR:
- CR 710.1b: the alternative name / type line / P/T / text box apply only while
  the permanent is on the battlefield and flipped.
- CR 710.1c: a flip card's color and mana cost do NOT change. `flip.rs` therefore
  has its own `apply_flipped_face_to_object` rather than reusing the DFC
  applicator, which swaps them. This is enforced on the flip path, the save/load
  rehydrate path, and by a guard that stops `Effect::Transform` /
  `GameAction::Transform` from ever running the DFC applicator on a flip card.
- CR 710.2 / 710.4: flipping is one-way, and a flipped permanent that leaves the
  battlefield reverts to its normal half (CR 110.5 new object).
- CR 707.2: a copy of a flipped permanent copies the unflipped normal half.

Also handles the interactions that giving flip cards a back face exposes: turning
a flipped permanent face down no longer clobbers its stashed normal half, and the
double-faced-card indicator (engine DTO + client) no longer fires for flip cards.

`GameEvent::Flipped` is emitted for the log/UI but intentionally dispatches no
trigger key — no printed card triggers on flipping.

Out of scope, documented in `flip.rs`: CR 710.5 (choosing the alternative name),
Homura's enters-flipped rider, and two second-order cases that would need a
second face-stash slot on GameObject.

18 tests: per-rule coverage of 710.1b/1c/2/4, the transform and face-down
interactions, copy semantics, save/load, the "flip it" pronoun form end to end,
and a corpus test pinning all 19 cards. CR 705.1 ("flip a coin") is unaffected.
@real-venus
real-venus requested a review from matthewevans as a code owner July 23, 2026 22:05
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 64c096ea-146c-4afc-a13e-571c8d1564e1

📥 Commits

Reviewing files that changed from the base of the PR and between 3746a2d and db3c44a.

📒 Files selected for processing (5)
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/game/trigger_matchers.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/tests/integration/kamigawa_flip_cards.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/game/trigger_matchers.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/tests/integration/kamigawa_flip_cards.rs

📝 Walkthrough

Walkthrough

Adds Kamigawa flip-card support across Oracle parsing, engine resolution, permanent lifecycle, event handling, persistence, client card rendering, compatibility output, AI classification, and integration tests.

Changes

Flip effect contracts and Oracle parsing

Layer / File(s) Summary
Flip effect model and parser lowering
crates/engine/src/types/ability.rs, crates/engine/src/types/events.rs, crates/engine/src/parser/...
Adds FlipPermanent and Flipped, parses “flip this” and “flip it” instructions, preserves coin-flip parsing, and threads target handling through parser classifications.

Flip resolution and permanent lifecycle

Layer / File(s) Summary
Alternative-face application and lifecycle
crates/engine/src/game/flip.rs, crates/engine/src/game/effects/..., crates/engine/src/game/transform.rs, crates/engine/src/game/zones.rs
Applies flip-card alternative characteristics, preserves mana cost and color, prevents repeated or invalid flips, restores the normal half on zone exit, and separates flipping from transforming.
Lifecycle validation
crates/engine/src/game/flip.rs, crates/engine/src/game/effects/flip_permanent.rs
Tests one-way flipping, explicit and implicit targets, stale source handling, face-down interaction, timestamps, copying, and printed-characteristic stashing.

Persistence, event propagation, and engine classification

Layer / File(s) Summary
Persistence and rehydration
crates/engine/src/game/printed_cards.rs, crates/engine/src/game/turn_face_down.rs
Stores LayoutKind::Flip, preserves the normal half during face-down handling, restores normal mana cost and color after rehydration, and supplies flipped copiable values.
Event and analysis integration
crates/engine/src/game/{log.rs,public_state.rs,targeting.rs,trigger_index.rs,trigger_matchers.rs}, crates/engine/src/game/ability_*.rs, crates/phase-ai/src/policies/*, crates/manabrew-compat/src/lib.rs
Classifies and renders Flipped, updates dirty-state and event-subject handling, excludes flips from trigger indexing, and adds analysis, policy, and DTO classifications.

Client wire model and end-to-end coverage

Layer / File(s) Summary
Client layout and card rendering
client/src/adapter/types.ts, client/src/viewmodel/cardProps.ts, client/src/components/card/ArtCropCard.tsx
Adds LayoutKind, exposes Flipped, and uses layout-aware detection so flip cards are not treated as separately inspectable printed faces.
Real-stack integration tests
crates/engine/tests/integration/kamigawa_flip_cards.rs, crates/engine/tests/integration/main.rs
Adds end-to-end tests covering Oracle lowering, pronoun targeting, alternative characteristics, cost/color preservation, zone changes, repeated flips, and coin-flip parsing.

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

Suggested labels: enhancement

Suggested reviewers: matthewevans

🚥 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 and concisely summarizes the main change: implementing CR 710 Kamigawa flip cards in the engine.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 unit tests (beta)
  • Create PR with unit tests

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

@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: 4

🧹 Nitpick comments (1)
crates/engine/tests/integration/kamigawa_flip_cards.rs (1)

386-399: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Negative-only assertions lack a reach-guard proving flip_permanent::resolve actually ran.

a_flip_card_off_the_battlefield_cannot_flip discards resolve_trigger_body's return value and only checks !object.flipped/unchanged name/PT. If the harness silently short-circuits before reaching effects::flip_permanent::resolve (e.g. resolve_trigger_body's loop breaking on an unrelated Err), this test would pass for the wrong reason — proving nothing about the CR 710.1b/710.2 off-battlefield no-op it claims to cover. flip_permanent::resolve always pushes GameEvent::EffectResolved { kind: EffectKind::FlipPermanent, .. } on its happy path (per the provided context snippet), so asserting that event's presence would close the gap. As per path instructions, "an upstream short-circuit makes a negative pass for the wrong reason."

A second instance of the same gap exists in flipping_an_already_flipped_permanent_is_a_no_op's second flip — see consolidated comment.

✅ Proposed reach-guard
     let execute = captured_trigger_body(&runner, bushi, "Bushi Tenderfoot");
-    let _ = resolve_trigger_body(&mut runner, bushi, "Bushi Tenderfoot", &execute);
+    let emitted = resolve_trigger_body(&mut runner, bushi, "Bushi Tenderfoot", &execute);
+    assert!(
+        emitted.iter().any(|event| matches!(
+            event,
+            GameEvent::EffectResolved { kind: EffectKind::FlipPermanent, .. }
+        )),
+        "reach guard: the flip effect resolver actually ran"
+    );
 
     let object = &runner.state().objects[&bushi];
🤖 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/kamigawa_flip_cards.rs` around lines 386 -
399, Update a_flip_card_off_the_battlefield_cannot_flip to retain the events
returned or populated by resolve_trigger_body and assert that a
GameEvent::EffectResolved with EffectKind::FlipPermanent is present. Keep the
existing unchanged-object assertions, and apply the same reach-guard to the
second flip in flipping_an_already_flipped_permanent_is_a_no_op.

Source: Path instructions

🤖 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/trigger_index.rs`:
- Around line 614-619: Replace the CR 710.4/701.27b citations at all listed
sites with exact rules text or engine-local explanations that directly justify
each described behavior: no trigger dispatch in GameEvent::Flipped handling,
subject matching, scan/read classification, ability-graph handling, and coverage
bookkeeping. Update crates/engine/src/game/trigger_index.rs:614-619 and 940-942,
trigger_matchers.rs:996-998, analysis/ability_graph.rs:900-902,
ability_rw.rs:2807-2808 and 4885-4888, ability_scan.rs:1043-1049, 5259-5260,
5642-5644, 5915-5917, and 6184-6185, ability_utils.rs:4831-4832, and
coverage.rs:2355-2356; ensure each reference matches the behavior it documents
without changing implementation logic.

In `@crates/engine/src/parser/oracle_effect/imperative.rs`:
- Around line 5045-5046: Update the CR citations in the comments near the shared
“this” prefix at both referenced sites, replacing CR 201.5 with the exact
verified Comprehensive Rules section covering “this”/“this permanent”
self-deictic references, such as CR 700.7. Preserve the existing annotation
wording and implementation unchanged.

In `@crates/engine/src/types/ability.rs`:
- Around line 11150-11161: Update the documentation above Effect::Flip to either
quote the exact text of CR 110.5, 701.27a, 710.1b, 710.1c, and 710.4, or remove
those rule citations. Preserve the existing engine-design rationale about Flip
remaining distinct from Effect::Transform.

In `@crates/phase-ai/src/policies/redundancy_avoidance.rs`:
- Around line 462-464: Update the redundancy classification handling for
Effect::FlipPermanent in the relevant policy function to inspect the targeted
object’s flipped state. Return the existing redundant/no-op signal when
object.flipped is already true, while preserving the current non-redundant
behavior for unflipped targets and avoiding the unconditional None fallthrough.

---

Nitpick comments:
In `@crates/engine/tests/integration/kamigawa_flip_cards.rs`:
- Around line 386-399: Update a_flip_card_off_the_battlefield_cannot_flip to
retain the events returned or populated by resolve_trigger_body and assert that
a GameEvent::EffectResolved with EffectKind::FlipPermanent is present. Keep the
existing unchanged-object assertions, and apply the same reach-guard to the
second flip in flipping_an_already_flipped_permanent_is_a_no_op.
🪄 Autofix (Beta)

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 66e78753-5ed4-42e1-b2d3-57c0ba5404b6

📥 Commits

Reviewing files that changed from the base of the PR and between 21a53d5 and 359e3d5.

📒 Files selected for processing (33)
  • client/src/adapter/types.ts
  • client/src/components/card/ArtCropCard.tsx
  • client/src/viewmodel/cardProps.ts
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/effects/flip_permanent.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/effects/turn_face_down.rs
  • crates/engine/src/game/flip.rs
  • crates/engine/src/game/log.rs
  • crates/engine/src/game/mod.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/public_state.rs
  • crates/engine/src/game/targeting.rs
  • crates/engine/src/game/transform.rs
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/game/trigger_matchers.rs
  • crates/engine/src/game/zones.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_ir/doc.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/events.rs
  • crates/engine/tests/integration/kamigawa_flip_cards.rs
  • crates/engine/tests/integration/main.rs
  • crates/manabrew-compat/src/lib.rs
  • crates/phase-ai/src/policies/effect_classify.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs

Comment thread crates/engine/src/game/trigger_index.rs Outdated
Comment on lines +614 to +619
// CR 710.4 + CR 701.27b: flipping is its own game action, distinct from
// transforming and from turning face up/down, and no printed card
// triggers on a permanent flipping. Deliberately dispatches NO trigger
// key — folding it into `FaceOrTransform` would consult transform/
// face-change triggers for an event none of them can match.
GameEvent::Flipped { .. } => {}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -A5 -B2 '^(701\.27b|710\.1b|710\.2|710\.4)\b' docs/MagicCompRules.txt

Repository: phase-rs/phase

Length of output: 216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Top-level files:\n'
git ls-files | sed -n '1,120p'

printf '\nSearch for MagicCompRules docs:\n'
fd -a 'MagicCompRules.txt|CLAUDE.md' .

printf '\nSearch for CR annotations in the affected Rust files:\n'
rg -n 'CR 710\.4|CR 701\.27b|CR 710\.1b|CR 710\.2|CR 119|CR 120|CR 121' \
  crates/engine/src/game/trigger_index.rs \
  crates/engine/src/game/trigger_matchers.rs \
  crates/engine/src/analysis/ability_graph.rs \
  crates/engine/src/game/ability_rw.rs \
  crates/engine/src/game/ability_scan.rs \
  crates/engine/src/game/ability_utils.rs \
  crates/engine/src/game/coverage.rs

printf '\nShow the relevant docs around the flip rules if found:\n'
if fd -a 'MagicCompRules.txt' . >/tmp/mcr_path.txt; then
  MCR=$(head -n 1 /tmp/mcr_path.txt)
  echo "Using: $MCR"
  rg -n -A4 -B2 '^(701\.27b|710\.1b|710\.2|710\.4)\b' "$MCR"
fi

Repository: phase-rs/phase

Length of output: 19874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate any rules text and inspect the nearby flip rules.
fd -a 'MagicCompRules.txt' . || true
fd -a 'MagicCompRules.*' . || true

# Also inspect the repo root docs layout.
find . -maxdepth 2 -type f | sed -n '1,200p'

Repository: phase-rs/phase

Length of output: 3904


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repo root:\n'
pwd
printf '\nCandidate docs files:\n'
find . -maxdepth 3 -type f \( -iname 'MagicCompRules.txt' -o -iname 'CLAUDE.md' \) | sort

printf '\nRelevant CR text search (if docs file exists):\n'
DOC=$(find . -maxdepth 3 -type f -iname 'MagicCompRules.txt' | head -n 1 || true)
if [ -n "$DOC" ]; then
  echo "DOC=$DOC"
  rg -n -A4 -B2 '^(701\.27b|710\.1b|710\.2|710\.4)\b' "$DOC"
fi

printf '\nRelevant in-code CR comments around flip handling:\n'
sed -n '600,625p' crates/engine/src/game/trigger_index.rs
printf '\n---\n'
sed -n '932,948p' crates/engine/src/game/trigger_index.rs
printf '\n---\n'
sed -n '988,1005p' crates/engine/src/game/trigger_matchers.rs

Repository: phase-rs/phase

Length of output: 3759


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the rules document and the root guidance file, then inspect the affected comments.
git ls-files | rg '(^|/)(CLAUDE\.md|MagicCompRules\.txt)$|trigger_index\.rs$|trigger_matchers\.rs$|ability_graph\.rs$|ability_rw\.rs$|ability_scan\.rs$|ability_utils\.rs$|coverage\.rs$'

printf '\nPath hints for MagicCompRules:\n'
git ls-files | rg 'MagicCompRules|CompRules|rules' || true

Repository: phase-rs/phase

Length of output: 2126


🌐 Web query:

Magic: The Gathering Comprehensive Rules 710.4 701.27b flipping transform exact text

💡 Result:

In the Magic: The Gathering Comprehensive Rules, there are distinct sections governing Flip cards and the Transform action, which are not directly cross-referenced in the way your query implies [1][2]. Rule 710.4 addresses Flip cards, stating: 710.4. Flipping a permanent is a one-way process. Once a permanent is flipped, it’s impossible for it to become unflipped. However, if a flipped permanent leaves the battlefield, it retains no memory of its status. See rule 110.5 [3]. Rule 701.27b (or 701.28b in some versions, depending on the rulebook's numbering evolution) governs the Transform action, stating: 701.27b. Although transforming a permanent uses the same physical action as turning a permanent face up or face down, they are different game actions. Abilities that trigger when a permanent is turned face down won’t trigger when that permanent transforms, and so on [4]. Please note that "Flipping" (as in Flip cards) and "Transforming" (as in Double-faced cards) are entirely separate mechanical actions in Magic: The Gathering. Flip cards (Rule 710) are a legacy mechanic where a card is rotated 180 degrees to reveal a new ability or state, while Transform (Rule 701) applies to Double-faced cards (Rule 712) [1][4][3]. Because they are distinct, there is no shared rule between them regarding their transformation processes [1][2].

Citations:


Replace the flip citations with exact rules text or engine-local justification. CR 710.4 / 701.27b cover the flip/transform actions themselves, but these comments use them to justify trigger dispatch, subject matching, scan/read classification, and coverage bookkeeping. Reword the references so the cited text matches the behavior being described.

📍 Affects 7 files
  • crates/engine/src/game/trigger_index.rs#L614-L619 (this comment)
  • crates/engine/src/game/trigger_index.rs#L940-L942
  • crates/engine/src/game/trigger_matchers.rs#L996-L998
  • crates/engine/src/analysis/ability_graph.rs#L900-L902
  • crates/engine/src/game/ability_rw.rs#L2807-L2808
  • crates/engine/src/game/ability_rw.rs#L4885-L4888
  • crates/engine/src/game/ability_scan.rs#L1043-L1049
  • crates/engine/src/game/ability_scan.rs#L5259-L5260
  • crates/engine/src/game/ability_scan.rs#L5642-L5644
  • crates/engine/src/game/ability_scan.rs#L5915-L5917
  • crates/engine/src/game/ability_scan.rs#L6184-L6185
  • crates/engine/src/game/ability_utils.rs#L4831-L4832
  • crates/engine/src/game/coverage.rs#L2355-L2356
🤖 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/trigger_index.rs` around lines 614 - 619, Replace the
CR 710.4/701.27b citations at all listed sites with exact rules text or
engine-local explanations that directly justify each described behavior: no
trigger dispatch in GameEvent::Flipped handling, subject matching, scan/read
classification, ability-graph handling, and coverage bookkeeping. Update
crates/engine/src/game/trigger_index.rs:614-619 and 940-942,
trigger_matchers.rs:996-998, analysis/ability_graph.rs:900-902,
ability_rw.rs:2807-2808 and 4885-4888, ability_scan.rs:1043-1049, 5259-5260,
5642-5644, 5915-5917, and 6184-6185, ability_utils.rs:4831-4832, and
coverage.rs:2355-2356; ensure each reference matches the behavior it documents
without changing implementation logic.

Sources: Path instructions, MCP tools

Comment on lines +5045 to +5046
// CR 201.5: "this" / "this <permanent type>" self-deictics, nested on
// the shared "this" prefix.

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

Correct the self-deictic CR citation.

CR 201.5 covers references by an object’s name, not “this creature” / “this permanent.” Cite CR 700.7 (or another exact supporting rule) at both sites; the current annotations violate the required verified-rule citation standard. (mtg.wtf)

As per coding guidelines, “Verify every implementation of MTG game rules against the relevant Comprehensive Rules section” and annotate it with a verified CR citation.

Also applies to: 5277-5278

🤖 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/imperative.rs` around lines 5045 -
5046, Update the CR citations in the comments near the shared “this” prefix at
both referenced sites, replacing CR 201.5 with the exact verified Comprehensive
Rules section covering “this”/“this permanent” self-deictic references, such as
CR 700.7. Preserve the existing annotation wording and implementation unchanged.

Sources: Coding guidelines, Path instructions

Comment on lines +11150 to +11161
/// CR 710.4: Flip a Kamigawa flip permanent — a one-way status change
/// (CR 110.5) after which the card's alternative name, text box, type line,
/// power, and toughness apply (CR 710.1b) while it remains on the
/// battlefield.
///
/// Deliberately a sibling of [`Effect::Transform`] rather than a
/// parameterization of it: CR 701.27a restricts transforming to permanents
/// represented by double-faced cards, and CR 710.1c holds a flipped
/// permanent's color and mana cost FIXED where transforming swaps them.
/// The two live in different CR sections (701.27 vs 710) with different
/// copiable-value semantics, so the shared "turn the card over" surface is
/// not a single parameterized axis.

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A2 -B1 '^(110\.5|701\.27a|710\.1b|710\.1c|710\.4)\b' docs/MagicCompRules.txt

Repository: phase-rs/phase

Length of output: 216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repo root:\n'
pwd
printf '\nTop-level files:\n'
git ls-files | sed -n '1,120p'

printf '\nLikely rules docs:\n'
fd -HI 'MagicCompRules.txt|CompRules|ComprehensiveRules|rules' . || true

Repository: phase-rs/phase

Length of output: 5231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for the cited CR sections in the repository docs.
for path in $(fd -HI 'MagicCompRules\.txt|CompRules|ComprehensiveRules' .); do
  echo "== $path =="
  rg -n -A2 -B1 '^(110\.5|701\.27a|710\.1b|710\.1c|710\.4)\b' "$path" || true
done

Repository: phase-rs/phase

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate any rules-document file and show nearby references to the cited CR sections.
find . -maxdepth 4 \( -iname '*MagicCompRules*' -o -iname '*CompRules*' -o -path './docs/*' \) -type f | sed -n '1,200p'

Repository: phase-rs/phase

Length of output: 258


🌐 Web query:

Magic Comprehensive Rules 2026 110.5 701.27a 710.1b 710.1c 710.4 exact text

💡 Result:

As of July 23, 2026, the Magic: The Gathering Comprehensive Rules define the requested sections as follows [1][2]: 110.5. A permanent’s status is its physical state. There are four status categories, each of which has two possible values: tapped/untapped, flipped/unflipped, face up/face down, and phased in/phased out. Each permanent always has one of these values for each of these categories. 701.27a. To “flip” a permanent means to turn it over so that its other face is showing. If a permanent is turned face down, it is no longer flipped or unflipped; its face-down status (see rule 110.5) takes precedence. (Note: This rule refers to double-faced cards or other mechanics that use the flip action). 710.1b. The bottom half of a flip card contains an alternative name, text box, type line, power, and toughness. These characteristics are used only if the permanent is on the battlefield and only if the permanent is flipped. 710.1c. A flip card’s color and mana cost don’t change if the permanent is flipped. Also, any changes to it by external effects will still apply. 710.4. Flipping a permanent is a one-way process. Once a permanent is flipped, it’s impossible for it to become unflipped. However, if a flipped permanent leaves the battlefield, it retains no memory of its status. See rule 110.5.

Citations:


Quote the CR text verbatim or drop the citations
The annotations for CR 110.5, 701.27a, 710.1b, 710.1c, and 710.4 need the exact rule wording; the current text only paraphrases them. If you don’t want to quote the rules, keep the engine-design rationale and remove the CR references.

🤖 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/types/ability.rs` around lines 11150 - 11161, Update the
documentation above Effect::Flip to either quote the exact text of CR 110.5,
701.27a, 710.1b, 710.1c, and 710.4, or remove those rule citations. Preserve the
existing engine-design rationale about Flip remaining distinct from
Effect::Transform.

Source: Path instructions

Comment thread crates/phase-ai/src/policies/redundancy_avoidance.rs Outdated
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 18 card(s), 13 signature(s) (baseline: main 7a95408fe0a1)

🟢 Added (7 signatures)

  • 8 cards · ➕ ability/FlipPermanent · added: FlipPermanent (target=parent target)
    • Affected (first 3): Akki Lavarunner, Budoka Pupil, Callow Jushi (+5 more)
  • 5 cards · ➕ ability/FlipPermanent · added: FlipPermanent (target=self)
    • Affected (first 3): Bushi Tenderfoot, Erayo, Soratami Ascendant, Kuon, Ogre Ascendant (+2 more)
  • 1 card · ➕ ability/FlipPermanent · added: FlipPermanent (conditional=# of in battlefield you control creature ≥ 10, target=self)
    • Affected (first 3): Orochi Eggwatcher
  • 1 card · ➕ ability/FlipPermanent · added: FlipPermanent (conditional=# of in battlefield you control land ≥ 10, target=self)
    • Affected (first 3): Budoka Gardener
  • 1 card · ➕ ability/FlipPermanent · added: FlipPermanent (conditional=cards in hand (scoped player) = 0, target=self)
    • Affected (first 3): Nezumi Shortfang
  • 1 card · ➕ ability/FlipPermanent · added: FlipPermanent (conditional=cards in hand (you) ≥ 9, target=self)
    • Affected (first 3): Jushi Apprentice
  • 1 card · ➕ ability/RevealHand · added: RevealHand (card filter=none, player=any target)
    • Affected (first 3): Sasaya, Orochi Ascendant

🔴 Removed (6 signatures)

  • 13 cards · ➖ ability/flip · removed: flip
    • Affected (first 3): Akki Lavarunner, Budoka Pupil, Bushi Tenderfoot (+10 more)
  • 1 card · ➖ ability/flip · removed: flip (conditional=# of in battlefield you control creature ≥ 10)
    • Affected (first 3): Orochi Eggwatcher
  • 1 card · ➖ ability/flip · removed: flip (conditional=# of in battlefield you control land ≥ 10)
    • Affected (first 3): Budoka Gardener
  • 1 card · ➖ ability/flip · removed: flip (conditional=cards in hand (scoped player) = 0)
    • Affected (first 3): Nezumi Shortfang
  • 1 card · ➖ ability/flip · removed: flip (conditional=cards in hand (you) ≥ 9)
    • Affected (first 3): Jushi Apprentice
  • 1 card · ➖ ability/static_structure · removed: static_structure
    • Affected (first 3): Sasaya, Orochi Ascendant

@matthewevans

Copy link
Copy Markdown
Member

The CR 710 research here is the strongest I have seen from a contributor on this repo — every rule the implementation leans on checks out verbatim, and the flip-vs-transform separation is the right architectural call. Blocked on one red required gate plus two one-line latent-correctness gaps.

Reviewed at head 359e3d52.

🔴 Blocker

Rust lint (fmt, clippy, parser gate) is red on your own added lines. The Engine authority gate (scripts/check-engine-authorities.sh) rejects four raw keyword queries in crates/engine/src/game/flip.rs:

flip.rs:480   assert!(obj.keywords.contains(&Keyword::DoubleStrike));
flip.rs:742   !obj.keywords.contains(&Keyword::Flying),
flip.rs:750   obj.keywords.contains(&Keyword::Bushido(1)),
flip.rs:872   !copy.keywords.contains(&Keyword::DoubleStrike),

Use keywords::has_keyword(obj, &kw), or annotate // allow-raw-authority: <reason> where the raw read is genuinely structural. The PR body says "clippy clean, tests green" — both are true, and cargo clippy, cargo fmt, both Rust test shards, WASM, card-data, and frontend are all green. This gate is a separate step that clippy does not cover, so please run ./scripts/check-engine-authorities.sh origin/main locally before the next push.

Tauri compile check red is ours, not yours — the v0.35.2 release left client/src-tauri/Cargo.lock stale against --locked. Fixed on main in #6568; it will clear when you rebase. Not attributed to you.

🟡 Non-blocking

1. flip_permanent doesn't gate on its own declared authority. flip.rs:139 returns early on obj.back_face.is_none(), but the doc on is_flip_permanent (:69) declares that function "the single authority for 'is this a flip card?'". If Effect::FlipPermanent's ParentTarget arm ever binds a transform DFC, the flip applicator installs its back face without the CR 712 cost/color swap, sets flipped, and stash_flip_face re-stamps LayoutKind::Flip on the stash — permanently blocking transform::transform_permanent on that object. Unreachable across today's corpus; one line to close: if !is_flip_permanent(obj) { return Ok(()); }.

2. flipped_normal_copiable_values ignores face_down, breaking CR 708.2a. flip.rs:213 guards only on !obj.flipped. Your own turn_face_down.rs deliberately keeps the normal half in back_face for a flipped permanent (Some(flip_stash) if obj.flipped => flip_stash.clone()) and never clears flipped — which is the correct call for the zone-exit path, and the comment explaining it is excellent. But it means a Clone copying an Ixidron'd flipped Bushi Tenderfoot receives Bushi Tenderfoot's copiable values instead of CR 708.2a's "2/2 face-down creature with no text, no name, no subtypes, and no mana cost" (docs/MagicCompRules.txt:5703). One line: if obj.face_down { return None; }.

3. CR 201.5 → CR 700.7 for the "this <type>" arms (imperative.rs:5046, :5277-5278). CR 201.5 (:1322) is "text that refers to the object it's on by name" — correct for your tag("~") arm, wrong for tag("this"). CR 700.7 (:3243) is the "this [something]" rule. (Agreeing with @coderabbitai here.)

4. Sasaya, Orochi Ascendant changed in a way the PR doesn't claim. The parse diff shows ability/static_structure removed and ability/RevealHand (card filter=none, player=any target) added — the only card in the diff without a top-level FlipPermanent. Scryfall confirms the printed ability is "Reveal your hand: If you have seven or more land cards in your hand, flip Sasaya." — an activated ability whose cost is the reveal. The head parse models the reveal as the top-level effect with player = TargetFilter::Any rather than the controller, and moves the card off an honest Effect::Unimplemented { name: "static_structure" } marker onto a shape no test pins. Could you add a shape assertion (cost = reveal-your-hand, condition = ≥7 land cards in hand, effect = FlipPermanent on self) confirming this is the intended lowering?

5. The corpus test cannot catch #4. every_printed_flip_card_lowers_its_instruction_to_flip_permanent asserts only that a FlipPermanent exists somewhere in the chain, never the ability shape. Worth pinning the shape for at least the three non-uniform carriers — Sasaya's reveal cost, Initiate of Blood's delayed trigger, and Nezumi Shortfang's Then if….

6. Frontend change is untested. client/src/components/card/__tests__/ArtCropCard.test.tsx:53 still builds a back_face with no layout_kind, so reverting hasOtherPrintedFace fails nothing. A fixture with layout_kind: "Flip" asserting no DFC badge would close it. Secondary concern: "which layouts have a separately printed face" now lives in TS as layout_kind !== "Flip" and can drift from the engine's is_double_faced_permanent.

7. Comment accuracy. parse_flip_permanent_subject's doc says "the ENTIRE printed flip-card corpus (21 cards)"; Scryfall layout:flip include:extras lists 24 distinct names. It also calls Budoka Pupil / Callow Jushi / Cunning Bandit / Faithful Squire / Hired Muscle "the five ki-counter Ascendants" — those are the Betrayers ki cycle; the Ascendants are the Saviors legends.

8. One vacuous negative. kamigawa_flip_cards.rs:386-399 discards resolve_trigger_body's events and asserts only !flipped. Add the EffectResolved { kind: FlipPermanent } reach guard its sibling flipping_an_already_flipped_permanent_is_a_no_op already carries. (Agreeing with @coderabbitai.)

On @coderabbitai's other three findings, I'm overruling — no action needed. The "quote CR text verbatim or drop the citation" ask on types/ability.rs:11161 is wrong: our convention requires a mandatory description, not a quotation, and paraphrase is repo-wide. The redundancy_avoidance.rs:464 already-flipped check isn't warranted — FlipPermanent is never independently chosen (it rides a trigger, or an ability whose primary effect carries the value), so there is no activation to penalize, and parity with Transform is correct. The 13-site citation-rewording sweep is over-strict; only ability_scan.rs:6184 is loosely tagged and it isn't worth the churn.

✅ Clean

Every CR 710 assumption the implementation rests on is verbatim-correct. I grep-verified each one rather than taking the annotations on trust:

  • 710.1b (:5781) — the alternative characteristics are "used only if the permanent is on the battlefield and only if the permanent is flipped." apply_flipped_face_to_object copies exactly name / P&T / type line / text box, and flip_permanent early-returns off the battlefield. Matches.
  • 710.1c (:5783) — "A flip card's color and mana cost don't change if the permanent is flipped." This is precisely why flip.rs carries its own applicator instead of reusing printed_cards::apply_back_face_to_object, which swaps both. Correct, and mutation-verified by your own tests.
  • 710.2 (:5785) — non-battlefield zones have only normal characteristics. zones::apply_zone_exit_cleanup calls revert_flip_on_zone_exit, ordered after the CR 708.9 face-down restore because both statuses share one back_face slot. The ordering rationale in the comment is right.
  • 710.4 (:5790) — one-way, and a flipped permanent leaving the battlefield "retains no memory of its status." Repeat-flip is a silent no-op emitting no event; status clears on zone exit.
  • Copies — CR 707.2 + 707.3. flipped_normal_copiable_values returns the normal half, and 707.3's worked example is literally a flip card (Tomoya the Revealer copying Nezumi Shortfang). Documenting the second-order case as out of scope, matching CR 110.5c's Dimir Doppelganger example, is the right scoping.
  • The CR 613.7 no-new-timestamp argument holds. 613.7d/e/f/g enumerate zone entry, attachment, face up/down, and transform/convert — flipping is genuinely absent, so keeping the existing timestamp is correct rather than merely convenient.

It handles the class, not the named cards. parse_flip_permanent_subject is a composed, eof-anchored nom production over three independent surface forms (flip this [creature|permanent|artifact|enchantment|land], flip it|itself, flip ~) — not a per-card tag list, and it covers types no printed flip card uses. Nom discipline held throughout: no find/split_once/contains/starts_with dispatch, no verbatim Oracle match.

FlipPermanent as a sibling of Transform rather than a parameterization is the correct call under the categorical-boundary rule. CR 701.27a (transform, DFC-only) and CR 710 (flip, single-faced, cost and color fixed) are different rule sections with different copiable-value semantics — exactly the case where unifying would conflate boundaries the engine resolves separately.

Right seam, verified rather than assumed. flipped_normal_copiable_values hooks intrinsic_copiable_values, which I traced to confirm it is read only by copy-source paths (token_copy, become_copy, conjure, merge, augment, replacement, zones) and not by the object's own layer-1 baseline — so the hook cannot un-flip a permanent on layer flush. Widening is_double_faced_permanent to pub for manabrew-compat replaces a back_face.is_some() re-derivation with the engine's authority, which is the right direction. GameEvent::Flipped threading matches Transformed's full consumer set with no gaps, and CardPreview already guards flip cards via isFlip/flip180 — I checked GameCardPreview as a suspected sibling gap and it is correctly covered.

the_flip_it_pronoun_form_flips_its_own_source_end_to_end drives the real stack (push_to_stack + GameAction::PassPriority through apply()) with an explicit reach guard asserting the effect lowered to ParentTarget. That is a genuinely discriminating runtime test, and mod kamigawa_flip_cards; is registered at tests/integration/main.rs:598, so none of it is inert.


Recommendation: fix the authority gate, apply the two one-line guards (#1, #2), and answer #4. That is the whole remaining delta. Please don't read the gate failure as a comment on the substance — the rules work underneath it is excellent, and I would like this to land.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@matthewevans, thank you for the exceptionally thorough validation and for separating the required fixes from review churn.

I agree with your disposition of the three overruled findings: paraphrased CR descriptions are appropriate under the repository convention, FlipPermanent should remain aligned with Transform in redundancy avoidance, and the broad citation rewording sweep is not justified. I’m withdrawing those findings.

Your remaining required delta is well scoped: clear the authority gate, add the flip-layout and face-down copy guards, and pin Sasaya’s ability shape. The this <type> citation correction and the no-op test reach guard are also valid follow-ups.

🐇✨


✏️ Learnings added
Learnt from: matthewevans
URL: https://github.com/phase-rs/phase/pull/6569

Timestamp: 2026-07-23T23:08:48.811Z
Learning: In this repository, Comprehensive Rules citations require a meaningful description but do not require verbatim quotation; paraphrased descriptions are the established convention, including in `crates/engine/src/types/ability.rs`.

Learnt from: matthewevans
URL: https://github.com/phase-rs/phase/pull/6569

Timestamp: 2026-07-23T23:08:48.811Z
Learning: `Effect::FlipPermanent` is not independently selected by the AI: it resolves as part of a trigger or an ability whose primary effect carries the decision value. In `crates/phase-ai/src/policies/redundancy_avoidance.rs`, treating it like `Effect::Transform` without an already-flipped redundancy check is intentional.

Learnt from: matthewevans
URL: https://github.com/phase-rs/phase/pull/6569

Timestamp: 2026-07-23T23:08:48.811Z
Learning: Avoid broad citation-only wording sweeps when they do not affect behavior or clarity; in the CR 710 flip-card implementation, the loose CR tag in `crates/engine/src/game/ability_scan.rs` does not justify multi-file churn.

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.

…hority

Main added scripts/check-engine-authorities.sh, which rejects raw
obj.keywords.contains(&kw) in favor of game::keywords::has_keyword. Route the
four flip.rs test assertions through the authority helper. Also merges current
main (resolves the src-tauri Cargo.lock version stamp); no engine behavior
change.
@matthewevans

Copy link
Copy Markdown
Member

Strong work on a genuinely hard class — the CR 710 modelling is right and the registration is complete. Two things need resolving before this can land, one of which I verified from the full CI artifact rather than the sticky comment. Reviewed at head 3746a2d69.

✅ What's right

Effect::FlipPermanent as a sibling of Effect::Transform rather than a parameterization of it is the correct call, and your justification at types/ability.rs:11168-11185 states it well. The axis crosses CR 701.27 (transform — DFC-only, swaps mana cost and color) and CR 710 (flip — single-faced, holds both fixed). Unifying those would conflate two rule sections the engine resolves separately, which the categorical-boundary rule in CLAUDE.md explicitly forbids. This is the case where adding a variant is right and parameterizing is wrong.

Every CR number checks out against docs/MagicCompRules.txt — 710.1/1a/1b/1c/2/3/4/5 (lines 5777-5792), 701.27a/c, 707.2, 707.3, 110.5/5b/5c, 708.9, 400.7. CR 710.1c ("A flip card's color and mana cost don't change if the permanent is flipped") is verbatim and is exactly the right basis for a separate apply_flipped_face_to_object instead of reusing the DFC applicator.

I also enumerated every production back_face.is_some()/is_none() consumer, since this PR gives 21 cards a back_face they never had. All safe: the Room paths are subtype-gated, zone_pipeline.rs:2405 is behind should_transform, and the transformation_count stampers are inert because flip_permanent never increments it, so the CR 701.27f guard can't misfire. Registration is threaded through all 20+ exhaustive-match sites with none dropped.

🔴 The PR claims 19 cards; production measures 17 — and Sasaya's flip clause is silently dropped

I did not rely on the sticky comment, which truncates to "first 3". I pulled the full parse-diff.json artifact from the head CI run (run 30055830721, artifact 8583762397):

distinct cards in diff 18
cards gaining FlipPermanent 17
Sasaya in that set no
added_cards / removed_cards [] / []

Sasaya, Orochi Ascendant appears only as ∅ → RevealHand and static_structure → ∅. So it moves from the honest catch-all onto a supported effect while its flip clause disappears with no Effect::unimplemented marker. That is a coverage-honesty regression — the card now reports as handled while half its text is gone — and it makes the PR body's "19 of the 21" inaccurate.

The reason CI didn't catch it is a fixture path-divergence, and that's the part worth fixing. tests/integration/kamigawa_flip_cards.rs:434 puts Sasaya in FLIP_INSTRUCTION_CORPUS, and :466 asserts every corpus card lowers to Effect::FlipPermanent. That test is green. It calls:

parse_oracle_text(oracle, card_name, &[], &[], &[])

with empty trailing arguments, while card-data generation passes real ones. The corpus test is therefore passing on a shape production never produces — which is precisely the failure mode a corpus test exists to prevent. Please route it through the same entry point card-data gen uses; that change is more valuable than the Sasaya fix itself, because it re-arms the guard for the next card.

🔴 Turning an unflipped flip permanent face down destroys its other half permanently

effects/turn_face_down.rs:68-71 guards on Some(flip_stash) if obj.flipped — the flipped status, not "is a flip permanent". A flip card sitting unflipped is in its normal state, so it takes the _ arm and snapshot_object_base_face(obj) overwrites back_face — which is holding the alternative half — with a snapshot of the normal half whose layout_kind is hard-coded None (printed_cards.rs:744). turn_face_up.rs:44 then does obj.back_face.take().

Two consequences:

  1. The permanent can never flip again. flip.rs:139 reads let Some(alternative_face) = obj.back_face.clone() else { return Ok(()) } and silently no-ops forever — Bushi Tenderfoot's trigger becomes dead text.
  2. It becomes indistinguishable from a DFC. While face down, is_flip_permanent is false (tag gone) and flipped is false, so both arms of your new guard at transform.rs:63 go false. A "transform each creature" effect then runs the DFC applicator on a flip card and blanks the mana cost and color — the exact outcome CR 710.1c exists to prevent, and the direct contradiction of your own invariant at flip.rs:61-68 ("the half sitting in back_face always carries the tag, so no double-faced path can ever mistake a flip card for a DFC").

This is production-reachable because the PR deliberately lets flip cards through CR 712.16 — your own comment at turn_face_down.rs:57-59 names Ixidron and Cyber Conversion. The module docs at flip.rs:27-37 cover the flipped-then-face-down case only; the unflipped case is undocumented and untested.

Minimum fix: widen the preserve-arm to Some(stash) if obj.flipped || flip::is_flip_permanent(obj) and route turn-face-up through the flip-aware applicator. If you'd rather scope it down, at least re-stamp LayoutKind::Flip on the snapshot so the transform guard and the frontend DFC predicate stay honest — and extend the out-of-scope note to cover this case.

🟡 The "flip" | "flips" fallback opens a wider dispatch surface than CR 710 needs

In parser/oracle_effect/imperative.rs, the new .or_else(|| parse_utility_imperative_ast(...)) appended to the coin-flip dispatch means any unmatched "flip …" clause is now offered to every utility arm. parse_flip_permanent_subject is carefully closed over self-references and correctly keeps CR 705.1 coin flips out (flip_a_coin_still_parses_to_flip_coin at :558 pins that) — but the fallback reaching it is not. The measured blast radius is contained today, so this is a tightening rather than a live bug: call parse_flip_permanent_subject directly instead of the full utility parser.

📋 Checked and clean — no need to revisit

  • Frontend fix is non-vacuous. BackFaceData.layout_kind is skip_serializing_if = "Option::is_none" and LayoutKind serializes as "Flip", matching the new TS union at client/src/adapter/types.ts:1122-1134. hasOtherPrintedFace still returns true for a transformed DFC, so that button isn't regressed.
  • Test discrimination. The unit tests carry explicit reach guards, and the_flip_it_pronoun_form_flips_its_own_source_end_to_end drives real apply()/stack rather than calling the resolver directly. No vacuous negatives.
  • CR 110.5c second-order copy (Dimir Doppelganger) is a real gap but explicitly deferred at flip.rs:38-43 and needs a second stash slot. Fine as a deferral.

One note for whoever reviews the next CR 710 change: grep "^710.1c\." returns nothing because subrules carry no trailing period. Grep ^710.1c instead — the rule is there.


Recommendation: changes requested. The Sasaya divergence and the face-down stash overwrite are both concrete and both have small fixes. Nothing about the architecture needs revisiting — the seam, the variant choice, and the CR modelling are all correct.

- Reach-guards: the two negative flip tests now capture the resolver's events
  and assert GameEvent::EffectResolved{FlipPermanent} fired, so an upstream
  short-circuit can't make the CR 710.2 / 710.4 no-op assertions pass for the
  wrong reason.
- Self-deictic citation: "this creature"/"this permanent" is CR 700.7, not
  CR 201.5 (which governs references by name); 201.5 kept only for the "~"
  name form.
- Trigger annotations: dropped a misapplied CR 710.4 (one-way process, says
  nothing about trigger generation) from the three trigger-dispatch sites,
  replaced with the design fact (no printed flip card triggers on flipping) and
  the CR 701.27b distinct-game-action analogy.
- AI: FlipPermanent is one-way (CR 710.4), so targeting an already-flipped
  permanent is a deterministic no-op — redundancy_avoidance now penalizes it
  instead of letting the AI waste an activation (mirrors tap_redundancy).
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.

2 participants