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_refinast/nodes/conditions/_dsl/selectors.py→ typed dispatcher;with-ref :refrestricted to a closed pronoun enum +(card-name …).qualifier_alias_forinast/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, …)