Skip to content

fix(parser): UNSUPPORTED full-set one-off: Ood Sphere#5170

Merged
matthewevans merged 4 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-94-unsupported-full-set-one
Jul 6, 2026
Merged

fix(parser): UNSUPPORTED full-set one-off: Ood Sphere#5170
matthewevans merged 4 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-94-unsupported-full-set-one

Conversation

@ntindle

@ntindle ntindle commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a parser misparse affecting 1 card(s) in the Doctor Who Commander precons.

Root cause: UNSUPPORTED full-set one-off: Ood Sphere

Cards corrected

  • Ood Sphere

Fix

Implemented cluster 94 (Ood Sphere, WHO Planechase plane) — fully fixed both misparses and built the "can't become tapped" runtime enforcement seam across all layers. Verified via oracle-gen: 0 Unimplemented, both defects corrected.

DEFECT 1 — "Noncreature spells have convoke" was a silent misparse (runtime-inert anthem AddKeyword). Added a non-possessive, all-players branch (Pattern 4) to parse_spells_have_keyword that emits StaticMode::CastWithKeyword{Convoke} with a CONTROLLER-AGNOSTIC affected filter (Typed[Non(Creature)], controller:null), active_zones [Battlefield] → synthesis appends Command → functions in the command zone via game_active_statics/granted_spell_keywords for every player's noncreature spell. Parameterizes the controller axis of the existing casting-keyword-grant pattern; covers any plane/global "[type] spells have [keyword]" grant.

DEFECT 2 — the Red-Eye trigger's second sentence was Effect::Unimplemented("Unsupported unless clause"). Added try_parse_temporary_cant_become_tapped (nom combinators, hand-rolled ParentTarget subject + CR 508.1f attacker exemption, FAILS CLOSED for any other rider) → lowers to Effect::GenericEffect{ GrantStaticAbility(CantTap on SelfRef), affected/target: ParentTarget, duration: UntilNextTurnOf(Controller) } as the trigger's SequentialSibling sub-ability. Dispatched at the TOP of parse_effect_clause (before the unless-suffix stripper that was preempting it).

RUNTIME SEAM (per "build the seam" mandate) — turned the inert StaticMode::CantTap stub into an enforced restriction: (1) new single-authority predicate restrictions::object_cant_tap (fast-path static_kind_present + intrinsic active_static_definitions scan); (2) new single-authority restrictions::tap_permanent_for_cost routed through all 8 cost-tap chokepoints ({T} ability, convoke, crew, station, saddle, harmonize, tap-N additional cost, {T}/tap-another mana abilities); (3) process_one_tap effect-tap gate (no-op on restricted creature); (4) {T}-activation legality gate in check_summoning_sickness_for_cost (tap-only, {Q} unaffected) + mana-ability readiness gate + auto-tap mana-source exclusion; (5) AI/MP offer filtering in candidates.rs (crew/saddle/station eligibility hoist + convoke offer). Combat declare-attackers path deliberately NOT gated (CR 508.1f exemption). Helper no-ops for unrestricted objects (fast-path), so zero behavior change to existing cards.

VERIFICATION (worktree runs cargo directly, no Tilt): cargo fmt OK; cargo clippy -p engine --all-targets exit 0 clean; full engine test suite GREEN (15328 lib + 1775 integration + all targets, 0 failed); oracle-gen "Ood Sphere" → 0 Unimplemented with CastWithKeyword{Convoke}(active_zones [Battlefield,Command]) + CantTap GenericEffect; parser-combinator gate — my added production lines clean (rsplit/tag/alt only). Added building-block + runtime tests including the Finding-1 REQUIRED gate (generic_effect_cant_tap_binds_every_parent_target_object: multi-object ParentTarget binds one CantTap TCE per goaded creature, non-goaded unaffected). Did NOT commit.

Files changed

  • crates/engine/src/parser/oracle_static/keyword_grant.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/types/statics.rs
  • crates/engine/src/game/restrictions.rs
  • crates/engine/src/game/costs.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/engine_casting.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/mana_abilities.rs
  • crates/engine/src/game/effects/tap_untap.rs
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/game/effects/effect.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/src/parser/oracle_trigger.rs

CR references

  • CR 701.26a — To tap a permanent... Only untapped permanents can be tapped (tap action; the CantTap restriction modifies it). Verified docs/MagicCompRules.txt:3520
  • CR 508.1f — Tapping a creature when declared as attacker isn't a cost (the sole exemption; declare-attackers path never gated). Verified :2274
  • CR 702.51a — Convoke is a static ability functioning while the spell is on the stack (all-players casting-keyword grant). Verified :4397
  • CR 702.122b — A creature crews a Vehicle when tapped to pay the crew cost. Verified :4884
  • CR 702.184a — Station: Tap another untapped creature you control. Verified :5366
  • CR 702.171c — A creature saddles a permanent as it's tapped to pay the saddle cost. Verified :5271
  • CR 113.6b — An ability that states which zones it functions in functions only from those zones (command-zone plane static). Verified :775
  • CR 611.2a — Continuous effect from resolution lasts as stated (until your next turn). Verified :2904
  • CR 611.2c — Affected-object set fixed when the continuous effect begins (the goaded creatures). Verified :2909
  • CR 302.6 — Summoning-sickness rule (convoke tap exempt; referenced in existing comment). Verified :1630

Verification

  • cargo fmt --all — clean (no files changed, no commit needed)
  • check-parser-combinators.sh <upstream/main merge-base> — clean (exit 0)
  • cargo clippy -p engine --all-targets -- -D warnings — clean (no warnings in this cluster's 17 changed files)
  • cargo test -p engine — clean (exit 0, 0 failures; all Ood Sphere tests pass)
  • oracle-gen -- data --filter "ood sphere" — confirmed (parses correctly, no Unimplemented/Unknown)
    Cards confirmed re-parsed correctly: Ood Sphere

🤖 Generated with Claude Code

@ntindle ntindle requested a review from matthewevans as a code owner July 6, 2026 02:55
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@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.

Thanks for the Ood Sphere pass. I found two blocking gaps in the new CantTap enforcement: the backstop rejects some taps, but several eligibility surfaces still advertise restricted creatures as legal choices.

  • Crew, Station, and Saddle still build their eligible_creatures lists from controller/untapped/creature checks without filtering restrictions::object_cant_tap. See crates/engine/src/game/engine.rs around the Crew list, Station list, and Saddle list. Those lists are used to decide whether the activation is possible and to construct the waiting-state selection surface, so a creature that cannot become tapped can still count toward the required total and be offered to the user/AI, then fail only later when the tap backstop runs.

  • Harmonize has the same mismatch in both paths: the castability probe in crates/engine/src/game/casting.rs simulates tapping any untapped powered creature, and the HarmonizeTapChoice list in crates/engine/src/game/casting_costs.rs offers any untapped powered creature. Both need to exclude object_cant_tap creatures before declaring the spell castable or offering the tap choice.

Please route all tap-for-cost eligibility through the same object_cant_tap authority, and add targeted tests where the only apparent contributor is a restricted creature, plus a threshold case where including the restricted creature incorrectly makes the activation/cast appear legal.

Also holding approval until the required parse-diff sticky is posted for this parser/engine head.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 3 card(s), 8 signature(s) (baseline: main 309807405fe2)

1 card(s) · static/CastWithKeyword(Convoke) · added: CastWithKeyword(Convoke) (affects=non-creature)

Examples: Ood Sphere

1 card(s) · static/CastWithKeyword(Rebound) · added: CastWithKeyword(Rebound) (affects=instant or sorcery)

Examples: Trail of the Mage-Rings

1 card(s) · static/CastWithKeyword(SplitSecond) · added: CastWithKeyword(SplitSecond) (affects=any)

Examples: Lake Silencio

1 card(s) · static/Continuous · removed: Continuous (affects=card non-creature, mods=grant Convoke)

Examples: Ood Sphere

1 card(s) · static/Continuous · removed: Continuous (affects=card, mods=grant SplitSecond)

Examples: Lake Silencio

1 card(s) · static/Continuous · removed: Continuous (affects=instant or sorcery, mods=grant Rebound)

Examples: Trail of the Mage-Rings

1 card(s) · ability/Unsupported unless clause · removed: Unsupported unless clause

Examples: Ood Sphere

1 card(s) · ability/grant static ability · added: grant static ability (affects=parent target, duration=until next turn (you), grants=grant static ability, target=parent target)

Examples: Ood Sphere

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

# Conflicts:
#	crates/engine/src/game/casting.rs
#	crates/engine/src/game/restrictions.rs
#	crates/engine/src/parser/oracle_effect/mod.rs

@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.

Addressed the remaining tap-restriction blocker. Crew, Station, Saddle, Harmonize castability, and HarmonizeTapChoice now exclude creatures that can't become tapped at the eligibility surface, with focused regressions. Local checks: cargo fmt --all; git diff --check; check-parser-combinators; check-engine-authorities; cargo test -p engine --lib cant_tap.

@matthewevans matthewevans enabled auto-merge July 6, 2026 10:41
@matthewevans matthewevans added this pull request to the merge queue Jul 6, 2026
@matthewevans matthewevans added the enhancement New feature or request label Jul 6, 2026
Merged via the queue into phase-rs:main with commit 72939fd Jul 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants