Image Path Collision Disambiguation
| Date | 2026-04-08 |
| Status | Accepted |
| Decision Makers | @benjaminW78 |
Context and Problem Statement
The SWU importer generates image destination paths from the TCGCSV extNumber (card collector number) and finish suffix:
destination: `${groupSlug}/${printKey}${slugSuffix}.${imageExt}`This assumes extNumber is unique per card within an expansion. However, in SWU, different cards can share the same collector number within the same set. This happens in two scenarios:
- TCGCSV data errors — e.g., Massassi Group Marines assigned
148/264instead of146/264in A Lawless Time, colliding with Smuggler’s YT-2400 which is correctly148/264 - Genuine shared numbers — e.g., Willrow Hood and Bardottan Ornithopter both officially numbered
571(Foil) and817(Hyperspace Foil) in Secrets of Power - Promo sets — Judge Promos, Organized Play Promos, and Prerelease Promos routinely assign the same number to different promo waves
This causes 67 products across 21 collision groups in production where the last card processed overwrites the other’s image file on S3. For example, Massassi Group Marines displays Smuggler’s YT-2400’s image.
Decision Drivers
- Image destination paths must be unique per product within an expansion
- The fix should only affect colliding products — changing all ~5000 SWU image paths would trigger a full S3 re-upload on next import
- The solution must handle both TCGCSV data errors and genuinely shared card numbers
- Serialized Gold/Rose Gold variants of the same card already have unique paths via
slugSuffix(_serialized-gold,_serialized-rose-gold) and are not affected
Considered Options
- Post-processing deduplication — After building all printings, detect duplicate image destinations and prepend
cardSlugonly to colliding paths - Always include cardSlug in image path — Change the destination template to
${groupSlug}/${cardSlug}_${printKey}${slugSuffix}.${imageExt}for all products - Use SWU API cardNumber to correct TCGCSV errors — When the API has a different card number, prefer it over the TCGCSV extNumber
Decision Outcome
Chosen option: Post-processing deduplication, because it fixes all collision types (TCGCSV errors, genuine shared numbers, promo sets) while leaving ~5000 non-colliding product images untouched.
Option 2 was rejected because it would change every SWU image path, triggering a full re-upload of all images on the next import run.
Option 3 was rejected because it only fixes TCGCSV data errors, not genuinely shared card numbers or promo sets.
Implementation
Logic Flow
Impacted Files
| File | Change |
|---|---|
packages/games/game-importer/src/swu-importer.ts | Added post-loop deduplication step in getCards() that detects and disambiguates colliding image destinations |
Key Code
// After building all printings, detect duplicate image destinations
const destinationToEntries = new Map<string, { cardKey: string; printingIndex: number }[]>()
for (const [cardKey, card] of cardMap) {
for (let i = 0; i < card.printings.length; i++) {
const dest = card.printings[i].image?.destination
if (!dest) continue
if (!destinationToEntries.has(dest)) destinationToEntries.set(dest, [])
destinationToEntries.get(dest)?.push({ cardKey, printingIndex: i })
}
}
// Disambiguate only collisions by prepending cardSlug to filename
for (const [dest, entries] of destinationToEntries) {
const uniqueCards = new Set(entries.map((e) => e.cardKey))
if (uniqueCards.size <= 1) continue // same card, different finish -- not a collision
for (const { cardKey, printingIndex } of entries) {
const card = cardMap.get(cardKey)!
const printing = card.printings[printingIndex]
const image = printing.image!
const parts = image.destination!.split("/")
parts[parts.length - 1] = `${card.slug}_${parts[parts.length - 1]}`
image.destination = parts.join("/")
}
}Consequences
Positive
- Fixes all 21 collision groups (67 products) in one shot
- Non-colliding images (~5000 products) keep their existing S3 paths — no unnecessary re-upload
- Handles all collision types: TCGCSV data errors, genuine shared numbers, promo sets
- Logs a warning for each disambiguated group, providing visibility into upstream data issues
- Future collisions (new sets, new promos) are automatically handled
Negative
- The ~67 colliding products will get new image paths, requiring a one-time re-upload on next import
More Information
- Related: TCGPlayer images as primary source — describes how image destinations are built
- Related: Serialized Gold / Rose Gold as separate products — these already have unique paths via
generatePrintingGroupKeyand are not affected - PR: #2783Â