fix(engine): publish the announce-locked X on the LOYALTY fast path too (CR 606.1 + CR 602.2b)#5786
Merged
Merged
Conversation
…oo (CR 606.1 + CR 602.2b)
CR 606.1: a loyalty ability IS an activated ability, so CR 602.2b applies to it
verbatim — activating it follows rules 601.2b-i exactly as casting a spell does,
including CR 601.2b's announcement of X.
But planeswalker::handle_activate_loyalty has a MANA-FREE FAST PATH. It delegates
to casting::handle_activate_ability — where the announce-locked X is published —
ONLY when a cost-raise static (Eidolon of Obstruction) adds a mana component it
cannot pay. Otherwise it builds its own ResolvedAbility via build_pw_resolved and
goes straight to targeting, bypassing the publication entirely. That fast path is
the one every ordinary activation takes.
So Lukka, Bound to Ruin's [-4] ('... where X is the greatest power among creatures
you control as you activate this ability') never announced its X at all. The
engine then FAILED CLOSED at target selection ('Target count requires a resolved
quantity before target selection') rather than silently dealing 0 — which is why
the third call site is a fix and not a workaround: the announcement is simply
missing, and CR 606.1 says it must be there.
Fixed with the SAME single computation authority (ability_utils::publish_announced_x)
at the SAME position as the other two announce surfaces: at announcement, before
targets are chosen (CR 601.2c, immediately below the call). Three call sites = the
three places the engine physically announces (spell / activated ability / loyalty
fast path); one computation authority.
THIS WAS INVISIBLE TO EVERY LEDGER. Lukka is one of the four lying greens, so it
has no honest red to clear: the whole-face parse ledger and D-09 both show it merely
're-shaped', and neither can see that its X was never published at runtime. Only a
runtime witness on the loyalty surface exposes it — which is exactly what caught it.
CORRECTS THE RECORD ON COMMIT 2 (1a806318ad). That commit's message says the
announce-locked X is published at "the activation announcement
(handle_activate_ability, which loyalty abilities also ride)". THE PARENTHETICAL IS
FALSE. Loyalty abilities ride it ONLY when a cost-raise static taxes them into the
general flow; the ordinary, untaxed activation takes the fast path above and rides
nothing. Commit 2 therefore shipped Lukka, Bound to Ruin with its X never announced —
strictly WORSE at runtime than the resolution-time value it replaced (0 damage instead
of a wrongly-timed amount) — while every parse ledger still reported the face as merely
"re-shaped", because Lukka is a lying green with no honest red to clear. Neither the
whole-face ledger nor D-09 can see an X that is never published at runtime. Only a
runtime witness on the loyalty surface can, and that is what caught it.
WITNESSES (crates/engine/tests/integration/announce_locked_x_runtime.rs):
* lukka_minus_four_locks_the_greatest_power_at_loyalty_activation — WATCHED RED
(the engine REJECTED the activation outright) -> GREEN (5 damage, the announced
greatest power). Scope is stated in the test: AbilityActivation has no commit()
window (you can hold a SPELL on the stack but not an ABILITY — a harness asymmetry
that is plausibly why this bypass went unnoticed), so this witness proves
REACHABILITY of the publication on the loyalty surface. The LOCK semantics are a
property of publish_announced_x itself, already witnessed on the other two surfaces.
* aether_burst_locks_the_graveyard_count_at_cast — the announce-locked TARGET COUNT:
2 Aether Bursts in the graveyard at cast => X = 3, and it still reads 3 after one is
exiled mid-stack (a live count would say 2). Pinned on chosen_x, not on the bounce —
see below.
* a_text_defined_x_does_not_inflate_the_spells_mana_value_on_the_stack — the RUNTIME
half of the CR 202.3e gate: Monstrous Onslaught ({4}{R}, announced X=5) must answer
mana value 5 on the stack, not 10. WATCHED RED with the has_x() gate reverted (it
read 10), and its GREEN CONTROL — a real {X}{R} cost still reporting base + announced
X — PASSED IN THAT SAME RED RUN, so the gate narrows CR 202.3e to the objects the rule
covers without silencing the ones it does. This witness is only EXPRESSIBLE once the
announce-locked channel exists (without it, Monstrous Onslaught never gets a chosen_x,
so no cost_x_paid is stamped and the bug is unreachable), which is why it lands here
rather than in the gate's own commit.
SEPARATE PRE-EXISTING MISPARSE FOUND, NOT FIXED (recorded in the test, filed as a
finding): Aether Burst leaks the 'in all graveyards' phrase — which qualifies the
Aether Burst COUNT — into the TARGET's zone filter, lowering to
ChangeZone { origin: Graveyard, target: Typed{Creature, InZone(Graveyard)} }. The card
returns BATTLEFIELD creatures to their owners' hands. It is identical on this unit's
base and head, so it is neither caused nor cured here; the witness therefore pins the
announce-locked count (what this unit owns) and does not assert the bounce, because
asserting it would be asserting someone else's bug.
matthewevans
enabled auto-merge
July 14, 2026 05:23
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Parse changes introduced by this PR✓ No card-parse changes detected. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CR 606.1: a loyalty ability IS an activated ability, so CR 602.2b applies to it
verbatim — activating it follows rules 601.2b-i exactly as casting a spell does,
including CR 601.2b's announcement of X.
But planeswalker::handle_activate_loyalty has a MANA-FREE FAST PATH. It delegates
to casting::handle_activate_ability — where the announce-locked X is published —
ONLY when a cost-raise static (Eidolon of Obstruction) adds a mana component it
cannot pay. Otherwise it builds its own ResolvedAbility via build_pw_resolved and
goes straight to targeting, bypassing the publication entirely. That fast path is
the one every ordinary activation takes.
So Lukka, Bound to Ruin's [-4] ('... where X is the greatest power among creatures
you control as you activate this ability') never announced its X at all. The
engine then FAILED CLOSED at target selection ('Target count requires a resolved
quantity before target selection') rather than silently dealing 0 — which is why
the third call site is a fix and not a workaround: the announcement is simply
missing, and CR 606.1 says it must be there.
Fixed with the SAME single computation authority (ability_utils::publish_announced_x)
at the SAME position as the other two announce surfaces: at announcement, before
targets are chosen (CR 601.2c, immediately below the call). Three call sites = the
three places the engine physically announces (spell / activated ability / loyalty
fast path); one computation authority.
THIS WAS INVISIBLE TO EVERY LEDGER. Lukka is one of the four lying greens, so it
has no honest red to clear: the whole-face parse ledger and D-09 both show it merely
're-shaped', and neither can see that its X was never published at runtime. Only a
runtime witness on the loyalty surface exposes it — which is exactly what caught it.
CORRECTS THE RECORD ON COMMIT 2 (1a806318ad). That commit's message says the
announce-locked X is published at "the activation announcement
(handle_activate_ability, which loyalty abilities also ride)". THE PARENTHETICAL IS
FALSE. Loyalty abilities ride it ONLY when a cost-raise static taxes them into the
general flow; the ordinary, untaxed activation takes the fast path above and rides
nothing. Commit 2 therefore shipped Lukka, Bound to Ruin with its X never announced —
strictly WORSE at runtime than the resolution-time value it replaced (0 damage instead
of a wrongly-timed amount) — while every parse ledger still reported the face as merely
"re-shaped", because Lukka is a lying green with no honest red to clear. Neither the
whole-face ledger nor D-09 can see an X that is never published at runtime. Only a
runtime witness on the loyalty surface can, and that is what caught it.
WITNESSES (crates/engine/tests/integration/announce_locked_x_runtime.rs):
lukka_minus_four_locks_the_greatest_power_at_loyalty_activation — WATCHED RED
(the engine REJECTED the activation outright) -> GREEN (5 damage, the announced
greatest power). Scope is stated in the test: AbilityActivation has no commit()
window (you can hold a SPELL on the stack but not an ABILITY — a harness asymmetry
that is plausibly why this bypass went unnoticed), so this witness proves
REACHABILITY of the publication on the loyalty surface. The LOCK semantics are a
property of publish_announced_x itself, already witnessed on the other two surfaces.
aether_burst_locks_the_graveyard_count_at_cast — the announce-locked TARGET COUNT:
2 Aether Bursts in the graveyard at cast => X = 3, and it still reads 3 after one is
exiled mid-stack (a live count would say 2). Pinned on chosen_x, not on the bounce —
see below.
a_text_defined_x_does_not_inflate_the_spells_mana_value_on_the_stack — the RUNTIME
half of the CR 202.3e gate: Monstrous Onslaught ({4}{R}, announced X=5) must answer
mana value 5 on the stack, not 10. WATCHED RED with the has_x() gate reverted (it
read 10), and its GREEN CONTROL — a real {X}{R} cost still reporting base + announced
X — PASSED IN THAT SAME RED RUN, so the gate narrows CR 202.3e to the objects the rule
covers without silencing the ones it does. This witness is only EXPRESSIBLE once the
announce-locked channel exists (without it, Monstrous Onslaught never gets a chosen_x,
so no cost_x_paid is stamped and the bug is unreachable), which is why it lands here
rather than in the gate's own commit.
SEPARATE PRE-EXISTING MISPARSE FOUND, NOT FIXED (recorded in the test, filed as a
finding): Aether Burst leaks the 'in all graveyards' phrase — which qualifies the
Aether Burst COUNT — into the TARGET's zone filter, lowering to
ChangeZone { origin: Graveyard, target: Typed{Creature, InZone(Graveyard)} }. The card
returns BATTLEFIELD creatures to their owners' hands. It is identical on this unit's
base and head, so it is neither caused nor cured here; the witness therefore pins the
announce-locked count (what this unit owns) and does not assert the bounce, because
asserting it would be asserting someone else's bug.