refactor(ir): MTG-E85 enum-cheat burndown

What & why

Retires the MTG-E85 stringly-enum "cheats": multi-underscore ALL_CAPS enum values that mangled whole English phrases (comparisons, prepositional clauses, possessive cross-references) into fake atomic enums in the gold DSL — e.g. POWER_LESS_THAN_THIS_CREATURE'S_POWER, A_+1/+1_COUNTER_ON_IT, TOTAL_MANA_VALUE_X_OR_LESS_FROM_YOUR_GRAVEYARD. These hid real structure from the fine-tuning DSL (inflating the rare-node long tail) and were minted by two uppercase-mangle fallbacks rather than typed parsing.

Result: 114 of the 117 CHEAT-inventory values (7,639 / 7,772 occurrences, 99.0%) are gone, replaced by typed sub-trees. Inventory: MTG-E85_ENUM_CHEAT_INVENTORY.md (mtg-e85 branch).

How

16 commits, one per work unit, each independently gated (full pytest suite, repo-wide ruff, curated benchmark, exemplar-card round-trips) and adversarially verified before landing.

Root-cause kills (the two generators that could mint new cheats — both now hard-fail with ParseError on unmatched text instead of .upper().replace(' ','_')):

  • _detect_with_ref in ast/nodes/conditions/_dsl/selectors.py → typed dispatcher; with-ref :ref restricted to a closed pronoun enum + (card-name …).
  • qualifier_alias_for in ast/nodes/conditions/state.py.

Typed replacements by family:

family replacement occ
selector quantifiers (ONE_OR_MORE, ANY_NUMBER_OF, …) (at-least N) / (more-than N) / (any-number) 1,970
trigger keyword AT_BEGINNING_OF / AT_END_OF split into keyword AT + typed temporal :point 1,970
comparison atoms (OR_MORE, OR_FEWER, MORE_THAN, …) folded into existing DSLComparison GTE/LTE/GT 1,336
timing/duration phrases (BEGINNING_OF_NEXT_END_STEP, AT_END_OF_COMBAT, …) new (step-ref :edge :step :which :whose) node (CR 500.1, 603.7) 1,030
with-ref counter refs (A_+1/+1_COUNTER_ON_IT, …) routed to existing has-counters (CR 122.1) 930
with-ref stat comparisons (POWER_LESS_THAN_*'S_POWER, POWER_X_OR_LESS, …) pt-compare / pt-threshold with typed references 255
put-onto source refs (FROM_AMONG_THEM, MILLED_THIS_WAY, …) (them) / (this-way MILLED|DISCARDED) anaphora 365
zone-total refs (TOTAL_MANA_VALUE_X_OR_LESS_FROM_YOUR_GRAVEYARD, …) new total-stat-compare sub-tree 102
shared-property refs (THE_SAME_NAME_AS_*, …) new shares-property sub-tree 105
extremum refs (THE_GREATEST_POWER_AMONG_*, …) new stat-extremum value node 44
as-though-no-defender, chosen-color, frequency/activation limits, misc typed slots/nodes (:as-though-no DEFENDER, (chosen-color), counted limits, player-extremum) ~270

Ratchet: tests/ir/test_e85_no_stringly_enums.py pins all 126 dead values three ways (emitted-DSL scan over the curated set, GBNF-terminal check, ir/enums member check). New stringly enums cannot silently reappear.

No back-compat by design: dead enum values are deleted outright; from_dsl does not accept the old forms. The DSL is fine-tuning-only — training data is regenerated, not migrated.

Audit (curated 109 + seed-42 1,000 sample = 1,102 faces, 0 errors)

metric baseline 90a3c25c1 branch delta
parse_ok 99.8% 99.8% 0
ir_perfect 72.6% 72.6% 0
render_faithful 64.1% 64.2% +1
gold 62.7% 62.8% +1

115/117 inventory values absent from the sample; guard test 93 passed / 11 skipped.

Not addressed (excluded as legit-atomic, 3 distinct / 133 occ): BE_PUT_INTO_GRAVEYARD, PUT_INTO_GRAVEYARD, FOR_THE_REST_OF_THE_GAME — atomic event/duration concepts (CR 614.1 / 700.4). A follow-up typed zone-change node could retire the first two (~126 occ) if desired.

Author checklist

  • Full test suite green per unit and on the integrated branch
  • ruff check . clean
  • Benchmark non-regression (curated per unit; seeded quick n=1000 on the final branch)
  • Guard/ratchet test added (tests/ir/test_e85_no_stringly_enums.py)
  • CR citations in logic changes (603.7, 500.1, 122.1, 614.1, …)

🤖 Generated with Claude Code

Edited by Tagashy

Merge request reports

Loading