# Stadium Identity System How SportsTime handles stadium renames, new stadiums, and team relocations while preserving user data. ## Architecture Overview The system uses **immutable canonical IDs** for all references and **mutable display data** for names/locations. User data only stores canonical IDs, so it never needs migration when real-world changes occur. ``` User data → canonical IDs (stable) → current display names (via StadiumIdentityService) ``` ## Data Models ### Canonical Data (Synced from CloudKit) ```swift // Core identity - canonicalId NEVER changes CanonicalStadium: canonicalId: "stadium_nba_los_angeles_lakers" // Immutable name: "Crypto.com Arena" // Can change city: "Los Angeles" // Can change deprecatedAt: nil // Set when demolished CanonicalTeam: canonicalId: "team_mlb_athletics" // Immutable stadiumCanonicalId: "stadium_mlb_las_vegas" // Can change (relocation) city: "Las Vegas" // Can change deprecatedAt: nil // Set if team ceases // Historical name tracking StadiumAlias: aliasName: "staples center" // Lowercase for matching stadiumCanonicalId: "stadium_nba_los_angeles_lakers" validFrom: 2021-01-01 validUntil: 2022-12-25 TeamAlias: aliasValue: "Oakland" aliasType: .city teamCanonicalId: "team_mlb_athletics" validUntil: 2024-12-31 ``` ### User Data (Local Only) ```swift StadiumVisit: stadiumId: String // Canonical ID - stable reference stadiumNameAtVisit: String // Frozen at visit time for history homeTeamId: String? // Canonical ID awayTeamId: String? // Canonical ID homeTeamName: String? // Frozen at visit time awayTeamName: String? // Frozen at visit time Achievement: achievementTypeId: String // League structure ID (e.g., "mlb_al_west") sport: String? visitIdsSnapshot: Data // UUIDs of StadiumVisits - immutable ``` ## Scenario Handling ### Stadium Renames **Example:** Staples Center → Crypto.com Arena (December 2021) **What happens:** 1. CloudKit updates `CanonicalStadium.name` to "Crypto.com Arena" 2. CloudKit adds `StadiumAlias` for "Staples Center" with validity dates 3. Next sync updates local SwiftData 4. `canonicalId` remains `"stadium_nba_los_angeles_lakers"` **User impact:** None - Existing `StadiumVisit.stadiumId` still resolves correctly - `StadiumVisit.stadiumNameAtVisit` preserves "Staples Center" for historical display - Searching "Staples Center" still finds the stadium via alias lookup ### New Stadium Built **Example:** New Las Vegas A's stadium opens in 2028 **What happens:** 1. CloudKit adds new `CanonicalStadium` record with new canonical ID 2. Next sync creates record in SwiftData 3. Deterministic UUID generated: `SHA256(canonicalId) → UUID` 4. Stadium appears in app automatically **User impact:** None - New stadium available for visits and achievements - No migration needed ### Team Relocates **Example:** Oakland A's → Las Vegas A's (2024-2028) **What happens:** 1. CloudKit updates `CanonicalTeam`: - `canonicalId` stays `"team_mlb_athletics"` (never changes) - `stadiumCanonicalId` updated to Las Vegas stadium - `city` updated to "Las Vegas" 2. CloudKit adds `TeamAlias` for "Oakland" with end date 3. Old Oakland Coliseum gets `deprecatedAt` timestamp (soft delete) **User impact:** None - Old visits preserved: `StadiumVisit.stadiumId` = Oakland Coliseum (still valid) - Old visits show historical context: "Oakland A's at Oakland Coliseum" - Achievements adapt: "Complete AL West" now requires Las Vegas stadium ### Stadium Demolished / Team Ceases **What happens:** - Record gets `deprecatedAt` timestamp (soft delete, never hard delete) - Filtered from active queries: `predicate { $0.deprecatedAt == nil }` - Historical data fully preserved **User impact:** None - Visits remain in history, just not in active stadium lists - Achievements not revoked - you earned it, you keep it ## Identity Resolution `StadiumIdentityService` handles all lookups: ```swift // Find canonical ID from any name (current or historical) StadiumIdentityService.shared.canonicalId(forName: "Staples Center") // → "stadium_nba_los_angeles_lakers" // Get current display name from canonical ID StadiumIdentityService.shared.currentName(forCanonicalId: "stadium_nba_los_angeles_lakers") // → "Crypto.com Arena" // Get all historical names StadiumIdentityService.shared.allNames(forCanonicalId: "stadium_nba_los_angeles_lakers") // → ["Crypto.com Arena", "Staples Center", "Great Western Forum"] ``` ## Sync Safety During `CanonicalSyncService.mergeStadium()`: ```swift if let existing = try context.fetch(descriptor).first { // PRESERVE user customizations let savedNickname = existing.userNickname let savedFavorite = existing.isFavorite // UPDATE system fields only existing.name = remote.name existing.city = remote.city // canonicalId is NOT updated - it's immutable // RESTORE user customizations existing.userNickname = savedNickname existing.isFavorite = savedFavorite } ``` ## Impact Summary | Scenario | User Visits | Achievements | Historical Display | |----------|-------------|--------------|-------------------| | Stadium rename | ✅ Preserved | ✅ Preserved | Shows name at visit time | | New stadium | N/A | Available to earn | N/A | | Team relocates | ✅ Preserved | ✅ Logic adapts | Shows team + old stadium | | Stadium demolished | ✅ Preserved | ✅ Not revoked | Marked deprecated, visible in history | ## Key Principle **Immutable references, mutable display.** User data stores only canonical IDs. Display names are resolved at read time via `StadiumIdentityService`. This means we can update the real world without ever migrating user data.