wip
This commit is contained in:
171
docs/STADIUM_IDENTITY_SYSTEM.md
Normal file
171
docs/STADIUM_IDENTITY_SYSTEM.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user