A stack entry for an inherent rule ability reaches the client with an empty source name, and the client fills the hole with a literal "Unknown" of its own — then asks the card-image layer for a card by that name.
Reproduction
Reported from a real game: increasing speed off combat damage briefly shows a blank card on the stack.
Measured in the engine (inherent_rule_trigger_display_name.rs, red against d013272c1):
| trigger |
StackEntryKind::TriggeredAbility::source_name |
| monarch end-step draw (CR 725.2) |
"" |
| speed increase (CR 702.179d) |
"" |
Why the name is empty
CR 113.8 says an ability's source is the object it exists on. Four rules make an explicit exception and mint abilities with no source at all, each in the same words:
- CR 725.2 — the monarch: "These triggered abilities have no source."
- CR 726.2 — the initiative: "These triggered abilities have no source."
- CR 728.1 — rad counters: "This ability has no source."
- CR 702.179d — speed: "This ability has no source."
The engine models that faithfully, pushing them with the ObjectId(0) no-source sentinel. source_name is then filled by looking source_id up, which finds nothing, and the field's own doc comment records the outcome as intended: "Empty when the source has no name (synthetic game-rule triggers like monarch draw use ObjectId(0))."
Why that is a defect and not a wire detail
client/src/components/stack/StackEntry.tsx:
const sourceName = details?.source_name || triggerSourceName || sourceObj?.name || "Unknown";
const { src, isLoading } = useCardImage(sourceObj ? imageLookup.name : sourceName, { ... });
"Unknown" is game-facing text no rule produces, invented by the display layer because the wire carried nothing — the frontend deriving content, which CLAUDE.md forbids outright. It then travels into useCardImage, which searches for a card named "Unknown", finds none, and CardArtFallback paints a card-shaped box labelled "Unknown". That is the blank card the player sees.
Scope
The class is exactly the four sourceless rule triggers above, not one card. Monarch steal and initiative steal (CR 725.2 / CR 726.2) are unaffected — they carry the damaging creature as a real source.
Also found
Two engine descriptions cite CR 725.2 for an initiative ability; the initiative is CR 726.2 (verified against docs/MagicCompRules.txt:6250). CR 725.2 is the monarch.
Fix
PR attached. The engine names these abilities; the client's invented literal is deleted rather than replaced.
Open gap, deliberately not closed here
The stronger invariant "every stack entry names its source" is false today, and the PR states the measurement rather than implying otherwise: asserting it fails 53 existing tests (delayed triggers, CR 603.7d, carry neither a captured source nor a description) and a middle form fails 10 more (fixtures naming a source id they never insert). Both groups are still nameable by a viewer — the first through source_id, the second in any real game — so neither is this defect. Closing that is separate work.
A stack entry for an inherent rule ability reaches the client with an empty source name, and the client fills the hole with a literal
"Unknown"of its own — then asks the card-image layer for a card by that name.Reproduction
Reported from a real game: increasing speed off combat damage briefly shows a blank card on the stack.
Measured in the engine (
inherent_rule_trigger_display_name.rs, red againstd013272c1):StackEntryKind::TriggeredAbility::source_name""""Why the name is empty
CR 113.8 says an ability's source is the object it exists on. Four rules make an explicit exception and mint abilities with no source at all, each in the same words:
The engine models that faithfully, pushing them with the
ObjectId(0)no-source sentinel.source_nameis then filled by lookingsource_idup, which finds nothing, and the field's own doc comment records the outcome as intended: "Empty when the source has no name (synthetic game-rule triggers like monarch draw useObjectId(0))."Why that is a defect and not a wire detail
client/src/components/stack/StackEntry.tsx:"Unknown"is game-facing text no rule produces, invented by the display layer because the wire carried nothing — the frontend deriving content, which CLAUDE.md forbids outright. It then travels intouseCardImage, which searches for a card named "Unknown", finds none, andCardArtFallbackpaints a card-shaped box labelled "Unknown". That is the blank card the player sees.Scope
The class is exactly the four sourceless rule triggers above, not one card. Monarch steal and initiative steal (CR 725.2 / CR 726.2) are unaffected — they carry the damaging creature as a real source.
Also found
Two engine descriptions cite CR 725.2 for an initiative ability; the initiative is CR 726.2 (verified against
docs/MagicCompRules.txt:6250). CR 725.2 is the monarch.Fix
PR attached. The engine names these abilities; the client's invented literal is deleted rather than replaced.
Open gap, deliberately not closed here
The stronger invariant "every stack entry names its source" is false today, and the PR states the measurement rather than implying otherwise: asserting it fails 53 existing tests (delayed triggers, CR 603.7d, carry neither a captured source nor a description) and a middle form fails 10 more (fixtures naming a source id they never insert). Both groups are still nameable by a viewer — the first through
source_id, the second in any real game — so neither is this defect. Closing that is separate work.