🤖 AI text below 🤖
Related: #6857
Summary
#6857's root cause is that affected_objects_from_events
(crates/engine/src/game/effects/mod.rs:5590) has no arm for effect heads that affect objects without
emitting a per-object event: they publish an empty tracked set and every downstream
TargetFilter::TrackedSet anaphor binds nothing.
DraftFromSpellbook (2 gated rows) and ManifestDread (1 gated row) are filed together because they
share one mechanism: each parks a WaitingFor mid-resolution, and the objects they produce land on
the continuation's event vector — not on the resolver's. A compensating publish therefore belongs
in the continuation handler, not in affected_objects_from_events.
Both heads are excluded from the #6857 PR because they were dispositioned by code-read only and were
never runtime-measured. Under the maintainer's measured-core ruling that is disqualifying.
The mechanism
| head |
parks |
handler |
DraftFromSpellbook |
WaitingFor::SpellbookDraft (crates/engine/src/game/effects/spellbook.rs:83) |
engine_resolution_choices.rs:6428, GameAction::SubmitSpellbookDraft -> spellbook::complete_draft |
ManifestDread |
WaitingFor::ManifestDreadChoice (crates/engine/src/game/effects/manifest_dread.rs:66) |
engine_resolution_choices.rs:1785, GameAction::SelectCards |
Each head has two arms, and only one of them is the continuation case:
ManifestDread with count == 1 manifests synchronously via morph::manifest_card ->
zone_pipeline::move_object, which emits ZoneChanged — the existing _ => harvest already sees it.
count >= 2 parks ManifestDreadChoice.
DraftFromSpellbook's random arm conjures synchronously and emits
GameEvent::ObjectConjured { object_id } unconditionally (conjure.rs:221); the ZoneChanged at
conjure.rs:217 is gated on destination == Battlefield and both spellbook cards conjure to
Exile, so a ZoneChanged-only harvest sees nothing there. The choice arm parks SpellbookDraft.
Precedent — the same pattern already exists twice
engine_resolution_choices.rs already computes effects::chain_references_tracked_set(...) inside a
continuation handler and publishes when it is true:
// engine_resolution_choices.rs:979
.is_some_and(|continuation| effects::chain_references_tracked_set(&continuation.chain));
if continuation_consumes_tracked_set {
effects::publish_fresh_tracked_set(state, chosen.to_vec());
}
// engine_resolution_choices.rs:4325
.is_some_and(|cont| effects::chain_references_tracked_set(&cont.chain));
if continuation_consumes_tracked_set {
effects::publish_fresh_tracked_set(state, chosen.clone());
}
chain_references_tracked_set (effects/mod.rs:5118) is the same predicate as
next_sub_needs_tracked_set under a second name, so a continuation publish here would be gated
identically to the resolver publish — no new authority, an existing one applied at the right seam.
Candidate cards
| head |
card |
arm |
DraftFromSpellbook |
kayla's kindling |
choice arm (SpellbookDraft continuation) |
DraftFromSpellbook |
dazzling flameweaver |
random arm (synchronous, ObjectConjured) |
ManifestDread |
valgavoth's onslaught |
count >= 2 parks the continuation; count == 1 is synchronous |
What a future PR needs
- Runtime measurement of all three rows — none exists today. Both arms of each head need coverage
(continuation and synchronous), because they reach the publish through different code.
- A control that pins the continuation publish not double-publishing the synchronous path (the
count == 1 manifest and the random spellbook arm already emit their own events).
- A rich projection:
tracked_object_sets plus per-object zone/counters and the pending WaitingFor.
A sets-only projection has already produced wrong verdicts elsewhere in this lane.
Evidence provenance
Everything above is code-read and AST classification at BASE 9b7c66e30 — file/line citations are
direct reads. No runtime measurement exists for any of these three rows, and no runtime symptom is
asserted for them.
🤖 AI text below 🤖
Related: #6857
Summary
#6857's root cause is that
affected_objects_from_events(
crates/engine/src/game/effects/mod.rs:5590) has no arm for effect heads that affect objects withoutemitting a per-object event: they publish an empty tracked set and every downstream
TargetFilter::TrackedSetanaphor binds nothing.DraftFromSpellbook(2 gated rows) andManifestDread(1 gated row) are filed together because theyshare one mechanism: each parks a
WaitingFormid-resolution, and the objects they produce land onthe continuation's event vector — not on the resolver's. A compensating publish therefore belongs
in the continuation handler, not in
affected_objects_from_events.Both heads are excluded from the #6857 PR because they were dispositioned by code-read only and were
never runtime-measured. Under the maintainer's measured-core ruling that is disqualifying.
The mechanism
DraftFromSpellbookWaitingFor::SpellbookDraft(crates/engine/src/game/effects/spellbook.rs:83)engine_resolution_choices.rs:6428,GameAction::SubmitSpellbookDraft->spellbook::complete_draftManifestDreadWaitingFor::ManifestDreadChoice(crates/engine/src/game/effects/manifest_dread.rs:66)engine_resolution_choices.rs:1785,GameAction::SelectCardsEach head has two arms, and only one of them is the continuation case:
ManifestDreadwithcount == 1manifests synchronously viamorph::manifest_card->zone_pipeline::move_object, which emitsZoneChanged— the existing_ =>harvest already sees it.count >= 2parksManifestDreadChoice.DraftFromSpellbook's random arm conjures synchronously and emitsGameEvent::ObjectConjured { object_id }unconditionally (conjure.rs:221); theZoneChangedatconjure.rs:217is gated ondestination == Battlefieldand both spellbook cards conjure toExile, so a
ZoneChanged-only harvest sees nothing there. The choice arm parksSpellbookDraft.Precedent — the same pattern already exists twice
engine_resolution_choices.rsalready computeseffects::chain_references_tracked_set(...)inside acontinuation handler and publishes when it is true:
chain_references_tracked_set(effects/mod.rs:5118) is the same predicate asnext_sub_needs_tracked_setunder a second name, so a continuation publish here would be gatedidentically to the resolver publish — no new authority, an existing one applied at the right seam.
Candidate cards
DraftFromSpellbookSpellbookDraftcontinuation)DraftFromSpellbookObjectConjured)ManifestDreadcount >= 2parks the continuation;count == 1is synchronousWhat a future PR needs
(continuation and synchronous), because they reach the publish through different code.
count == 1manifest and the random spellbook arm already emit their own events).tracked_object_setsplus per-object zone/counters and the pendingWaitingFor.A
sets-only projection has already produced wrong verdicts elsewhere in this lane.Evidence provenance
Everything above is code-read and AST classification at BASE
9b7c66e30— file/line citations aredirect reads. No runtime measurement exists for any of these three rows, and no runtime symptom is
asserted for them.