This commit is contained in:
Trey t
2026-01-19 22:12:53 -06:00
parent 11c0ae70d2
commit a8b0491571
19 changed files with 1328 additions and 525 deletions

View 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.