Skip to content

M19 catalog accuracy and ability-IR scaling audit #819

Description

@ninthworld

Tracking issue. Two audits of the M19 catalog and the ability IR, run against the real
set data rather than against the implementation's own tests. Every finding is now a child
issue; this one carries the method, the headline numbers, the sequencing, and the list of
things that were checked and found correct, which lives nowhere else.

Method

Every one of the 299 files in crates/sage-engine/data/catalog/ was matched by name to its
MTGJSON M19.json record (315 printings) and diffed mechanically on mana cost, types,
supertypes, subtypes, power/toughness, loyalty, and colours. Then each of the 267 definitions
carrying an ability block was read side by side with its Oracle text. Every finding that
depends on engine semantics was checked against the code before being reported.

Baseline: main at 02efe72. All of it passes make check.

Children

Part 1 — card text accuracy: 17 of 299 cards are wrong

Issue What Scope
#820 Fifteen cards do not match the printed card — 12 wrong mana cost / P/T / subtypes / colours, 8 wrong costs, amounts, or trigger scopes, 5 both. Data only, one PR. Medium
#821 Two cards the IR cannot currently say — Resplendent Angel is missing an entire ability, Skyrider Patrol asks for two targets where the card names one. Needs vocabulary, not JSON. Medium
#822 Nothing gates the catalog against real printed data. build.rs checks shape; the behavioural tests assert against whatever the definition says and therefore cement the bug. Medium

The tests are the sharp end of #822. tests/m19_granted_abilities.rs:169 is named
issue_740_gift_of_paradise_enchants_a_land_and_gains_two_life and asserts life + 2 for a
card that gains three — the name, the assertion, and the comment all encode it, and
docs/generated/test-coverage.md shows the card as covered.

Part 2 — mechanic implementation

The engine is unusually disciplined about purity, and the scripted escape hatch is genuinely
empty — every bundled card is data. The findings are all one problem: the IR grows by naming
phrasings instead of by composing axes
, so the cost of the next card is paid in enum variants
and exhaustive match arms rather than in JSON. Effect is at 82 variants against 86 Effect::
arms in sage-server/src/rules_text/effects.rs alone.

The codebase already states the right principle. ObservedActivation's doc comment:

A small product of two independent filters rather than a closed list of named classes, for
the reason PermanentCount is one: the two questions a card asks about an activation … vary
independently, and a variant per pairing would grow the vocabulary once per card.

Every child below is a place where that principle was not applied.

Issue What Evidence Scope
#823 ActivatorScope cannot say "you". Sarkhan's Whelp fires on an opponent's Sarkhan. The gap is not in exclusions.json, whose neighbouring entry asserts the activator filter is complete. 1 card, 0 correct answers available Small
#824 One permanent filter for the non-targeting selectors. MassAffects, StaticAffects, DestroyAffects, PermanentCount all answer "which permanents?" and none composes. DestroyAffects is two variants for one card and disappears. 9 mass scopes used by exactly 1 card each Large
#825 Collapse TargetSpec's permanent arms onto that filter. ~29 variants, nearly all a product of axes already modelled. 20 of 36 catalog selectors used by exactly 1 card Large
#826 One Amount, not a ByCount and a ByAmount twin per verb. 11 variants for 6 verbs, with holes where no M19 card happened to need the pairing. CreateToken already solved this and its doc says why Medium
#827 One subject and one modification payload for pump / grant / restrict. The matrix is 9 variants with 4 empty cells — and the empty self-row keyword cell is why #821's Resplendent Angel is missing. chromium_the_mutable = 3 effects for 1 sentence Large
#828 Decompose the one-card bundled verbs. sacrifice_chosen_per_player bundles four rules steps and owns the only special case in Effect::target_groups; may_cast_exiled_this_way silently also bottoms a library. 60 of 121 catalog kind values used by exactly 1 card Large

#825's cost table, for scale — one new TargetSpec variant today touches, all wildcard-free:

Site Arms
sage-engine/src/resolve.rs — legality 31
sage-engine/src/actions/targeting.rs — candidates 27
sage-server/src/view/prompt.rs — the posed English 29
sage-server/src/rules_text/words.rs 86

Sequencing

#820 ──▶ #822                     data first, then the gate that holds it
  │
  ├──▶ #824 ──▶ #825              selectors: engine half, then the half that
  │                               generates the English a player reads
  ├──▶ #826 ──▶ #827              verbs: amount axis, then subject axis
  │              └── supersedes #821 if scheduled first
  └──▶ #828                       after the above; before another set is authored

#823                              independent, small, any time

#824#828 are behaviour-preserving refactors. The catalog JSON changes shape, no card's rules
change, and the existing behavioural suites are the safety net for all five.

Related

Checked and found correct

Recording these so the next audit does not re-open them. Each looked wrong on a reading of the
card and is right in the code:

Card Apparent problem Why it is fine
chaos_wand no effect bottoms the non-cast cards MayCastExiledThisWay does it — which is #828's complaint about the verb, not a card bug
fell_specter player_ref: target_player on a trigger the card does not target with named_subjects in triggers.rs:587 fixes the discarder from the event
amulet_of_safekeeping target: spell_on_stack for "counter that spell" same mechanism, SelfBecomesTarget arm
doublecast target: spell_on_stack for "copy that spell" delayed.rs:136 pre-fills the target (CR 603.7c)
skilled_animator no "for as long as this creature remains" duration that is Animate::until_end_of_turn's false default
runic_armasaur no "that isn't a mana ability" filter structural — a mana ability never reaches the stack (CR 605.3a)
enigma_drake base power 0 where the card prints * the catalog convention: base 0 plus defined_power, reading the graveyard

Also verified clean: all 14 keyword abilities across all 299 cards match the printed keyword
line, with exactly one exception (mistcaller's phantom flash, in #820); and every card the
catalog treats as vanilla really is vanilla.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions