fix: deduplicate card names globally before Scryfall batch lookup

enforce_deck_ratios can return the same card name under multiple
categories (e.g. Teferi's Protection in both protection + synergy).
all_card_names had no cross-category dedup, so get_cards_by_names
returned duplicate CardEntry objects that then failed the singleton
validator in CommanderDeck.

Fix: build all_card_names with dict.fromkeys() across all categories
so each name appears exactly once. Also deduplicate after Scryfall
lookup by name as a second safety net.
This commit is contained in:
KoalaTrapLord committed 2026-03-25 11:23:06 -04:00
1 parent 737863c55d
commit 88f1022d0e
1 file changed
+39 -12
@@ -168,22 +168,49 @@ def build_deck(request: BuildRequest) -> BuildResult:
# ── Step 7: Assemble final deck ──────────────────────────────
logger.info("Step 7: Assembling final deck")
# Convert to CardEntry list via Scryfall batch lookup
all_card_names = []
for names in adjusted.values():
all_card_names.extend(names)
# Build a globally-deduplicated name list. enforce_deck_ratios can
# return the same card name under multiple category keys (e.g. a
# protection card that also appears in synergy). Without this step
# get_cards_by_names produces duplicate CardEntry objects that
# immediately fail the CommanderDeck singleton validator.
seen_names: dict[str, str] = {} # name -> first category that claimed it
for cat, names in adjusted.items():
for name in names:
if name not in seen_names:
seen_names[name] = cat
else:
logger.warning(
"Duplicate card '%s' found in categories '%s' and '%s' — "
"keeping first assignment, dropping second",
name, seen_names[name], cat,
)
if len(seen_names) < sum(len(v) for v in adjusted.values()):
warnings.append(
"enforce_deck_ratios returned duplicate card names across categories; "
"duplicates were silently dropped before singleton validation."
)
all_card_names = list(seen_names.keys())
card_entries = scryfall.get_cards_by_names(all_card_names)
# Map back categories
name_to_cat: Dict[str, str] = {}
for cat, names in adjusted.items():
for name in names:
name_to_cat[name] = cat
# Safety-net: deduplicate CardEntry list by name in case Scryfall
# returns multiple printings for the same name.
seen_entry_names: set[str] = set()
unique_entries: List[CardEntry] = []
for entry in card_entries:
if entry.name in name_to_cat:
entry.category = name_to_cat[entry.name]
if entry.name not in seen_entry_names:
seen_entry_names.add(entry.name)
unique_entries.append(entry)
else:
logger.warning("Scryfall returned duplicate entry for '%s' — skipping", entry.name)
card_entries = unique_entries
# Map back categories
for entry in card_entries:
if entry.name in seen_names:
entry.category = seen_names[entry.name]
# Build the deck
elapsed = time.time() - start