Skip to content

fix(coverage): surface an ability's activation zone in the parse signature - #7330

Merged
matthewevans merged 1 commit into
phase-rs:mainfrom
cuinhellcat:fix/coverage-activation-zone
Aug 13, 2026
Merged

fix(coverage): surface an ability's activation zone in the parse signature#7330
matthewevans merged 1 commit into
phase-rs:mainfrom
cuinhellcat:fix/coverage-activation-zone

Conversation

@cuinhellcat

@cuinhellcat cuinhellcat commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Fixes #7317.

Summary

activation_zone appeared zero times in crates/engine/src/game/coverage.rs, so an activated ability's parse-diff signature never rendered the zone it functions from. can_activate_ability_now_with_restriction_gates gates legality on that field (casting.rs:18421) and the candidate enumerators key their hand, graveyard and library loops off it, so a change to it moves cards between "offered" and "not offered" — with nothing to show for it at the PR gate.

Surfaced on #7316, which moved 55 cards from activation_zone: null to Graveyard, withdrawing a battlefield offer and unlocking a graveyard one on each. Its parse-diff sticky read ✓ No card-parse changes detected.

That is the inverse of #5507's failure mode and worse. There, removals with no compensating addition made a correct fix look like a regression — visibly wrong, so a reviewer investigates. Here a rules-behavior change across 55 cards was indistinguishable from a no-op, and the only way to tell was to grep coverage.rs for the field name.

Fourth instance of the class — #5492 (PreventDamage.damage_source_filter), #5495 (ChangeZone entry qualifiers), #5507 (ManaSpellGrant), #5673 (ReplacementDefinition scoping) — and the first in ability_details rather than effect_details, which is where #5501/#5507 applied the exhaustive-destructuring guard. The ability shell renders AbilityDefinition's own fields and picks nine of them by hand from a struct with roughly forty-five.

⚠️ This PR's own sticky will report 79 clusters / 986 card rows

That is the intended one-time signature migration, not parser blast radius. Every row is ability | field | activates from | ∅ → <zone>; nothing changes value, nothing gains or loses support. The review check is that the sticky shows zero added, removed, oracle_changed and support flips — measured locally against 9b8bb91..96eadbd:

after-value card rows
hand 651
graveyard 333
exile 1
command zone 1
total 986 (977 unique cards, 79 clusters)

Completeness cross-check: client/public/card-data.json carries 991 non-null activation_zone values (hand 656, graveyard 333, command 1, exile 1); the regenerated coverage-data.json carries 991 activates from entries. One-to-one — nothing is lost to build_ability_item's dedup on any real card. (Zone::Library, used by Plot, is set at runtime and never persisted, so it correctly produces no rows.)

Files changed

Track

Developer

LLM

Model: claude-opus-5
Tier: Frontier
Thinking: high

Implementation method (required)

Method: not-applicable — no game logic, parser, effect, trigger, targeting or state-machine behavior changes. This is a projection-only change to the coverage/parse-diff instrument: the sole production edit appends a (String, String) pair to a details vector consumed by coverage-report. No AbilityDefinition value, no AST, no runtime path is affected. A final /review-impl pass ran against the committed head regardless, per CONTRIBUTING.

CR references

CR 113.6b, CR 113.6j, CR 113.6m in the new comment, with CR 113.6 named as the battlefield default that makes None emit nothing — each grepped from docs/MagicCompRules.txt (:775, :791, :796, :771) before being written. They are the three rules that put a non-None zone on an ability: stated-zone text, a cost unpayable on the battlefield, and a cost or effect that moves the object out of a zone.

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 test -p phase-engine — 18922 + 21 + 9 + 4867 passed, 0 failed, 15 ignored

  • cargo clippy-strict — clean

  • cargo fmt --all — clean

  • ./scripts/check-parser-combinators.sh — Gate G PASS, Gate A PASS

  • pre-commit — Gate P PASS

  • coverage-parse-diff base→head on one corpus (MTGJSON 5.3.0+20260810) — 79 clusters, all key="activates from", all before=∅; 0 SupportFlip, 0 added, 0 removed, 0 oracle_changed

  • Two-way control — with the d.push removed, activation_zone_reaches_parse_details_without_colliding_with_effect_origin fails at its "must render under its own key" assertion and prints the real signature [("from","graveyard"),("to","hand"),("target","self"),("kind","activated")], reproducing the blindness. Its None row independently pins the byte-identity requirement, so the ~12k battlefield-default abilities cannot churn.

Tilt is not installed in this environment; scripts/tilt-wait.sh would return 3 (cannot answer), never a build result, so the isolated-direct commands above were used. cargo nextest is likewise unavailable.

Gate A

Gate A PASS head=96eadbd base=9b8bb919a0c0ccd7a6e2eb1217d25c341d90a14f

Anchored on

Final review-impl

Final review-impl PASS head=96eadbd

Claimed parse impact

No card's parse changes. 977 cards gain a detail key that was previously absent; no existing key changes value, and no card's supported flips. See the table above.

Scope Expansion

None. Two adjacent gaps were found during review and are deliberately not in this PR — recorded here so they are not mistaken for oversights, and offered as follow-ups:

  1. The recurrence guard is still missing. Fixing the instance does not stop instance chore: update coverage stats and badges #5. ability_details still hand-picks its keys with no compile-time parity check, while the house idiom for this exact struct already exists in four places — Serialize for AbilityDefinition (types/ability.rs:19086, "Exhaustive destructure with NO .. — this is the field-parity guard"), game/ability_scan.rs:4333, game/ability_rw.rs:3863, ai_support/shortcut_efficacy.rs:635. A destructure guard changes zero bytes of output, so the usual churn argument does not apply; it is separable and belongs in its own diff.
  2. Two sibling activation gates remain unprojected, checked by the same production function as the zone. activator_filter (CR 602.2a, casting.rs:18357) appears zero times in coverage.rs, and of ActivationRestriction's 17 variants only AsSorcery is rendered. A future PR flipping OnlyOnceEachTurn, MaxTimesEachTurn, ClassLevelIs, CounterThreshold, or activator_filter: None → Some(Opponent) would move cards between offered and not-offered and post ✓ No card-parse changes detected. — the fix(parser): derive activation zone from a self-move's origin, not its destination #7316 failure, one field over.

Happy to open either as its own PR if maintainers want them.

Validation Failures

None.

CI Failures

None.

Summary by CodeRabbit

  • New Features
    • Ability details now clearly distinguish where an effect originates from where the ability activates.
    • Non-default activation zones are shown under a dedicated “activates from” entry.
    • Abilities using the default battlefield activation zone remain concise, without extra activation-zone details.
  • Bug Fixes
    • Improved ability detail accuracy and differentiation when activation zones vary.

…ature

`activation_zone` appeared zero times in `game/coverage.rs`, so an activated
ability's parse-diff signature never rendered the zone it functions from.
`can_activate_ability_now` gates legality on that field and the candidate
enumerators key their hand, graveyard and library loops off it, so a change to
it moves cards between "offered" and "not offered" — and the PR gate could not
see any of it.

Surfaced on phase-rs#7316, which moved 55 cards from `activation_zone: null` to
`Graveyard`, withdrawing a battlefield offer and unlocking a graveyard one on
each. Its parse-diff sticky reported `No card-parse changes detected.` That is
the inverse of phase-rs#5507's failure mode and worse: there, removals with no
compensating addition made a correct fix look like a regression — visibly
wrong, so a reviewer investigates. Here a rules-behavior change across 55 cards
was indistinguishable from a no-op.

Fourth instance of the class, after phase-rs#5492, phase-rs#5495, phase-rs#5507 and phase-rs#5673 — and the
first in `ability_details` rather than `effect_details`, which is where phase-rs#5507's
exhaustive-destructuring recommendation was applied. The ability shell renders
`AbilityDefinition`'s own fields and picks them by hand from a struct with over
thirty of them.

The key is `activates from`, not `from`: `effect_details` already emits `from`
for a `ChangeZone` origin and `trigger_details` for a trigger origin, and
`build_ability_item` silently drops duplicate keys — reusing `from` would hide
this on precisely the abilities it exists to watch. Emitted unconditionally;
`None` is the CR 113.6 battlefield default and emits nothing, so the ~12k
abilities that default to the battlefield keep byte-identical signatures.

Measured, base vs head on the same corpus: 79 clusters, every one
`ability | field | activates from | ∅ → <zone>`, 986 card rows (651 hand,
333 graveyard, 1 exile, 1 command zone). Zero SupportFlip, zero added, zero
removed, zero oracle_changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 13, 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: 85203376-45c7-4b11-9f1c-ec002e754b1b

📥 Commits

Reviewing files that changed from the base of the PR and between 1f612b5 and 96eadbd.

📒 Files selected for processing (1)
  • crates/engine/src/game/coverage.rs

📝 Walkthrough

Walkthrough

ability_details now records explicit activation zones under activates from. Battlefield-default abilities omit this detail. Regression tests cover default behavior, effect-origin key coexistence, and signature differences.

Changes

Activation-Zone Coverage

Layer / File(s) Summary
Emit activation-zone details
crates/engine/src/game/coverage.rs
ability_details emits activates from for explicit activation zones and preserves from for effect origins.
Validate activation-zone signatures
crates/engine/src/game/coverage.rs
Regression tests verify omission for battlefield defaults, coexistence with graveyard effect origins, and distinct signatures for different activation zones.

Estimated code review effort: 2 (Simple) | ~10 minutes

Mergeability Score: ⚪ Minimal · up to 96ead

This localized change only exposes an existing activation-zone property in coverage signatures; it does not alter game behavior or card parsing. The PR is merge-ready after normal checks and review, with no actionable merge-blocking risk remaining.

Suggested labels: bug

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 describes exposing an ability's activation zone in the coverage parse signature.
Linked Issues check ✅ Passed The PR satisfies #7317 by rendering non-default activation zones under "activates from" while preserving battlefield-default signatures.
Out of Scope Changes check ✅ Passed The changes stay within #7317 and add only the requested projection update plus regression coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@matthewevans matthewevans self-assigned this Aug 13, 2026
@matthewevans matthewevans added the bug Bug fix label Aug 13, 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.

Reviewed at head 96eadbdc224498ca8711f52d0e69854bc545b039: this places the activation-zone projection at the correct coverage-signature seam, uses a non-colliding key, and the focused test covers both the default omission and the ChangeZone-origin collision.

Held for the exact-head required CI, card-data parse-diff artifact, and CodeRabbit review. The branch is mergeable; its behind status alone does not require a noisy rebase because the merge queue rebases speculatively.

@matthewevans matthewevans removed their assignment Aug 13, 2026
@github-actions

Copy link
Copy Markdown

Generated for head 96eadbdc224498ca8711f52d0e69854bc545b039.

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

🟡 Modified fields (25 signatures)

  • 312 cards · 🔄 ability/Draw · changed field activates from: hand
    • Affected (first 3): Agonasaur Rex, Airship Crash, Akroma's Blessing (+309 more)
  • 185 cards · 🔄 ability/ChangeZone · changed field activates from: graveyard
    • Affected (first 3): A-Cauldron Familiar, A-Earthquake Dragon, A-Llanowar Greenwidow (+182 more)
  • 115 cards · 🔄 ability/SearchLibrary · changed field activates from: hand
    • Affected (first 3): A.I.M. Scientists, Absorb Vis, Alabaster Host Intercessor (+112 more)
  • 87 cards · 🔄 ability/PutCounter · changed field activates from: hand
    • Affected (first 3): 17-Year Cicadas, Aeon Chronicler, Ancestral Vision (+84 more)
  • 34 cards · 🔄 ability/GrantCastingPermission · changed field activates from: hand
    • Affected (first 3): Aloe Alchemist, Beastbond Outcaster, Blacksnag Buzzard (+31 more)
  • 33 cards · 🔄 ability/PutCounter · changed field activates from: graveyard
    • Affected (first 3): Adorned Crocodile, Agent of Kotis, Alchemist's Assistant (+30 more)
  • 28 cards · 🔄 ability/CopyTokenOf · changed field activates from: graveyard
    • Affected (first 3): Adorned Pouncer, Angel of Sanctions, Anointer Priest (+25 more)
  • 26 cards · 🔄 ability/Encore · changed field activates from: graveyard
    • Affected (first 3): Amphin Mutineer, Angel of Indemnity, Belonging (+23 more)
  • 17 cards · 🔄 ability/Token · changed field activates from: graveyard
    • Affected (first 3): Dauntless Cathar, Eternal Student, Expendable Lackey (+14 more)
  • 14 cards · 🔄 ability/CombineHost · changed field activates from: hand
    • Affected (first 3): Bat-, Half-Kitten, Half-, Half-Orc, Half- (+11 more)
  • 14 cards · 🔄 ability/Draw · changed field activates from: graveyard
    • Affected (first 3): A-Cobbled Lancer, Cobbled Lancer, Faerie Dreamthief (+11 more)
  • 11 cards · 🔄 ability/Pump · changed field activates from: hand
    • Affected (first 3): Ghost-Lit Nourisher, Harvester of Misery, Pyrewild Shaman (+8 more)
  • 10 cards · 🔄 ability/grant ability · changed field activates from: hand
    • Affected (first 3): A-Glamorous Outlaw, A-Masked Bandits, A-Rakish Revelers (+7 more)
  • 9 cards · 🔄 ability/ChangeZone · changed field activates from: hand
    • Affected (first 3): Altanak, the Thrice-Called, Colossal Skyturtle, Faerie Macabre (+6 more)
  • 8 cards · 🔄 ability/Token · changed field activates from: hand
    • Affected (first 3): Born to Drive, Careful Cultivation, Creative Outburst (+5 more)
  • 5 cards · 🔄 ability/DealDamage · changed field activates from: hand
    • Affected (first 3): Eiganjo, Seat of the Empire, Ghost-Lit Raider, Team Avatar (+2 more)
  • 4 cards · 🔄 ability/Destroy · changed field activates from: hand
    • Affected (first 3): Bamboo Grove Archer, Boseiju, Who Endures, Kogla and Yidaro (+1 more)
  • 3 cards · 🔄 ability/DamageAll · changed field activates from: hand
    • Affected (first 3): Arashi, the Sky Asunder, Jiwari, the Earth Aflame, Mjölnir, Hammer of Thor
  • 3 cards · 🔄 ability/SearchLibrary · changed field activates from: graveyard
    • Affected (first 3): Colossal Rattlewurm, Soul of Mirrodin, Trustworthy Scout
  • 3 cards · 🔄 ability/Tap · changed field activates from: hand
    • Affected (first 3): Piercing Rays, Plumes of Peace, Saiba Trespassers
  • 2 cards · 🔄 ability/Bounce · changed field activates from: hand
    • Affected (first 3): Colossal Skyturtle, Otawara, Soaring City
  • 2 cards · 🔄 ability/Counter · changed field activates from: hand
    • Affected (first 3): Ghost-Lit Warder, Mirrorshell Crab
  • 2 cards · 🔄 ability/Dig · changed field activates from: hand
    • Affected (first 3): Visionary's Dance, Waker of Waves
  • 2 cards · 🔄 ability/GainLife · changed field activates from: graveyard
    • Affected (first 3): Bramble Wurm, Stone Docent
  • 2 cards · 🔄 ability/Mana · changed field activates from: hand
    • Affected (first 3): Elvish Spirit Guide, Simian Spirit Guide
… 54 more signature(s) (55 card-changes) — showing first 54; see parse-diff.json

🟡 Modified fields (54 signatures)

  • 2 cards · 🔄 ability/grant Flying · changed field activates from: hand
  • 1 card · 🔄 ability/BecomeMonarch · changed field activates from: graveyard
  • 1 card · 🔄 ability/CantBeBlocked · changed field activates from: graveyard
  • 1 card · 🔄 ability/CantBeBlocked · changed field activates from: hand
  • 1 card · 🔄 ability/CantBlock · changed field activates from: hand
  • 1 card · 🔄 ability/ChangeZone · changed field activates from: command zone
  • 1 card · 🔄 ability/ChangeZone · changed field activates from: exile
  • 1 card · 🔄 ability/Choose · changed field activates from: hand
  • 1 card · 🔄 ability/ChooseFromZone · changed field activates from: hand
  • 1 card · 🔄 ability/CombineHost · changed field activates from: graveyard
  • 1 card · 🔄 ability/Conjure · changed field activates from: graveyard
  • 1 card · 🔄 ability/Conjure · changed field activates from: hand
  • 1 card · 🔄 ability/CopySpell · changed field activates from: graveyard
  • 1 card · 🔄 ability/CreateDelayedTrigger · changed field activates from: hand
  • 1 card · 🔄 ability/DealDamage · changed field activates from: graveyard
  • 1 card · 🔄 ability/Dig · changed field activates from: graveyard
  • 1 card · 🔄 ability/Discard · changed field activates from: graveyard
  • 1 card · 🔄 ability/Discard · changed field activates from: hand
  • 1 card · 🔄 ability/Discover · changed field activates from: graveyard
  • 1 card · 🔄 ability/GainLife · changed field activates from: hand
  • 1 card · 🔄 ability/Incubate · changed field activates from: graveyard
  • 1 card · 🔄 ability/Mana · changed field activates from: graveyard
  • 1 card · 🔄 ability/Mill · changed field activates from: hand
  • 1 card · 🔄 ability/MustBeBlockedByAll · changed field activates from: hand
  • 1 card · 🔄 ability/PutCounterAll · changed field activates from: graveyard
  • 1 card · 🔄 ability/PutCounterAll · changed field activates from: hand
  • 1 card · 🔄 ability/PutOnTopOrBottom · changed field activates from: hand
  • 1 card · 🔄 ability/RingTemptsYou · changed field activates from: graveyard
  • 1 card · 🔄 ability/Sacrifice · changed field activates from: hand
  • 1 card · 🔄 ability/Scry · changed field activates from: graveyard
  • 1 card · 🔄 ability/SetLifeTotal · changed field activates from: graveyard
  • 1 card · 🔄 ability/Tap · changed field activates from: graveyard
  • 1 card · 🔄 ability/TargetOnly · changed field activates from: graveyard
  • 1 card · 🔄 ability/VentureIntoDungeon · changed field activates from: hand
  • 1 card · 🔄 ability/conjure · changed field activates from: graveyard
  • 1 card · 🔄 ability/for · changed field activates from: hand
  • 1 card · 🔄 ability/grant DoubleStrike · changed field activates from: graveyard
  • 1 card · 🔄 ability/grant FirstStrike · changed field activates from: hand
  • 1 card · 🔄 ability/grant Flying · changed field activates from: graveyard
  • 1 card · 🔄 ability/grant Haste · changed field activates from: hand
  • 1 card · 🔄 ability/grant Indestructible · changed field activates from: graveyard
  • 1 card · 🔄 ability/grant Lifelink · changed field activates from: graveyard
  • 1 card · 🔄 ability/grant Menace · changed field activates from: graveyard
  • 1 card · 🔄 ability/grant Shadow · changed field activates from: hand
  • 1 card · 🔄 ability/power +1, toughness +1, grant Reach, grant Trample, grant Ward · changed field activates from: graveyard
  • 1 card · 🔄 ability/power +1, toughness +2, grant Deathtouch · changed field activates from: hand
  • 1 card · 🔄 ability/power +2, toughness +2, grant FirstStrike, grant Lifelink · changed field activates from: graveyard
  • 1 card · 🔄 ability/power +3, toughness +1, add subtype Dinosaur · changed field activates from: hand
  • 1 card · 🔄 ability/power +3, toughness +1, grant FirstStrike · changed field activates from: hand
  • 1 card · 🔄 ability/power +3, toughness +1, grant Trample · changed field activates from: hand
  • 1 card · 🔄 ability/power +3, toughness +3, grant DoubleStrike · changed field activates from: hand
  • 1 card · 🔄 ability/power +3, toughness +3, grant Reach · changed field activates from: hand
  • 1 card · 🔄 ability/power +4, toughness +4, grant Trample · changed field activates from: hand
  • 1 card · 🔄 ability/power +9, toughness +9, grant Trample · changed field activates from: hand

@matthewevans matthewevans self-assigned this Aug 13, 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.

Reviewed current head 96eadbdc224498ca8711f52d0e69854bc545b039 in an isolated worktree. The coverage authority now emits the explicit activation_zone under a collision-free activates from key; it preserves from for effect/trigger origins and leaves the battlefield-default None signature unchanged. The current-SHA parse-diff shows exactly this field change across 977 cards, with no support flips or unrelated parse changes. Verified the CR 113.6b/j/m citation against the current Comprehensive Rules. CI is green and there are no unresolved review threads.

@matthewevans
matthewevans added this pull request to the merge queue Aug 13, 2026
@matthewevans matthewevans removed their assignment Aug 13, 2026
Merged via the queue into phase-rs:main with commit 8f6f85a Aug 13, 2026
15 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.

coverage-parse-diff is blind to activation_zone (fourth instance, in ability_details rather than effect_details)

2 participants