Skip to content

fix(parser): parse "Activate only during an opponent's upkeep" (Trade…#6284

Open
keloide wants to merge 2 commits into
phase-rs:mainfrom
keloide:claude/phase-developer-track-owmkmw
Open

fix(parser): parse "Activate only during an opponent's upkeep" (Trade…#6284
keloide wants to merge 2 commits into
phase-rs:mainfrom
keloide:claude/phase-developer-track-owmkmw

Conversation

@keloide

@keloide keloide commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Trade Caravan's activated ability Remove two currency counters from ~: Untap target basic land. Activate only during an opponent's upkeep. dropped its timing restriction to Effect::Unimplemented, leaving the ability activatable at any time. parse_activation_during_role_gate handled during an opponent's turn and during your upkeep but had no arm for during an opponent's upkeep. This adds that arm by composing existing building blocks rather than proliferating a new DuringOpponents* restriction sibling.

Implementation method (required)

Method: /engine-implementer (gates applied inline: add-engine-variant existence/parameterization/boundary gate + mandatory final review-impl)

CR references

  • CR 503.1 — the upkeep step (new ParsedCondition::IsDuringUpkeep predicate + its evaluator arm).
  • CR 102.1 — the active player is the player whose turn it is (opponent scope via Not(IsYourTurn)).
  • CR 602.5b — an activated ability's use restriction persists across control changes.

All three grep-verified against docs/MagicCompRules.txt.

Design

Rather than add a DuringOpponentsUpkeep sibling to ActivationRestriction (which already expresses opponent-turn scope via RequiresCondition { Not(IsYourTurn) }, not a dedicated DuringOpponentsTurn variant), the fix composes:

  • ParsedCondition::IsDuringUpkeep — a scope-free upkeep-step predicate, orthogonal to every existing ParsedCondition variant (there was no prior phase predicate).
  • during an opponent's upkeepRequiresCondition { And([Not(IsYourTurn), IsDuringUpkeep]) }, reusing the same opponent-turn composition idiom as the existing bare opponent-turn gate.

add-engine-variant gate result for ParsedCondition::IsDuringUpkeep: Stage 1 DOES_NOT_EXIST (no phase predicate in ParsedCondition), Stage 2 EXTEND_OK (orthogonal — not a sibling cluster), Stage 3 WITHIN_SECTION (CR 503, upkeep step).

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 — clean.

  • cargo clippy -p engine --all-targets -- -D warnings — clean, 0 warnings (full compile; confirms every exhaustive ParsedCondition match is satisfied).

  • cargo test -p engine --libok. 17440 passed; 0 failed; 6 ignored.

  • cargo test -p engine --test integrationok. 3676 passed; 0 failed; 2 ignored.

  • New tests: activated_ability_opponents_upkeep_restriction_composes_scope_and_step (parser building block, drives production parse(), asserts composed restriction + zero Effect::Unimplemented) and opponents_upkeep_activation_condition_gates_on_step_and_scope (runtime, drives real evaluate_condition across the full turn-scope × step matrix) — both pass.

  • cargo coverage / cargo semantic-auditdeferred to CI: both require a full MTGJSON card-data.json export (156 MB+ multi-request per-set fetch) that isn't feasible in this environment (only a test fixture is present locally). Direct evidence instead: the parsed AST for Trade Caravan now contains zero Effect::Unimplemented and the correct RequiresCondition { And([Not(IsYourTurn), IsDuringUpkeep]) } restriction. Since card_face_has_unimplemented_parts (game/coverage.rs) derives supported precisely from the absence of Unimplemented parts, Trade Caravan is now supported: true, gap_count: 0.

Gate A

Gate A PASS head=c5024c97678be3ffdcbac252e235cd8ed145fdab base=ee061e5c7485619644dc6089976b94a1d29a0202

Anchored on

  • crates/engine/src/parser/oracle.rs:6792value(opponents_turn_activation_restriction(), alt((tag("during an opponent's turn"), tag("during an opponents turn")))): the same value(computed_restriction, alt((tag, tag))) during-role arm the new opponent's-upkeep arm mirrors (combinator family, naming, placement).
  • crates/engine/src/parser/oracle.rs:6803value(ActivationRestriction::DuringYourUpkeep, alt((tag("during your upkeep"), tag("during their upkeep")))): the sibling upkeep-step during-role arm, immediately adjacent, establishing the apostrophe/possessive variant pattern reused verbatim.
  • (supporting) crates/engine/src/game/restrictions.rs:1520 ParsedCondition::IsYourTurn => state.active_player == player and restrictions.rs:1066 CastingRestriction::DuringOpponentsUpkeep => state.active_player != player && state.phase == Phase::Upkeep — the evaluator arms the new IsDuringUpkeep => state.phase == Phase::Upkeep arm mirrors.

Final review-impl

Final review-impl PASS head=c5024c97678be3ffdcbac252e235cd8ed145fdab

Claimed parse impact

  • Trade Caravan (removed from docs/parser-misparse-backlog.md root cause 28)

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added an IsDuringUpkeep activation condition predicate to support abilities restricted to an opponent’s upkeep.
  • Bug Fixes
    • Activation timing now correctly gates on upkeep vs non-upkeep and your-turn vs opponent-turn, including the common “opponents upkeep” misspelling.
    • Improved memo-safety classification for upkeep-related conditions.
  • Tests
    • Added unit tests covering opponent-upkeep restriction parsing and evaluation behavior (including nested ability assertions).
  • Documentation
    • Updated the parser misparse backlog entry for the resolved timing/ordinal restriction drop.

… Caravan)

Trade Caravan's activated ability "Remove two currency counters from ~:
Untap target basic land. Activate only during an opponent's upkeep." dropped
its timing restriction to `Effect::Unimplemented`, leaving the ability
activatable at any time.

`parse_activation_during_role_gate` handled "during an opponent's turn" and
"during your upkeep" but not "during an opponent's upkeep". Rather than add a
`DuringOpponents*` sibling to `ActivationRestriction` (which expresses
opponent-turn scope via `RequiresCondition { Not(IsYourTurn) }`, not a
dedicated variant), compose from building blocks:

- Add `ParsedCondition::IsDuringUpkeep` (CR 503.1) — a scope-free upkeep-step
  predicate, orthogonal to every existing `ParsedCondition` variant.
- Map "during an opponent's upkeep" to
  `RequiresCondition { And([Not(IsYourTurn), IsDuringUpkeep]) }` (CR 602.5b +
  CR 102.1 + CR 503.1), reusing the existing opponent-turn composition idiom.
- Evaluate `IsDuringUpkeep` against `state.phase == Phase::Upkeep`; classify it
  memo-safe in the AI condition filter (reads only apply()-constant state).

Removes Trade Caravan from the parser-misparse backlog (root cause 28).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwvJgpQpXqutChWLLLGB8E
@keloide
keloide requested a review from matthewevans as a code owner July 21, 2026 16:05

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements support for the 'Activate only during an opponent's upkeep' restriction (specifically for the card Trade Caravan) by introducing the ParsedCondition::IsDuringUpkeep condition, updating the condition evaluator, adding parser support, and updating the misparse backlog. The review feedback highlights a violation of Rule R1, where verbatim string matching is used in the parser instead of decomposing the phrase into modular, reusable nom combinators.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +6784 to +6790
value(
opponents_upkeep_activation_restriction(),
alt((
tag("during an opponent's upkeep"),
tag("during an opponents upkeep"),
)),
),

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.

medium

Avoid verbatim string equality for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead of adding verbatim string variants to the alt combinator, decompose compound phrases into modular, reusable parsers for constituent parts (e.g., the preposition, determiner, subject, and phase) and compose them using idiomatic combinator aggregates to prevent combinatorial explosion and improve maintainability.

        value(
            opponents_upkeep_activation_restriction(),
            preceded(
                tag("during "),
                tuple((
                    alt((tag("an"), tag("each"))),
                    tag(" "),
                    alt((tag("opponent's"), tag("opponents"))),
                    tag(" upkeep"),
                )),
            ),
        ),
References
  1. L2. Sibling coverage: If a parser arm or string was extended, ensure plural, possessive, negated, and other variants are covered. (link)
  2. Avoid verbatim string equality for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts and compose them using idiomatic combinator aggregates to prevent combinatorial explosion and improve maintainability.

@matthewevans matthewevans self-assigned this Jul 21, 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.

[HIGH] This engine-mechanic change has no verifiable higher-level frontier-model planning provenance. Evidence: the PR claims /engine-implementer but records only generic Claude Code generation, with no planning-model identity or frontier-level review evidence. Why it matters: maintainer policy requires a higher-level frontier model for planning any engine mechanic; an unverified inline claim cannot satisfy that gate. Suggested fix: redo the planning/review cycle with the required frontier model and attach the model provenance, current-head Gate A, and final review-impl result.

[MED] The runtime test bypasses the production activation gate. Evidence: crates/engine/src/game/restrictions.rs:3099-3144 constructs ParsedCondition and calls evaluate_condition directly, while real activation legality flows through activation_restriction_applies at restrictions.rs:994-996 and check_activation_restrictions. Why it matters: the test proves only the new leaf evaluator, not that Trade Caravan's parsed ActivationRestriction::RequiresCondition blocks/permits the real activation path. Suggested fix: drive check_activation_restrictions (or the scenario activation pipeline) with Trade Caravan's parsed restriction for opponent-upkeep allowed, opponent-non-upkeep denied, and controller-upkeep denied cases.

[MED] The new Oracle phrase is encoded as two full-string alternatives instead of composed grammar. Evidence: crates/engine/src/parser/oracle.rs:6785-6789; the existing casting parser decomposes the same possessive axis in oracle_casting.rs:684-715. Why it matters: adding a second full phrase makes sibling forms accumulate as cartesian strings rather than sharing the opponent-possessive and phase axes. Suggested fix: compose during , the opponent possessive variants, and upkeep with nested nom combinators, then cover both supported possessive spellings.

@matthewevans matthewevans removed their assignment Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main ebcf52bfd924)

🔴 Removed (1 signature)

  • 1 card · ➖ ability/activate · removed: activate
    • Affected (first 3): Trade Caravan

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an IsDuringUpkeep predicate, parses opponent-upkeep activation timing into composed restrictions, evaluates it against game phase and turn scope, and adds parser and restriction tests.

Changes

Opponent Upkeep Activation

Layer / File(s) Summary
Condition model and evaluation
crates/engine/src/types/ability.rs, crates/engine/src/game/restrictions.rs, crates/engine/src/ai_support/filter.rs
Adds IsDuringUpkeep, evaluates it against Phase::Upkeep, classifies it as memo-safe, and tests opponent-turn and upkeep-step gating.
Parser integration and validation
crates/engine/src/parser/oracle.rs, crates/engine/src/parser/oracle_tests.rs, docs/parser-misparse-backlog.md
Parses opponent-upkeep wording, composes Not(IsYourTurn) with IsDuringUpkeep, validates the Trade Caravan parse, and updates the related backlog entry.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RulesText
  participant OracleParser
  participant ActivationRestriction
  participant RestrictionEvaluator
  participant GameState
  RulesText->>OracleParser: parse opponent-upkeep activation timing
  OracleParser->>ActivationRestriction: create Not(IsYourTurn) and IsDuringUpkeep
  ActivationRestriction->>RestrictionEvaluator: evaluate composed restriction
  RestrictionEvaluator->>GameState: read turn and phase
  GameState-->>RestrictionEvaluator: current scope and phase
  RestrictionEvaluator-->>ActivationRestriction: allow or deny activation
Loading

Suggested labels: bug

Suggested reviewers: matthewevans, ntindle

🚥 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 accurately summarizes the main change: adding parser support for "Activate only during an opponent's upkeep."
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate 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: 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.rs`:
- Around line 6784-6790: Update opponents_upkeep_activation_restriction so
“opponent” is determined by team membership rather than comparing the activator
directly with state.active_player; use the existing active-team/opponent
predicate and preserve both accepted upkeep phrases. Add a Two-Headed Giant
regression case covering a non-active teammate during that team’s upkeep, and
annotate the rule implementation with the verified applicable Comprehensive
Rules citation and description.

In `@docs/parser-misparse-backlog.md`:
- Around line 5074-5076: The summary table’s entry for root cause 28 is stale.
Update its ranked count from 16 to 13 to match the heading “Trigger/activation
timing or ordinal restriction dropped,” without changing the heading or other
prioritization data.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46c9725e-90c8-46ba-bb54-04765abab270

📥 Commits

Reviewing files that changed from the base of the PR and between 82343a6 and c5024c9.

📒 Files selected for processing (6)
  • crates/engine/src/ai_support/filter.rs
  • crates/engine/src/game/restrictions.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/src/types/ability.rs
  • docs/parser-misparse-backlog.md

Comment thread docs/parser-misparse-backlog.md
@matthewevans matthewevans self-assigned this Jul 21, 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.

Blocked — the new head still has a team-scope rules defect, and the original parser and runtime-test gaps remain.

🔴 Blocker

[HIGH] “Opponent’s upkeep” treats a teammate as an opponent in shared-team turns. Evidence: crates/engine/src/parser/oracle.rs:7027-7034 emits Not(IsYourTurn), while crates/engine/src/game/restrictions.rs:1520-1523 defines IsYourTurn solely as active_player == player; in Two-Headed Giant, a non-active teammate can act during its team’s upkeep, so that composition becomes true even though teammates are not opponents. The engine’s team-aware authority is crates/engine/src/game/players.rs:169-172topology::is_opponent. Why it matters: the ability is illegally activatable during its controller’s teammate’s upkeep. Suggested fix: replace the active-player inequality representation for the “opponent’s turn” class with a team-aware condition at the ParsedCondition/restriction evaluator seam, migrate the existing opponent-turn parser users, and add a 2HG production-gate regression that denies the teammate case while allowing the opposing team’s upkeep.

[MED] The parser still encodes the phrase as two complete strings. Evidence: crates/engine/src/parser/oracle.rs:6783-6790; the existing composed possessive/phase grammar is in crates/engine/src/parser/oracle_casting.rs:684-717. Why it matters: apostrophe and phase axes will accumulate as full-string permutations instead of sharing grammar. Suggested fix: compose during , the opponent-possessive parser, and upkeep using the shared nom building block (extract it to the appropriate parser-combinator authority if necessary).

[MED] The new runtime test bypasses the activation-restriction production entry. Evidence: crates/engine/src/game/restrictions.rs:3099-3145 calls evaluate_condition directly, whereas check_activation_restrictions reaches activation_restriction_applies at crates/engine/src/game/restrictions.rs:542-557, 907-997. Why it matters: it does not prove Trade Caravan’s parsed RequiresCondition gates a real activation. Suggested fix: parse the card restriction and test check_activation_restrictions for opposing-upkeep allow, own-upkeep deny, opposing non-upkeep deny, and the 2HG teammate-upkeep deny case.

🟡 Non-blocking

The unresolved CodeRabbit documentation finding is valid: the root-cause table still says 16 while the updated heading says 13 (docs/parser-misparse-backlog.md:42,5074). Update the table in the same follow-up.

Recommendation: request changes. This needs a small but cross-cutting rules-model correction and production-path coverage; it is not safe to enqueue or to patch as a one-line maintainer fixup.

@matthewevans matthewevans removed their assignment Jul 21, 2026
@matthewevans

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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/parser/oracle.rs`:
- Around line 6784-6790: Update opponents_upkeep_activation_restriction() to
compose the phrase from existing parsers for the “during” preposition, opponent
player role, and upkeep step instead of matching complete Oracle strings with
tag arms. Preserve support for both possessive spellings through the typed role
grammar and remove the special-case full-clause alternatives.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 946abb8c-07bc-4ddc-9597-e3d9a93fd8f6

📥 Commits

Reviewing files that changed from the base of the PR and between ebcf52b and 315ee0e.

📒 Files selected for processing (6)
  • crates/engine/src/ai_support/filter.rs
  • crates/engine/src/game/restrictions.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/src/types/ability.rs
  • docs/parser-misparse-backlog.md

Comment on lines +6784 to +6790
value(
opponents_upkeep_activation_restriction(),
alt((
tag("during an opponent's upkeep"),
tag("during an opponents upkeep"),
)),
),

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 | 🏗️ Heavy lift

Compose the role and step grammar instead of tagging whole clauses.

These full-clause tag arms create another special-case parser path and do not generalize possessive/role variants. Factor “during” + player role + step into the existing typed grammar building blocks.

As per coding guidelines, “Never dispatch parsing by matching verbatim Oracle strings” and “Compose nom combinators across independent dimensions instead of enumerating full-string permutations.”

🤖 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.rs` around lines 6784 - 6790, Update
opponents_upkeep_activation_restriction() to compose the phrase from existing
parsers for the “during” preposition, opponent player role, and upkeep step
instead of matching complete Oracle strings with tag arms. Preserve support for
both possessive spellings through the typed role grammar and remove the
special-case full-clause alternatives.

Sources: Coding guidelines, Path instructions

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.

3 participants