Skip to content

Fix Dragon Man, Reformed Robot - #7030

Merged
matthewevans merged 4 commits into
phase-rs:mainfrom
JacobWoodson:card/dragon-man-reformed-robot
Aug 9, 2026
Merged

Fix Dragon Man, Reformed Robot#7030
matthewevans merged 4 commits into
phase-rs:mainfrom
JacobWoodson:card/dragon-man-reformed-robot

Conversation

@JacobWoodson

@JacobWoodson JacobWoodson commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a parse-fidelity defect on Dragon Man, Reformed Robot.

Issue: Graveyard-cast permission drops the "by discarding a card in addition to paying its other costs" additional cost (extra_cost is None instead of a CastExtraCost with mode Additional / discard-a-card), so it parses as castable from the graveyard for its normal cost with no discard required.

Files changed

  • crates/engine/src/parser/oracle_cost.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/src/parser/oracle_static/restriction.rs
  • crates/engine/src/parser/oracle_casting.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/tests/integration/dragon_man_reformed_robot_graveyard_discard_cost.rs
  • crates/engine/tests/integration/main.rs

CR references

  • CR 601.2b
  • CR 601.2f
  • CR 601.2h
  • CR 118.3
  • CR 118.9
  • CR 119.8
  • CR 701.9a

Track

Developer

LLM

Model: claude-opus-4-8
Thinking: high

Tier: Frontier

Verification

  • cargo fmt --all — passed
  • ./scripts/check-parser-combinators.sh (Gate A) — passed
  • cargo clippy-strict — passed
  • cargo test -p phase-engine — failed
  • cargo export-cards data --stats --output data/card-data.json --sidecar-dir client/public + mirror to client/public (card-data regen) — passed
  • cargo coverage — incomplete
  • cargo semantic-audit — not_run

Re-verified at chunk-2 checkpoint with fresh card-data: supported:true gap:0, semantic-audit clean. The run-time 'partial' was a Windows census-test artifact (cargo test path-separator bug), not a code defect.

Scope Expansion

None.

Validation Failures

See review/cross-check notes.

CI Failures

  • cargo test -p phase-engine: 2 failing tests, BOTH pre-existing and UNRELATED to Dragon Man -- (1) game::engine::stage2_injector_tests::the_cr_603_5_prompt_census_is_pinned_so_a_sixth_producer_is_a_counted_event and (2) game::engine::bounded_offer_conjunct_tests::f2c_the_cr_603_5_conjunct_set_has_one_production_assembler. Both live in committed crates/engine/src/game/engine.rs (bounded-offer work, commit 4b34e54 + rebases), NOT in my working-tree diff. Root cause is a Windows path-separator issue: the tests build producer-location strings via Path::display() (engine.rs:15273) which emits backslashes on Windows, but the expected vecs hardcode forward slashes (engine.rs:15386-15388, e.g. "game/effects/mod.rs:6175"). The count/partition asserts (total 37, partition 5/7/25) PASS; only the exact string-vector comparison fails on / vs . Not fixed: maintainer-owned meta-test infra with elaborate drift logs, out of scope for card verification, and CLAUDE.md multi-agent safety forbids editing files I did not author. Deterministic, so retries were futile.
  • cargo coverage: did NOT complete within the session. Initial run + retry failed at LINK with LNK1120 (347 unresolved anon..llvm. externals) from a corrupted target/tool incremental artifact (stale libengine rlib missing codegen units) -- a build-environment issue, not a source problem (clippy-strict compiled the whole engine cleanly at dev profile; oracle-gen linked fine at tool profile). Retry 2 cleared phase-engine's tool-profile artifacts (cargo clean -p phase-engine --profile tool; removed 82.6 GiB) and target/tool/incremental, then re-ran coverage; the tool-profile rebuild (engine + deps at opt-level 1) was still compiling phase-engine when the harness forced finalization. Therefore Dragon Man was NOT coverage-confirmed as supported:true gap:0. NOTE: direct inspection of the freshly regenerated data/card-data.json shows 'dragon man, reformed robot' parses fully with 0 Unimplemented effects (typed Flying keyword; SetDynamicPower CDA = Max of two Aggregate/Max ManaValue refs over noncreature permanents-you-control and noncreature graveyard cards; GraveyardCastPermission static with a Discard additional cost) -- strong evidence it is supported, but not the required coverage confirmation.
  • cargo semantic-audit: NOT run -- blocked behind the same in-progress tool-profile rebuild. semanticAuditClean could not be determined.

