release: 2.2.6 – refresh bracket list JSONs; finalize brackets compliance docs and UI polish

This commit is contained in:
matt 2025-09-04 19:28:48 -07:00
parent 4e03997923
commit 375349e56e
11 changed files with 734 additions and 16 deletions

View file

@ -12,6 +12,8 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
## [Unreleased]
## [2.2.6] - 2025-09-04
### Added
- Bracket policy enforcement: global pool-level prune for disallowed categories when limits are 0 (e.g., Game Changers in Brackets 12). Applies to both Web and headless runs.
- Inline enforcement UI: violations surface before the summary; Continue/Rerun disabled until you replace or remove flagged cards. Alternatives are role-consistent and exclude commander/locked/in-deck cards.
@ -20,10 +22,29 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
### Changed
- Spells and creatures phases apply bracket-aware pre-filters to reduce violations proactively.
- Compliance detection for Game Changers falls back to in-code constants when `config/card_lists/game_changers.json` is empty.
- Data refresh: updated static lists used by bracket compliance/enforcement with current card names and metadata:
- `config/card_lists/extra_turns.json`
- `config/card_lists/game_changers.json`
- `config/card_lists/mass_land_denial.json`
- `config/card_lists/tutors_nonland.json`
Each list includes `list_version: "manual-2025-09-04"` and `generated_at`.
### Fixed
- Summary/export mismatch in headless JSON runs where disallowed cards could be pruned from exports but appear in summaries; global prune ensures consistent state across phases and reports.
### Notes
- These lists underpin the bracket enforcement feature introduced in 2.2.5; shipping them as a follow-up release ensures consistent results across Web and headless runs.
## [2.2.5] - 2025-09-03
### Added
- Bracket WARN thresholds: `config/brackets.yml` supports optional `<category>_warn` keys (e.g., `tutors_nonland_warn`, `extra_turns_warn`). Compliance now returns PASS/WARN/FAIL; low brackets (12) conservatively WARN on presence of tutors/extra_turns when thresholds arent provided.
- Web UI compliance polish: the panel auto-opens on non-compliance (WARN/FAIL) and shows a colored overall status chip (green/WARN amber/red). WARN items now render as tiles with a subtle amber style and a WARN badge; tiles and enforcement actions remain FAIL-only.
- Tests: added coverage to ensure WARN thresholds from YAML are applied and that fallback WARN behavior appears for low brackets.
### Changed
- Web: flagged metadata now includes WARN categories with a `severity` field to support softer UI rendering for advisory cases.
## [2.2.4] - 2025-09-02
### Added

BIN
README.md

Binary file not shown.

View file

@ -100,10 +100,19 @@ def _canonicalize(name: str | None) -> str:
return s.casefold()
def _status_for(count: int, limit: Optional[int]) -> str:
def _status_for(count: int, limit: Optional[int], warn: Optional[int] = None) -> str:
# Unlimited hard limit -> always PASS (no WARN semantics without a cap)
if limit is None:
return "PASS"
return "PASS" if count <= int(limit) else "FAIL"
if count > int(limit):
return "FAIL"
# Soft guidance: if warn threshold provided and met, surface WARN
try:
if warn is not None and int(warn) > 0 and count >= int(warn):
return "WARN"
except Exception:
pass
return "PASS"
def evaluate_deck(
@ -155,7 +164,13 @@ def evaluate_deck(
flagged_names_disp = sorted({deck_canon_to_display.get(cn, cn) for cn in flagged_set})
c = len(flagged_set)
lim = limits.get(key)
status = _status_for(c, lim)
# Optional warn thresholds live alongside limits as "<key>_warn"
try:
warn_key = f"{key}_warn"
warn_val = limits.get(warn_key)
except Exception:
warn_val = None
status = _status_for(c, lim, warn=warn_val)
cat: CategoryFinding = {
"count": c,
"limit": lim,
@ -166,12 +181,27 @@ def evaluate_deck(
categories[key] = cat
if status == "FAIL":
messages.append(f"{key.replace('_',' ').title()}: {c} exceeds limit {lim}")
elif status == "WARN":
try:
if warn_val is not None:
messages.append(f"{key.replace('_',' ').title()}: {c} present (discouraged for this bracket)")
except Exception:
pass
# Conservative fallback: for low brackets (levels 12), tutors/extra-turns should WARN when present
# even if a warn threshold was not provided in YAML.
if status == "PASS" and level in (1, 2) and key in ("tutors_nonland", "extra_turns"):
try:
if (warn_val is None) and (lim is not None) and c > 0 and c <= int(lim):
categories[key]["status"] = "WARN"
messages.append(f"{key.replace('_',' ').title()}: {c} present (discouraged for this bracket)")
except Exception:
pass
# Two-card combos detection
combos = detect_combos(deck_cards.keys(), combos_path=combos_path)
cheap_early_pairs = [p for p in combos if p.cheap_early]
c_limit = limits.get("two_card_combos")
combos_status = _status_for(len(cheap_early_pairs), c_limit)
combos_status = _status_for(len(cheap_early_pairs), c_limit, warn=None)
categories["two_card_combos"] = {
"count": len(cheap_early_pairs),
"limit": c_limit,

View file

@ -51,3 +51,33 @@ def test_two_card_combination_detection_respects_cheap_early():
rep2 = evaluate_deck(deck, commander_name=None, bracket="optimized")
assert rep2["categories"]["two_card_combos"]["limit"] is None
assert rep2["overall"] == "PASS"
def test_warn_thresholds_in_yaml_are_applied():
# Exhibition: tutors_nonland_warn=1 -> WARN when a single tutor present (hard limit 3)
deck1 = {
# Use a non-"Game Changer" tutor to avoid hard fail in Exhibition
"Solve the Equation": _mk_card(["Bracket:TutorNonland"]),
"Cultivate": _mk_card([]),
}
rep1 = evaluate_deck(deck1, commander_name=None, bracket="exhibition")
assert rep1["level"] == 1
assert rep1["categories"]["tutors_nonland"]["status"] == "WARN"
assert rep1["overall"] == "WARN"
# Core: extra_turns_warn=1 -> WARN at 1, PASS at 0, FAIL above hard limit 3
deck2 = {
"Time Warp": _mk_card(["Bracket:ExtraTurn"]),
"Explore": _mk_card([]),
}
rep2 = evaluate_deck(deck2, commander_name=None, bracket="core")
assert rep2["level"] == 2
assert rep2["categories"]["extra_turns"]["limit"] == 3
assert rep2["categories"]["extra_turns"]["status"] in {"WARN", "PASS"}
# With two extra turns, still <= limit, but should at least WARN
deck3 = {
"Time Warp": _mk_card(["Bracket:ExtraTurn"]),
"Temporal Manipulation": _mk_card(["Bracket:ExtraTurn"]),
}
rep3 = evaluate_deck(deck3, commander_name=None, bracket="core")
assert rep3["categories"]["extra_turns"]["status"] == "WARN"

View file

@ -2273,12 +2273,12 @@ async def build_compliance_panel(request: Request) -> HTMLResponse:
seen_lower: set[str] = set()
for key, cat in cats.items():
try:
lim = cat.get('limit')
cnt = int(cat.get('count', 0) or 0)
if lim is None or cnt <= int(lim):
status = str(cat.get('status') or '').upper()
# Only surface tiles for WARN and FAIL
if status not in {"WARN", "FAIL"}:
continue
# For two-card combos, split pairs into individual cards and skip commander
if key == 'two_card_combos':
if key == 'two_card_combos' and status == 'FAIL':
# Prefer the structured combos list to ensure we only expand counted pairs
pairs = []
try:
@ -2316,6 +2316,7 @@ async def build_compliance_panel(request: Request) -> HTMLResponse:
'category': labels.get(key, key.replace('_',' ').title()),
'role': role,
'owned': (nm_l in owned_lower),
'severity': status,
})
continue
# Default handling for list/tag categories
@ -2332,6 +2333,7 @@ async def build_compliance_panel(request: Request) -> HTMLResponse:
'category': labels.get(key, key.replace('_',' ').title()),
'role': role,
'owned': (nm_l in owned_lower),
'severity': status,
})
except Exception:
continue

View file

@ -1,7 +1,18 @@
{% if compliance %}
<details id="compliance-panel" style="margin-top:.75rem;">
{% set non_compliant = compliance.overall is defined and (compliance.overall|string|lower != 'pass') %}
<details id="compliance-panel" style="margin-top:.75rem;" {% if non_compliant %}open{% endif %}>
<summary>Bracket compliance</summary>
<div class="muted" style="margin:.35rem 0;">Overall: <strong>{{ compliance.overall }}</strong> (Bracket: {{ compliance.bracket|title }}{{ ' #' ~ compliance.level if compliance.level is defined }})</div>
{% set ov = compliance.overall|string|lower %}
<div class="muted" style="margin:.35rem 0;">Overall:
{% if ov == 'fail' %}
<span class="chip" title="Overall bracket status"><span class="dot" style="background: var(--red-main);"></span> FAIL</span>
{% elif ov == 'warn' %}
<span class="chip" title="Overall bracket status"><span class="dot" style="background: var(--orange-main);"></span> WARN</span>
{% else %}
<span class="chip" title="Overall bracket status"><span class="dot" style="background: var(--green-main);"></span> PASS</span>
{% endif %}
(Bracket: {{ compliance.bracket|title }}{{ ' #' ~ compliance.level if compliance.level is defined }})
</div>
{% if compliance.messages and compliance.messages|length > 0 %}
<ul style="margin:.25rem 0; padding-left:1.25rem;">
{% for m in compliance.messages %}
@ -15,7 +26,8 @@
<h5 style="margin:.75rem 0 .35rem 0;">Flagged cards</h5>
<div class="card-grid">
{% for f in flagged_meta %}
<div class="card-tile" data-card-name="{{ f.name }}" data-role="{{ f.role or '' }}">
{% set sev = (f.severity or 'FAIL')|upper %}
<div class="card-tile" data-card-name="{{ f.name }}" data-role="{{ f.role or '' }}" {% if sev == 'FAIL' %}style="border-color: var(--red-main);"{% elif sev == 'WARN' %}style="border-color: var(--orange-main);"{% endif %}>
<a href="https://scryfall.com/search?q={{ f.name|urlencode }}" target="_blank" rel="noopener" class="img-btn" title="{{ f.name }}">
<img class="card-thumb" src="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal" alt="{{ f.name }} image" width="160" loading="lazy" decoding="async" data-lqip="1"
srcset="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=large 672w"
@ -23,7 +35,14 @@
</a>
<div class="owned-badge" title="{{ 'Owned' if f.owned else 'Not owned' }}" aria-label="{{ 'Owned' if f.owned else 'Not owned' }}">{% if f.owned %}✔{% else %}✖{% endif %}</div>
<div class="name">{{ f.name }}</div>
<div class="muted" style="text-align:center; font-size:12px;">{{ f.category }}{% if f.role %} • {{ f.role }}{% endif %}</div>
<div class="muted" style="text-align:center; font-size:12px; display:flex; gap:.35rem; justify-content:center; align-items:center; flex-wrap:wrap;">
<span>{{ f.category }}{% if f.role %} • {{ f.role }}{% endif %}</span>
{% if sev == 'FAIL' %}
<span class="chip" title="Severity: FAIL"><span class="dot" style="background: var(--red-main);"></span> FAIL</span>
{% elif sev == 'WARN' %}
<span class="chip" title="Severity: WARN"><span class="dot" style="background: var(--orange-main);"></span> WARN</span>
{% endif %}
</div>
<div style="display:flex; justify-content:center; margin-top:.25rem;">
{# Role-aware alternatives: pass the flagged name; server will infer role and exclude in-deck/locked #}
<button type="button" class="btn" hx-get="/build/alternatives" hx-vals='{"name": "{{ f.name }}"}' hx-target="#alts-flag-{{ loop.index0 }}" hx-swap="innerHTML" title="Suggest role-consistent replacements">Pick replacement…</button>

View file

@ -9,6 +9,9 @@ exhibition:
extra_turns: 0
tutors_nonland: 3
two_card_combos: 0
# Soft-warning guidance (does not affect FAIL threshold):
# Tutors are discouraged at this level; warn if any are present.
tutors_nonland_warn: 1
core:
level: 2
name: Core
@ -18,6 +21,10 @@ core:
extra_turns: 3
tutors_nonland: 3
two_card_combos: 0
# Soft-warning guidance: extra turns and tutors are allowed sparsely.
# Warn when any appear, fail only when exceeding the hard limit above.
extra_turns_warn: 1
tutors_nonland_warn: 1
upgraded:
level: 3
name: Upgraded

View file

@ -1 +1,56 @@
{"source_url": "test", "generated_at": "now", "cards": ["Time Warp"]}
{
"cards": [
"Alchemist's Gambit",
"Alrund's Epiphany",
"Beacon of Tomorrows",
"Capture of Jingzhou",
"Chance for Glory",
"Expropriate",
"Final Fortune",
"Gonti's Aether Heart",
"Ichormoon Gauntlet",
"Karn's Temporal Sundering",
"Last Chance",
"Lighthouse Chronologist",
"Lost Isle Calling",
"Magistrate's Scepter",
"Magosi, the Waterveil",
"Medomai the Ageless",
"Mu Yanling",
"Nexus of Fate",
"Notorious Throng",
"Part the Waterveil",
"Plea for Power",
"Ral Zarek",
"Regenerations Restored",
"Rise of the Eldrazi",
"Sage of Hours",
"Savor the Moment",
"Search the City",
"Second Chance",
"Seedtime",
"Stitch in Time",
"Teferi, Master of Time",
"Teferi, Timebender",
"Temporal Extortion",
"Temporal Manipulation",
"Temporal Mastery",
"Temporal Trespass",
"Time Sieve",
"Time Stretch",
"Time Warp",
"Timesifter",
"Timestream Navigator",
"Twice Upon a Time // Unlikely Meeting",
"Twice Upon a TimeUnlikely Meeting",
"Ugin's Nexus",
"Ultimecia, Time Sorceress",
"Ultimecia, Time Sorceress // Ultimecia, Omnipotent",
"Walk the Aeons",
"Wanderwine Prophets",
"Warrior's Oath",
"Wormfang Manta"
],
"list_version": "v1.0",
"generated_at": "2025-09-04"
}

View file

@ -1 +1,68 @@
{"source_url": "test", "generated_at": "now", "cards": []}
{
"cards": [
"Ad Nauseam",
"Ancient Tomb",
"Aura Shards",
"Bolas's Citadel",
"Braids, Cabal Minion",
"Chrome Mox",
"Coalition Victory",
"Consecrated Sphinx",
"Crop Rotation",
"Cyclonic Rift",
"Deflecting Swat",
"Demonic Tutor",
"Drannith Magistrate",
"Enlightened Tutor",
"Expropriate",
"Field of the Dead",
"Fierce Guardianship",
"Food Chain",
"Force of Will",
"Gaea's Cradle",
"Gamble",
"Gifts Ungiven",
"Glacial Chasm",
"Grand Arbiter Augustin IV",
"Grim Monolith",
"Humility",
"Imperial Seal",
"Intuition",
"Jeska's Will",
"Jin-Gitaxias, Core Augur",
"Kinnan, Bonder Prodigy",
"Lion's Eye Diamond",
"Mana Vault",
"Mishra's Workshop",
"Mox Diamond",
"Mystical Tutor",
"Narset, Parter of Veils",
"Natural Order",
"Necropotence",
"Notion Thief",
"Opposition Agent",
"Orcish Bowmasters",
"Panoptic Mirror",
"Rhystic Study",
"Seedborn Muse",
"Serra's Sanctum",
"Smothering Tithe",
"Survival of the Fittest",
"Sway of the Stars",
"Teferi's Protection",
"Tergrid, God of Fright",
"Tergrid, God of Fright // Tergrid's Lantern",
"Thassa's Oracle",
"The One Ring",
"The Tabernacle at Pendrell Vale",
"Underworld Breach",
"Urza, Lord High Artificer",
"Vampiric Tutor",
"Vorinclex, Voice of Hunger",
"Winota, Joiner of Forces",
"Worldly Tutor",
"Yuriko, the Tiger's Shadow"
],
"list_version": "v1.0",
"generated_at": "2025-09-04"
}

View file

@ -1 +1,79 @@
{"source_url": "test", "generated_at": "now", "cards": ["Armageddon"]}
{
"cards": [
"Acid Rain",
"Apocalypse",
"Armageddon",
"Back to Basics",
"Bearer of the Heavens",
"Bend or Break",
"Blood Moon",
"Boil",
"Boiling Seas",
"Boom // Bust",
"BoomBust",
"Break the Ice",
"Burning of Xinye",
"Cataclysm",
"Catastrophe",
"Choke",
"Cleansing",
"Contamination",
"Conversion",
"Curse of Marit Lage",
"Death Cloud",
"Decree of Annihilation",
"Desolation Angel",
"Destructive Force",
"Devastating Dreams",
"Devastation",
"Dimensional Breach",
"Disciple of Caelus Nin",
"Epicenter",
"Fall of the Thran",
"Flashfires",
"Gilt-Leaf Archdruid",
"Glaciers",
"Global Ruin",
"Hall of Gemstone",
"Harbinger of the Seas",
"Hokori, Dust Drinker",
"Impending Disaster",
"Infernal Darkness",
"Jokulhaups",
"Keldon Firebombers",
"Land Equilibrium",
"Magus of the Balance",
"Magus of the Moon",
"Myojin of Infinite Rage",
"Naked Singularity",
"Natural Balance",
"Obliterate",
"Omen of Fire",
"Raiding Party",
"Ravages of War",
"Razia's Purification",
"Reality Twist",
"Realm Razer",
"Restore Balance",
"Rising Waters",
"Ritual of Subdual",
"Ruination",
"Soulscour",
"Stasis",
"Static Orb",
"Storm Cauldron",
"Sunder",
"Sway of the Stars",
"Tectonic Break",
"Thoughts of Ruin",
"Tsunami",
"Wildfire",
"Winter Moon",
"Winter Orb",
"Worldfire",
"Worldpurge",
"Worldslayer"
],
"list_version": "v1.0",
"generated_at": "2025-09-04"
}

View file

@ -1 +1,410 @@
{"source_url": "test", "generated_at": "now", "cards": ["Demonic Tutor"]}
{
"cards": [
"Academy Rector",
"Aether Searcher",
"Altar of Bone",
"Amrou Scout",
"Analyze the Pollen",
"Anchor to Reality",
"Archdruid's Charm",
"Archmage Ascension",
"Arcum Dagsson",
"Arena Rector",
"Artificer's Intuition",
"Assembly Hall",
"Auratouched Mage",
"Aurochs Herd",
"Axgard Armory",
"Ayara's Oathsworn",
"Begin the Invasion",
"Behold the Beyond",
"Beseech the Mirror",
"Beseech the Queen",
"Bifurcate",
"Bilbo, Birthday Celebrant",
"Birthing Pod",
"Bitterheart Witch",
"Blightspeaker",
"Blood Speaker",
"Boggart Harbinger",
"Bog Glider",
"Boonweaver Giant",
"Brainspoil",
"Brightglass Gearhulk",
"Bringer of the Black Dawn",
"Bring to Light",
"Brutalizer Exarch",
"Buried Alive",
"Burning-Rune Demon",
"Call the Gatewatch",
"Captain Sisay",
"Caradora, Heart of Alacria",
"Case of the Stashed Skeleton",
"Cateran Brute",
"Cateran Enforcer",
"Cateran Kidnappers",
"Cateran Overlord",
"Cateran Persuader",
"Cateran Slaver",
"Cateran Summons",
"Central ElevatorPromising Stairs",
"Central Elevator // Promising Stairs",
"Chandra, Heart of Fire",
"Chord of Calling",
"Citanul Flute",
"Clarion Ultimatum",
"Cloud, Midgar Mercenary",
"Clutch of the Undercity",
"Conduit of Ruin",
"Conflux",
"Congregation at Dawn",
"Corpse Connoisseur",
"Corpse Harvester",
"Coveted Prize",
"Cruel Tutor",
"Curse of Misfortunes",
"Cynical Loner",
"Dark Petition",
"Deadeye Quartermaster",
"Deathbellow War Cry",
"Defense of the Heart",
"Defiant Falcon",
"Defiant Vanguard",
"Delivery Moogle",
"Demonic Bargain",
"Demonic Collusion",
"Demonic Consultation",
"Demonic Counsel",
"Demonic Tutor",
"Diabolic Intent",
"Diabolic Revelation",
"Diabolic Tutor",
"Dig Up",
"Dimir House Guard",
"Dimir Infiltrator",
"Dimir Machinations",
"Disciples of Gix",
"Distant Memories",
"Dizzy Spell",
"Djeru, With Eyes Open",
"Doomsday",
"Doubling Chant",
"Draconic Muralists",
"Dragon's Approach",
"Dragonstorm",
"Drift of Phantasms",
"Dwarven Recruiter",
"Ecological Appreciation",
"Eerie Procession",
"Eladamri's Call",
"Eldritch Evolution",
"Elvish Harbinger",
"Emergent Ultimatum",
"Enduring Ideal",
"Enigmatic Incarnation",
"Enlightened Tutor",
"Entomb",
"Ethereal Usher",
"Evolving Door",
"Eye of Ugin",
"Fabricate",
"Faerie Harbinger",
"Fang-Druid Summoner",
"Fauna Shaman",
"Fervent Mastery",
"Fiend Artisan",
"Fierce Empath",
"Fighter Class",
"Finale of Devastation",
"Final Parting",
"Firemind's Foresight",
"Flamekin Harbinger",
"Fleshwrither",
"Forerunner of the Coalition",
"Forerunner of the Empire",
"Forerunner of the Heralds",
"Forerunner of the Legion",
"Forging the Tyrite Sword",
"From Beyond",
"From Father to Son",
"Frostpyre Arcanist",
"Fugitive of the Judoon",
"Gamble",
"Garruk, Caller of Beasts",
"Garruk Relentless",
"Garruk Relentless // Garruk, the Veil-Cursed",
"Garruk, Unleashed",
"General Tazri",
"Giant Harbinger",
"Gifts Ungiven",
"Goblin Engineer",
"Goblin Matron",
"Goblin Recruiter",
"Godo, Bandit Warlord",
"Gravebreaker Lamia",
"Green Sun's Zenith",
"Grim Servant",
"Grim Tutor",
"Grozoth",
"Guardian Sunmare",
"Guidelight Pathmaker",
"Heliod's Pilgrim",
"Hibernation's End",
"Higure, the Still Wind",
"Hoarding Broodlord",
"Hoarding Dragon",
"Homing Sliver",
"Honored Knight-Captain",
"Hour of Victory",
"Huatli, Poet of Unity",
"Huatli, Poet of Unity // Roar of the Fifth People",
"Idyllic Tutor",
"Ignite the Beacon",
"Illicit Shipment",
"Imperial Hellkite",
"Imperial Recruiter",
"Imperial Seal",
"Iname as One",
"Iname, Death Aspect",
"Increasing Ambition",
"Infernal Tutor",
"Insatiable Avarice",
"Insidious Dreams",
"Instrument of the Bards",
"Intuition",
"Invasion of Arcavios",
"Invasion of Arcavios // Invocation of the Founders",
"Invasion of Ikoria",
"Invasion of Ikoria // Zilortha, Apex of Ikoria",
"Invasion of Theros",
"Invasion of Theros // Ephara, Ever-Sheltering",
"Inventors' Fair",
"InvertInvent",
"Invert // Invent",
"Iron Man, Titan of Innovation",
"Isperia the Inscrutable",
"Jarad's Orders",
"Kaho, Minamo Historian",
"Kaito Shizuki",
"Kasmina, Enigma Sage",
"Kellan, the Fae-BloodedBirthright Boon",
"Kellan, the Fae-Blooded // Birthright Boon",
"Kithkin Harbinger",
"Kuldotha Forgemaster",
"Lagomos, Hand of Hatred",
"Library of Lat-Nam",
"Lifespinner",
"Light-Paws, Emperor's Voice",
"Liliana Vess",
"Lim-Dûl's Vault",
"Lin Sivvi, Defiant Hero",
"Lively Dirge",
"Long-Term Plans",
"Lost Auramancers",
"Lotuslight Dancers",
"Loyal Inventor",
"Maelstrom of the Spirit Dragon",
"Magda, Brazen Outlaw",
"Magus of the Order",
"Mangara's Tome",
"Maralen of the Mornsong",
"March of Burgeoning Life",
"Mask of the Mimic",
"Mastermind's Acquisition",
"Mausoleum Secrets",
"Merchant Scroll",
"Merrow Harbinger",
"Micromancer",
"Mimeofacture",
"Mishra, Artificer Prodigy",
"Moggcatcher",
"Momir Vig, Simic Visionary",
"Moon-Blessed Cleric",
"Moonsilver Key",
"Muddle the Mixture",
"Mwonvuli Beast Tracker",
"Myr Kinsmith",
"Myr Turbine",
"Mystical Teachings",
"Mystical Tutor",
"Mythos of Brokkos",
"Nahiri, the Harbinger",
"Natural Order",
"Nature's Rhythm",
"Nazahn, Revered Bladesmith",
"Neoform",
"Netherborn Phalanx",
"Night Dealings",
"Nissa Revane",
"Noble Benefactor",
"Open the Armory",
"Opposition Agent",
"Oriq Loremage",
"Oswald Fiddlebender",
"Pack Hunt",
"Parallel Thoughts",
"Pattern Matcher",
"Pattern of Rebirth",
"Perplex",
"Personal Tutor",
"Phantom Carriage",
"Planar Bridge",
"Planar Portal",
"Plea for Guidance",
"Priest of the Wakening Sun",
"Primal Command",
"Prime Speaker Vannifar",
"Profane Tutor",
"Protean Hulk",
"Pyre of Heroes",
"Quest for the Holy Relic",
"Quiet Speculation",
"Ramosian Captain",
"Ramosian Commander",
"Ramosian Lieutenant",
"Ramosian Sergeant",
"Ramosian Sky Marshal",
"Ranger-Captain of Eos",
"Ranger of Eos",
"Ratcatcher",
"Rathi Assassin",
"Rathi Fiend",
"Rathi Intimidator",
"Razaketh's Rite",
"Razaketh, the Foulblooded",
"Reckless Handling",
"Recruiter of the Guard",
"Relic Seeker",
"Remembrance",
"Repurposing Bay",
"Reshape",
"Rhystic Tutor",
"Ring of Three Wishes",
"Ringsight",
"Rocco, Cabaretti Caterer",
"Rootless Yew",
"Runed Crown",
"Runeforge Champion",
"Rune-Scarred Demon",
"Rushed Rebirth",
"Saheeli Rai",
"Samut, the Tested",
"Sanctum of All",
"Sanctum of Ugin",
"Sarkhan, Dragonsoul",
"Sarkhan's Triumph",
"Sarkhan Unbroken",
"Savage Order",
"Sazh Katzroy",
"Scheming Symmetry",
"Scion of the Ur-Dragon",
"Scour for Scrap",
"Scrapyard Recombiner",
"Seahunter",
"Search for Glory",
"Secret Salvage",
"Self-Assembler",
"Servant of the Stinger",
"Shadowborn Apostle",
"Shadow-Rite Priest",
"Shared Summons",
"Shield-Wall Sentinel",
"Shred Memory",
"Shrine Steward",
"Sidisi, Undead Vizier",
"Signal the Clans",
"Sisay, Weatherlight Captain",
"Sivitri, Dragon Master",
"Skyship Weatherlight",
"Skyshroud Poacher",
"Sliver Overlord",
"Solve the Equation",
"Sovereigns of Lost Alara",
"Spellseeker",
"Sphinx Ambassador",
"Sphinx Summoner",
"Starfield Shepherd",
"Steelshaper Apprentice",
"Steelshaper's Gift",
"Step Through",
"Sterling Grove",
"Stoneforge Mystic",
"Stonehewer Giant",
"Summoner's Pact",
"Sunforger",
"SupplyDemand",
"Supply // Demand",
"Survival of the Fittest",
"Sylvan Tutor",
"Tainted Pact",
"Taj-Nar Swordsmith",
"Tallowisp",
"Tamiyo's Journal",
"Tempest Hawk",
"Templar Knight",
"Tezzeret, Artifice Master",
"Tezzeret, Cruel Captain",
"Tezzeret the Seeker",
"Thalia's Lancers",
"The Caves of Androzani",
"The Creation of Avacyn",
"The Cruelty of Gix",
"The Eleventh Hour",
"The Five Doctors",
"The Hunger Tide Rises",
"The Huntsman's Redemption",
"The Seriema",
"Thornvault Forager",
"Threats Undetected",
"Three Dreams",
"Tiamat",
"Time of Need",
"Tolaria West",
"Tooth and Nail",
"Totem-Guide Hartebeest",
"Transit Mage",
"Transmutation Font",
"Transmute Artifact",
"Trapmaker's Snare",
"Traverse the Ulvenwald",
"Treasure Chest",
"Treasure Mage",
"Treefolk Harbinger",
"Tribute Mage",
"Trinket Mage",
"Trophy Mage",
"Twice Upon a TimeUnlikely Meeting",
"Twice Upon a Time // Unlikely Meeting",
"Ugin, Eye of the Storms",
"Uncage the Menagerie",
"Unmarked Grave",
"Urza's Saga",
"Urza's Sylex",
"Vampiric Tutor",
"Varragoth, Bloodsky Sire",
"Vedalken Aethermage",
"Verdant Succession",
"Vexing Puzzlebox",
"Vile Entomber",
"Vivien, Monsters' Advocate",
"Vivien on the Hunt",
"Vizier of the Anointed",
"Wargate",
"War of the Last Alliance",
"Waterlogged Teachings",
"Waterlogged Teachings // Inundated Archive",
"Weird Harvest",
"Whir of Invention",
"Wild Pair",
"Wild Research",
"Wirewood Herald",
"Wishclaw Talisman",
"Woodland Bellower",
"Worldly Tutor",
"Yisan, the Wanderer Bard",
"Zirilan of the Claw",
"Zur the Enchanter"
],
"list_version": "v1.0",
"generated_at": "2025-09-04"
}