Skip to content

fix(engine): publish the announce-locked X on the LOYALTY fast path too (CR 606.1 + CR 602.2b)#5786

Merged
matthewevans merged 1 commit into
mainfrom
ship/t99-loyalty-fastpath
Jul 14, 2026
Merged

fix(engine): publish the announce-locked X on the LOYALTY fast path too (CR 606.1 + CR 602.2b)#5786
matthewevans merged 1 commit into
mainfrom
ship/t99-loyalty-fastpath

Conversation

@matthewevans

Copy link
Copy Markdown
Member

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.

…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
matthewevans enabled auto-merge July 14, 2026 05:23
@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 added this pull request to the merge queue Jul 14, 2026
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

Merged via the queue into main with commit 8ae64e5 Jul 14, 2026
13 checks passed
@matthewevans
matthewevans deleted the ship/t99-loyalty-fastpath branch July 14, 2026 05:44
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.

1 participant