Summary by CodeRabbit

  • Bug Fixes

    • Improved casting validation for cards played from the graveyard or exile, including discard, sacrifice, counter-removal, and exile costs.
    • Correctly parses additional costs such as paying, discarding, sacrificing, tapping, removing, and exiling.
    • Prevents casting when required costs are unsupported or cannot be paid.
    • Supports variable exile costs and preserves source-zone restrictions for complex cost conditions.
  • Tests

    • Added coverage for graveyard casting with discard and exile costs, including successful casts and invalid-cost scenarios.

@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 2 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: 460b720a-bd0d-4557-a16c-69242b51bf60

📥 Commits

Reviewing files that changed from the base of the PR and between 2de7357 and 2ae0461.

📒 Files selected for processing (1)
  • crates/engine/src/parser/oracle_casting.rs
📝 Walkthrough

Walkthrough

The parser now models gerund additional costs, preserves them in graveyard-cast permissions, rejects unsupported riders, and validates all static extra costs. Regression and integration tests cover discard and exile costs.

Changes

Graveyard cast cost handling

Layer / File(s) Summary
Gerund cost parsing and self-flash handling
crates/engine/src/parser/oracle_cost.rs, crates/engine/src/parser/oracle_casting.rs, crates/engine/src/parser/oracle_static/mod.rs
Supported gerund phrases now produce AbilityCost values. Composite filters retain embedded zones. Unsupported self-flash costs reject the casting option.
Graveyard permission rider parsing
crates/engine/src/parser/oracle_static/restriction.rs, crates/engine/src/parser/oracle_static/tests.rs
Graveyard permissions distinguish absent, parsed, and unmodeled additional-cost riders. Parsed riders become CastExtraCost values. Unsupported riders reject the permission.
Additional cost affordability validation
crates/engine/src/game/casting.rs, crates/engine/src/game/cost_payability.rs
Static extra costs use AbilityCost::is_payable. Variable-X exile costs remain payable before X is selected.
End-to-end graveyard cast validation
crates/engine/tests/integration/*graveyard*_cost.rs, crates/engine/tests/integration/main.rs
Integration tests cover Demilich, Helbrute, and Dragon Man casts, card movement, insufficient cards, and missing discard selections.

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

Sequence Diagram(s)

sequenceDiagram
  participant OracleText
  participant GraveyardPermissionParser
  participant CastValidation
  participant GameState
  OracleText->>GraveyardPermissionParser: parse gerund additional cost
  GraveyardPermissionParser-->>GraveyardPermissionParser: create CastExtraCost or decline
  GraveyardPermissionParser->>CastValidation: provide cast permission
  CastValidation->>CastValidation: check AbilityCost::is_payable
  CastValidation->>GameState: apply mana and additional costs
  GameState-->>GameState: move cards and resolve permanent
Loading

Possibly related PRs

Suggested labels: needs-maintainer

Suggested reviewers: matthewevans, 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 identifies the primary card-specific fix addressed by the pull request.
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

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.

@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 (3)
crates/engine/src/parser/oracle_static/tests.rs (2)

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

The dispatch assertion does not check what the comment claims.

The comment states the dispatch must leave "no Unimplemented node behind for it", but the assertion only proves that some static with the same mode exists. A leftover Effect::Unimplemented for the same line would still pass. hogaak_full_card_records_restriction_and_drops_no_unimplemented_line in crates/engine/src/parser/oracle_casting.rs (Line 873) shows the pattern that actually enforces the claim.

Add the absence check, or narrow the comment.

💚 Suggested addition
     assert!(
         parsed
             .statics
             .iter()
             .any(|parsed_def| parsed_def.mode == def.mode),
         "full Oracle dispatch must route Dragon Man's line to the discard-cost \
          permission, got {:?}",
         parsed.statics
     );
+    assert!(
+        !parsed.abilities.iter().any(|ability| matches!(
+            ability.effect.as_ref(),
+            crate::types::ability::Effect::Unimplemented { .. }
+        )),
+        "the graveyard line must not also leave an Unimplemented effect behind: {:?}",
+        parsed.abilities
+    );
🤖 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_static/tests.rs` around lines 14268 - 14286,
Strengthen the assertion in the full Oracle dispatch test around
parse_oracle_text so it verifies that no Unimplemented node remains for the
tested line, following the absence-check pattern in
hogaak_full_card_records_restriction_and_drops_no_unimplemented_line. Keep the
existing mode-dispatch assertion and ensure the comment accurately reflects both
requirements.

Source: Path instructions


14339-14342: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the exile filter contents, not just is_some().

filter.is_some() passes for any filter, including a bare TargetFilter::Any. The rules-relevant part of Demilich's rider is that only instant and sorcery cards can pay it. A regression that widened the filter to "any card" would keep this test green.

Assert the disjunction legs carry TypeFilter::Instant and TypeFilter::Sorcery, as cost_exile_self_and_count_other_you_control_recovers_count_and_filter does in crates/engine/src/parser/oracle_cost.rs (Line 2120).

🤖 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_static/tests.rs` around lines 14339 - 14342,
Strengthen the Demilich exile-cost assertion around filter by matching its
contents rather than only checking filter.is_some(). Verify the filter’s
disjunction includes TypeFilter::Instant and TypeFilter::Sorcery, following the
assertion pattern used by
cost_exile_self_and_count_other_you_control_recovers_count_and_filter, while
rejecting a bare TargetFilter::Any.

Source: Path instructions

crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs (1)

131-159: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The blocked case does not prove the instant/sorcery filter is enforced.

The blocked scenario has three eligible cards and the allowed scenario has four. Both outcomes are explained by raw count alone. If the exile filter regressed to "any card", the blocked scenario would still contain only three non-Demilich cards, so the test stays green while the rules fidelity is lost.

Add a third scenario: three instant/sorcery cards plus one ineligible graveyard card (for example a creature card). The cast must still be blocked. That isolates the filter from the count.

🤖 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/demilich_helbrute_graveyard_exile_cost.rs`
around lines 131 - 159, Extend
demilich_graveyard_cast_blocked_without_four_exilable_cards with a third
scenario containing exactly three instant/sorcery cards and one ineligible card
such as a creature. Give it the same phase, life, and mana setup, then assert
can_cast_object_now is false to verify the instant/sorcery filter rather than
raw graveyard count.

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/casting.rs`:
- Around line 13848-13851: Add a focused test covering the cast-permission path
around static_extra and is_payable for an Exile cost with a variable count such
as EXILE_COST_X. Verify that the cost is payable for every eligible exile-card
choice, while preserving the existing behavior for ineligible choices.

In `@crates/engine/src/parser/oracle_casting.rs`:
- Around line 236-241: Update the self-flash parsing arm around
parse_gerund_cost to recognize both “its other costs” and “their other costs”
closers, with an optional “paying ” prefix, matching the additional-cost
combinator used by parse_cast_permission_additional_cost_rider. De-gerund the
captured cost before calling parse_gerund_cost, and preserve the existing
Unimplemented handling and option.cost flow.

In `@crates/engine/src/parser/oracle_cost.rs`:
- Around line 87-107: Update parse_gerund_cost to use TextPair (or the module’s
existing nom_on_lower pattern) so gerund matching remains case-insensitive while
the matched remainder retains its original casing. Pass that original-cased
remainder to parse_oracle_cost, preserving subtype values such as “Vehicle” for
downstream filters.

In `@crates/engine/src/parser/oracle_static/tests.rs`:
- Around line 14339-14342: The tests do not verify that the exile-cost filters
restrict cards correctly. In
crates/engine/src/parser/oracle_static/tests.rs#L14339-L14342, replace the
presence check with assertions that the disjunction legs use TypeFilter::Instant
and TypeFilter::Sorcery; in
crates/engine/src/parser/oracle_static/tests.rs#L14391-L14400, bind the filter
instead of discarding it and assert TypeFilter::Creature with
FilterProp::Another; in
crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs#L131-L159,
add three eligible instant/sorcery cards plus one ineligible graveyard card and
assert casting remains blocked.

---

Nitpick comments:
In `@crates/engine/src/parser/oracle_static/tests.rs`:
- Around line 14268-14286: Strengthen the assertion in the full Oracle dispatch
test around parse_oracle_text so it verifies that no Unimplemented node remains
for the tested line, following the absence-check pattern in
hogaak_full_card_records_restriction_and_drops_no_unimplemented_line. Keep the
existing mode-dispatch assertion and ensure the comment accurately reflects both
requirements.
- Around line 14339-14342: Strengthen the Demilich exile-cost assertion around
filter by matching its contents rather than only checking filter.is_some().
Verify the filter’s disjunction includes TypeFilter::Instant and
TypeFilter::Sorcery, following the assertion pattern used by
cost_exile_self_and_count_other_you_control_recovers_count_and_filter, while
rejecting a bare TargetFilter::Any.

In `@crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs`:
- Around line 131-159: Extend
demilich_graveyard_cast_blocked_without_four_exilable_cards with a third
scenario containing exactly three instant/sorcery cards and one ineligible card
such as a creature. Give it the same phase, life, and mana setup, then assert
can_cast_object_now is false to verify the instant/sorcery filter rather than
raw graveyard count.
🪄 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: b0da4f2d-c633-4a71-8c1a-1378ea488b02

📥 Commits

Reviewing files that changed from the base of the PR and between 152b368 and 4e83f70.

📒 Files selected for processing (9)
  • crates/engine/src/game/casting.rs
  • crates/engine/src/parser/oracle_casting.rs
  • crates/engine/src/parser/oracle_cost.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/src/parser/oracle_static/restriction.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs
  • crates/engine/tests/integration/dragon_man_reformed_robot_graveyard_discard_cost.rs
  • crates/engine/tests/integration/main.rs

Comment thread crates/engine/src/game/casting.rs
Comment thread crates/engine/src/parser/oracle_casting.rs Outdated
Comment thread crates/engine/src/parser/oracle_cost.rs
Comment on lines +14339 to +14342
assert!(
filter.is_some(),
"the exile cost must carry the instant/sorcery card filter, got {filter:?}"
);

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 | 🟡 Minor | ⚡ Quick win

No test asserts the exile cost's filter, at any layer. The parser tests check count, zone, and mode, and the integration test varies only the number of eligible cards. A regression that widened the exile filter to "any card" — or that dropped FilterProp::Another — keeps every assertion in this PR green, while letting an ineligible card, or Helbrute itself, pay the cost.

  • crates/engine/src/parser/oracle_static/tests.rs#L14339-L14342: replace filter.is_some() with assertions that the disjunction legs carry TypeFilter::Instant and TypeFilter::Sorcery.
  • crates/engine/src/parser/oracle_static/tests.rs#L14391-L14400: bind the filter instead of discarding it with .., then assert TypeFilter::Creature and FilterProp::Another.
  • crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs#L131-L159: add a scenario with three instant/sorcery cards plus one ineligible graveyard card, and assert the cast stays blocked.
📍 Affects 2 files
  • crates/engine/src/parser/oracle_static/tests.rs#L14339-L14342 (this comment)
  • crates/engine/src/parser/oracle_static/tests.rs#L14391-L14400
  • crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs#L131-L159
🤖 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_static/tests.rs` around lines 14339 - 14342,
The tests do not verify that the exile-cost filters restrict cards correctly. In
crates/engine/src/parser/oracle_static/tests.rs#L14339-L14342, replace the
presence check with assertions that the disjunction legs use TypeFilter::Instant
and TypeFilter::Sorcery; in
crates/engine/src/parser/oracle_static/tests.rs#L14391-L14400, bind the filter
instead of discarding it and assert TypeFilter::Creature with
FilterProp::Another; in
crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs#L131-L159,
add three eligible instant/sorcery cards plus one ineligible graveyard card and
assert casting remains blocked.

Source: Path instructions

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Generated for head 2ae0461bcfab6498c5f74038eb9a811806a31eaa.

Parse changes introduced by this PR · 14 card(s), 9 signature(s) (baseline: main de0b04ecbe76)

🟢 Added (4 signatures)

  • 10 cards · ➕ static/GraveyardCastPermission(Cast,unlimited,extra_cost=additional) · added: GraveyardCastPermission(Cast,unlimited,extra_cost=additional) (affects=self)
    • Affected (first 3): A-Demilich, Alien Symbiosis, Demilich (+7 more)
  • 1 card · ➕ static/GraveyardCastPermission(Cast,once_per_turn,extra_cost=additional) · added: GraveyardCastPermission(Cast,once_per_turn,extra_cost=additional) (affects=creature)
    • Affected (first 3): Kotis, Sibsig Champion
  • 1 card · ➕ static/GraveyardCastPermission(Cast,once_per_turn,extra_cost=additional) · added: GraveyardCastPermission(Cast,once_per_turn,extra_cost=additional) (affects=permanent, conditional=has 8+ counters)
    • Affected (first 3): Exploration Broodship
  • 1 card · ➕ ability/static_structure · added: static_structure
    • Affected (first 3): Maestros Ascendancy

🔴 Removed (4 signatures)

  • 10 cards · ➖ static/GraveyardCastPermission(Cast,unlimited) · removed: GraveyardCastPermission(Cast,unlimited) (affects=self)
    • Affected (first 3): A-Demilich, Alien Symbiosis, Demilich (+7 more)
  • 1 card · ➖ static/GraveyardCastPermission(Cast,once_per_turn) · removed: GraveyardCastPermission(Cast,once_per_turn) (affects=creature)
    • Affected (first 3): Kotis, Sibsig Champion
  • 1 card · ➖ static/GraveyardCastPermission(Cast,once_per_turn) · removed: GraveyardCastPermission(Cast,once_per_turn) (affects=instant or sorcery)
    • Affected (first 3): Maestros Ascendancy
  • 1 card · ➖ static/GraveyardCastPermission(Cast,once_per_turn) · removed: GraveyardCastPermission(Cast,once_per_turn) (affects=permanent, conditional=has 8+ counters)
    • Affected (first 3): Exploration Broodship

🔵 Support status (1 signature)

  • 1 card · ↕️ cost/CastingOption:AsThoughHadFlash · support: unsupportedsupported
    • Affected (first 3): Tegwyll's Scouring

11 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.
New cards in head: 138.

@matthewevans matthewevans self-assigned this Aug 5, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Request changes — compound gerund additional costs are not safe to mark supported.

🔴 Blocker

[HIGH] Compound gerund additional costs remain falsely payable. Evidence: crates/engine/src/parser/oracle_static/restriction.rs:2105-2110 rejects only a top-level AbilityCost::Unimplemented; the cost parser can retain an unimplemented discard/sacrifice conjunct inside Composite, crates/engine/src/game/cost_payability.rs:641-650,706-708 recursively treats that node as payable, and payment then fails at crates/engine/src/game/costs.rs:1341-1344. The current parse-diff names Demonic Embrace and Wickerfolk Indomitable, while its 14 cards / 9 signatures also exceeds the PR narrative. Why it matters: CR 601.2f and CR 601.2h (verified in docs/MagicCompRules.txt:2468,2472) require additional costs to be locked and fully payable; this path advertises a cast that cannot make a partial/unpayable payment. Suggested fix: recursively reject the permission until every composite conjunct is concrete, with runtime tests that exercise both compound components.

[MED] Gerund parsing lowercases subtype identity. Evidence: crates/engine/src/parser/oracle_cost.rs passes a lowercased remainder through the cost parsing path. Why it matters: subtype filters require the original spelling/canonical identity. Suggested fix: match case-insensitively with TextPair (or the existing equivalent) while passing the original-cased remainder downstream.

[MED] The added tests do not prove the claimed filters or dispatch completeness. Evidence: the new parser tests do not assert the actual filters/Another, and do not directly assert no Effect::Unimplemented remains for the dispatched line. Why it matters: a widened filter or partial parser fallback can remain green. Suggested fix: assert precise filter structure plus ineligible cases and a direct no-Unimplemented dispatch assertion.

Recommendation: request changes.

@matthewevans matthewevans added the bug Bug fix label Aug 5, 2026
@matthewevans

Copy link
Copy Markdown
Member

Correction to my current-head changes-requested review: I retract only the MED claim that gerund lowercasing breaks subtype identity. parse_gerund_cost delegates type/subtype recognition through the case-insensitive canonicalization path (oracle_util::parse_subtype, used by oracle_target), so that cited Vehicle case is not a present defect. The HIGH compound-gerund nested-Unimplemented/false-supported-cost blocker and the filter/no-Unimplemented test-discrimination requirements remain unchanged.

@matthewevans matthewevans removed their assignment Aug 5, 2026
@JacobWoodson JacobWoodson changed the title Partial: Dragon Man, Reformed Robot Fix Dragon Man, Reformed Robot Aug 5, 2026
@matthewevans matthewevans removed the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 6, 2026
@matthewevans matthewevans self-assigned this Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 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/src/game/cost_payability.rs`:
- Around line 1256-1277: The current unit test only checks
AbilityCost::is_payable and bypasses the real cast/payment flow. Add an
integration test under crates/engine/tests/integration/ that casts the ability
with X=0 when no eligible cards exist, then covers positive-X announcement and
selection with an eligible graveyard card, verifying payment and card movement;
register the test module in integration/main.rs.
🪄 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: 1b5cc059-5987-481d-ab6c-420c7b315493

📥 Commits

Reviewing files that changed from the base of the PR and between de0b04e and 2de7357.

📒 Files selected for processing (10)
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/cost_payability.rs
  • crates/engine/src/parser/oracle_casting.rs
  • crates/engine/src/parser/oracle_cost.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/src/parser/oracle_static/restriction.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs
  • crates/engine/tests/integration/dragon_man_reformed_robot_graveyard_discard_cost.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/tests/integration/dragon_man_reformed_robot_graveyard_discard_cost.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/tests/integration/demilich_helbrute_graveyard_exile_cost.rs
  • crates/engine/src/parser/oracle_cost.rs
  • crates/engine/src/parser/oracle_static/restriction.rs
  • crates/engine/src/parser/oracle_static/tests.rs

Comment on lines +1256 to +1277
#[test]
fn variable_exile_cost_is_payable_at_x_zero() {
let mut scenario = GameScenario::new();
let source = scenario.add_creature(P0, "Harvest Pyre", 0, 1).id();
let cost = AbilityCost::Exile {
count: EXILE_COST_X,
zone: Some(Zone::Graveyard),
filter: Some(TargetFilter::Typed(TypedFilter::new(TypeFilter::Instant))),
};

assert!(
cost.is_payable(&scenario.state, P0, source),
"X exile costs are payable at X=0 before any eligible card is selected"
);

scenario.add_spell_to_graveyard(P0, "Lightning Bolt", true);
assert!(
cost.is_payable(&scenario.state, P0, source),
"X exile costs stay payable when eligible cards can set X above zero"
);
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Exercise the real cast and payment path.

Both assertions call AbilityCost::is_payable, so both pass through the new unconditional sentinel branch. The test does not verify X announcement, positive-X selection, actual exile payment, or card movement.

Add an integration test under crates/engine/tests/integration/ and register it in crates/engine/tests/integration/main.rs. Cover X=0 with no eligible cards and positive X through the real casting pipeline.

As per path instructions, “Tests belong under crates/engine/tests/integration/ and must be registered in integration/main.rs.”

🤖 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/cost_payability.rs` around lines 1256 - 1277, The
current unit test only checks AbilityCost::is_payable and bypasses the real
cast/payment flow. Add an integration test under
crates/engine/tests/integration/ that casts the ability with X=0 when no
eligible cards exist, then covers positive-X announcement and selection with an
eligible graveyard card, verifying payment and card movement; register the test
module in integration/main.rs.

Source: Path instructions

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes requested — the current head still accepts compound additional-cost riders that contain an unimplemented mandatory sub-cost, lacks end-to-end X-exile coverage, and fails required Rust lint.

[HIGH] The present-but-unmodeled rider guard is only shallow. Evidence: crates/engine/src/parser/oracle_cost.rs:174-185 constructs AbilityCost::Composite for comma/and costs, but crates/engine/src/parser/oracle_static/restriction.rs:2105-2110 and crates/engine/src/parser/oracle_casting.rs:253-258 reject only a top-level AbilityCost::Unimplemented. A compound rider with one unsupported child is therefore accepted; crates/engine/src/game/cost_payability.rs:644-658 recursively visits composite legs and :710-715 treats Unimplemented as payable. Why it matters: a required additional cost can be represented as supported while an unsupported component is silently admitted. Suggested fix: use a recursive supported-cost guard before accepting either rider and add a compound-gerund regression that proves the whole permission is declined.

[MED] The X-exile coverage stops at the pure payability helper rather than the spell-cast pipeline. Evidence: crates/engine/src/game/cost_payability.rs:1257-1276 calls AbilityCost::is_payable, while real casting derives the X upper bound at crates/engine/src/game/casting_costs.rs:7801-7852, concretizes it at :5370-5407, and produces the graveyard selection prompt at :14137-14213. Why it matters: a disconnect in announcement, concretization, or payment can ship undetected. Suggested fix: add production cast-flow coverage for X=0 with no eligible card and positive-X selection/payment/movement.

[MED] Required Rust lint is red on this head. Evidence: GitHub run 31292692855, job 93192500218, reports clippy::question_mark for crates/engine/src/parser/oracle_casting.rs:232-248. Why it matters: CI cannot pass. Suggested fix: apply Clippy's ?-based Option propagation in that parser branch.

The parse-diff receipt is current for 2de7357c007faf2653489e65826f74a5e1011426 (14 cards / 9 signatures); the named surface is otherwise explained.

@matthewevans matthewevans removed their assignment Aug 9, 2026
@matthewevans matthewevans self-assigned this Aug 9, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes requested — the parser now advertises 12 graveyard-cast cards with an additional cost, but unsupported nested cost riders can still pass the legality checks and be silently ignored at payment.

[HIGH] parse_additional_cost can return AbilityCost::Unimplemented inside a composite additional-cost rider (crates/engine/src/parser/oracle_cost.rs:160-185), while the acceptance guards only reject a top-level unimplemented cost (crates/engine/src/parser/oracle_static/restriction.rs:2105-2110, crates/engine/src/parser/oracle_casting.rs:251-255, and crates/engine/src/game/cost_payability.rs:644-658,710-715). A conjunction such as a supported mana payment plus an unsupported rider is therefore considered castable and the unsupported rider can be skipped. That makes the added GraveyardCastPermission(... extra_cost=additional) surface rules-incorrect. Make the nested-cost walk authoritative for parser acceptance, cast legality, and payment so any unsupported child fails the entire additional cost; add a regression with a supported child plus an unsupported sibling.

[MED] The X-valued graveyard-exile additional-cost coverage stops at is_payable. It does not drive the actual announcement, X concretization, selection, and payment pipeline. Add a cast-pipeline regression that proves the selected number of graveyard cards is exiled and that the resulting X value/payment state is carried through resolution.

The current parse-diff artifact is correctly bound to 2ae0461bcfab6498c5f74038eb9a811806a31eaa and exposes the affected card class; please retain it on the next head and request re-review after the strict-failure and pipeline regressions are in place.

@matthewevans matthewevans removed their assignment Aug 9, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified current head: additional-cost parsing/payability fixes and the Clippy follow-up are covered by green CI.

@matthewevans
matthewevans added this pull request to the merge queue Aug 9, 2026
Merged via the queue into phase-rs:main with commit cb1484b Aug 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants