You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
filter_binding_diverges (crates/engine/src/game/triggers.rs:9269) ends with a _ => false tail. In this module false means "both legs of the CR 603.4 hoist read this identically, so hoisting is safe" — so an unclassified TargetFilter variant is silently asserted to be reproducible at fire time. That is fail-open in the dangerous direction, and it contradicts the fail-closed policy the surrounding code states and the sibling classifier enforces.
Split out of #7389 rather than fixed there: the fix means adjudicating 54 TargetFilter variants, and flipping the tail alone changes hoist behaviour broadly. Neither belongs in a card fix.
Why this is fail-open
filter_binding_diverges is one of the guards delayed_intervening_if consults before hoisting a delayed triggered ability's intervening-if to fire time (CR 603.4's first half). The two legs of that pair must be the same predicate over the same values:
returning true → decline the hoist, keep today's resolution-only reading. Costs only CR 603.4's fire-time half.
returning false → hoist. If the filter actually names a resolution-scoped population, the fire-time leg resolves it against a population the resolver never computed, which can gate the ability off the stack — and for a consumed one-shot, delete it outright (false_gate_consumes_one_shot).
So the cost of a wrong true is a conservative re-check; the cost of a wrong false is a destroyed ability. The tail currently defaults new variants to the destructive answer.
The sibling already fixed exactly this
quantity_ref_binding_diverges (:9079) carries this doc, added in #7389:
EXHAUSTIVE and wildcard-free (matching object_scope_unbound_at_fire_time / quantity_expr_binding_diverges), so a new QuantityRef must be adjudicated here rather than silently defaulting to "cannot diverge" — the earlier _ => false tail rested on exactly that claim and it was false for the resolution-scoped payload-free family below. When in doubt the answer is true: declining costs only the fire-time half of CR 603.4 for that shape, while a wrong false deletes a real ability off the stack.
That is the same tail, the same reasoning, and it was already found to be wrong in practice on the QuantityRef axis. object_scope_unbound_at_fire_time (:8999) and player_scope_unbound_at_fire_time (:9024) are likewise wildcard-free. filter_binding_diverges is the one axis of the four that was not converted.
The gap is structural, not hypothetical: the population axis is reachable from quantity_ref_binding_diverges through the filter-bearing quantity refs (ObjectCount, SacrificedThisTurn, TokensCreatedThisTurn, BattlefieldEntriesThisTurn, …), so a resolution-scoped filter reaches the hoist decision through a classifier that is exhaustive, via one that is not.
There is a second, smaller instance of the same tail on gate_binding_diverges_at_fire_time (:8949), noted by CodeRabbit on #7389. Its blast radius is limited by what ability_condition_to_static_condition can bridge, but it should be settled in the same pass for consistency.
Replace _ => false with explicit arms covering all 54 TargetFilter variants, so a new variant fails to compile until someone decides — the property the other three classifiers already have.
Adjudicate each against the one question the module asks: does the fire-time QuantityContext (built from the delayed ability's controller, its CR 400.7 source context, and the matched event) bind this population the same way the resolving ability does? Anaphoric / target-relative / per-resolution-local filters are true; live-board and controller-relative populations are false.
Apply the module's stated tie-break in the doc comment: when in doubt, true.
Settle gate_binding_diverges_at_fire_time's _ => false the same way.
Expect some hoists to stop happening. That is the conservative direction and only costs CR 603.4's fire-time half — but it is a behaviour change, so it wants the full suite plus a check that the existing hoist pins (divergent_gate_bindings_decline_the_fire_time_hoist, non_battlefield_presence_gate_declines_the_fire_time_hoist, resolution_scoped_quantity_gate_declines_the_fire_time_hoist) still assert what they intend rather than being silently relaxed.
Not urgent
No current card is known to hit a misclassified filter — this is latent-correctness work, hardening a guard whose failure mode is destructive, not a live bug report. Worth doing before the hoist gets extended to more gate shapes, since each extension widens what reaches this tail.
References
crates/engine/src/game/triggers.rs:9269 — filter_binding_diverges (the _ => false tail)
crates/engine/src/game/triggers.rs:9079 — quantity_ref_binding_diverges (the sibling, already exhaustive, with the rationale)
crates/engine/src/game/triggers.rs:8999 / :9024 — the object- and player-axis classifiers, both wildcard-free
crates/engine/src/game/triggers.rs:8949 — gate_binding_diverges_at_fire_time (second instance of the same tail)
Summary
filter_binding_diverges(crates/engine/src/game/triggers.rs:9269) ends with a_ => falsetail. In this modulefalsemeans "both legs of the CR 603.4 hoist read this identically, so hoisting is safe" — so an unclassifiedTargetFiltervariant is silently asserted to be reproducible at fire time. That is fail-open in the dangerous direction, and it contradicts the fail-closed policy the surrounding code states and the sibling classifier enforces.Split out of #7389 rather than fixed there: the fix means adjudicating 54
TargetFiltervariants, and flipping the tail alone changes hoist behaviour broadly. Neither belongs in a card fix.Why this is fail-open
filter_binding_divergesis one of the guardsdelayed_intervening_ifconsults before hoisting a delayed triggered ability's intervening-ifto fire time (CR 603.4's first half). The two legs of that pair must be the same predicate over the same values:true→ decline the hoist, keep today's resolution-only reading. Costs only CR 603.4's fire-time half.false→ hoist. If the filter actually names a resolution-scoped population, the fire-time leg resolves it against a population the resolver never computed, which can gate the ability off the stack — and for a consumed one-shot, delete it outright (false_gate_consumes_one_shot).So the cost of a wrong
trueis a conservative re-check; the cost of a wrongfalseis a destroyed ability. The tail currently defaults new variants to the destructive answer.The sibling already fixed exactly this
quantity_ref_binding_diverges(:9079) carries this doc, added in #7389:That is the same tail, the same reasoning, and it was already found to be wrong in practice on the
QuantityRefaxis.object_scope_unbound_at_fire_time(:8999) andplayer_scope_unbound_at_fire_time(:9024) are likewise wildcard-free.filter_binding_divergesis the one axis of the four that was not converted.The gap is structural, not hypothetical: the population axis is reachable from
quantity_ref_binding_divergesthrough the filter-bearing quantity refs (ObjectCount,SacrificedThisTurn,TokensCreatedThisTurn,BattlefieldEntriesThisTurn, …), so a resolution-scoped filter reaches the hoist decision through a classifier that is exhaustive, via one that is not.There is a second, smaller instance of the same tail on
gate_binding_diverges_at_fire_time(:8949), noted by CodeRabbit on #7389. Its blast radius is limited by whatability_condition_to_static_conditioncan bridge, but it should be settled in the same pass for consistency.Currently classified
Proposed fix
_ => falsewith explicit arms covering all 54TargetFiltervariants, so a new variant fails to compile until someone decides — the property the other three classifiers already have.QuantityContext(built from the delayed ability's controller, its CR 400.7 source context, and the matched event) bind this population the same way the resolving ability does? Anaphoric / target-relative / per-resolution-local filters aretrue; live-board and controller-relative populations arefalse.true.gate_binding_diverges_at_fire_time's_ => falsethe same way.divergent_gate_bindings_decline_the_fire_time_hoist,non_battlefield_presence_gate_declines_the_fire_time_hoist,resolution_scoped_quantity_gate_declines_the_fire_time_hoist) still assert what they intend rather than being silently relaxed.Not urgent
No current card is known to hit a misclassified filter — this is latent-correctness work, hardening a guard whose failure mode is destructive, not a live bug report. Worth doing before the hoist gets extended to more gate shapes, since each extension widens what reaches this tail.
References
crates/engine/src/game/triggers.rs:9269—filter_binding_diverges(the_ => falsetail)crates/engine/src/game/triggers.rs:9079—quantity_ref_binding_diverges(the sibling, already exhaustive, with the rationale)crates/engine/src/game/triggers.rs:8999/:9024— the object- and player-axis classifiers, both wildcard-freecrates/engine/src/game/triggers.rs:8949—gate_binding_diverges_at_fire_time(second instance of the same tail)if, both halves), CR 115.1 / CR 115.10 (target and player scopes)