Add StadiumAlias CloudKit sync and offline-first data architecture

- Add CKStadiumAlias model for CloudKit record mapping
- Add fetchStadiumAliases/fetchStadiumAliasChanges to CloudKitService
- Add syncStadiumAliases to CanonicalSyncService for delta sync
- Add subscribeToStadiumAliasUpdates for push notifications
- Update cloudkit_import.py with --stadium-aliases-only option

Data Architecture Updates:
- Remove obsolete provider files (CanonicalDataProvider, CloudKitDataProvider, StubDataProvider)
- AppDataProvider now reads exclusively from SwiftData
- Add background CloudKit sync on app startup (non-blocking)
- Document data architecture in CLAUDE.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-08 22:20:07 -06:00
parent 588938d2a1
commit 1ee47df53e
12 changed files with 482 additions and 780 deletions

View File

@@ -50,11 +50,75 @@ This is an iOS app for planning multi-stop sports road trips. It uses **Clean MV
- `Services/POISearchService` - Finds nearby restaurants, attractions via MKLocalSearch
- `Services/PDFAssetPrefetcher` - Parallel prefetching of all PDF assets
### Data Storage Strategy
### Data Architecture (Offline-First)
- **CloudKit Public DB**: Read-only schedules, stadiums, teams (shared across all users)
- **SwiftData Local**: User's saved trips, preferences, cached schedules
- **No network dependency** for trip planning once schedules are synced
**CRITICAL: `AppDataProvider.shared` is the ONLY source of truth for canonical data.**
All code that reads stadiums, teams, games, or league structure MUST use `AppDataProvider.shared`. Never access CloudKit or SwiftData directly for this data.
```
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Bundled JSON │ │ SwiftData │ │ CloudKit │
│ (App Bundle) │ │ (Local Store) │ │ (Remote Sync) │
└────────┬─────────┘ └────────▲─────────┘ └────────┬─────────┘
│ │ │
│ Bootstrap │ Read │ Background
│ (first launch) │ │ Sync
▼ │ ▼
┌─────────────────────────────────┴────────────────────────────────────┐
│ AppDataProvider.shared │
│ (Single Source of Truth) │
└─────────────────────────────────┬────────────────────────────────────┘
All Features, ViewModels, Services
```
**App Startup Flow:**
1. **Bootstrap** (first launch only): `BootstrapService` loads bundled JSON → SwiftData
2. **Configure**: `AppDataProvider.shared.configure(with: context)`
3. **Load**: `AppDataProvider.shared.loadInitialData()` reads SwiftData into memory
4. **App usable immediately** with local data
5. **Background sync**: `CanonicalSyncService.syncAll()` fetches CloudKit → updates SwiftData (non-blocking)
6. **Reload**: After sync completes, `loadInitialData()` refreshes in-memory cache
**Offline Handling:**
- SwiftData always has data (from bootstrap or last successful CloudKit sync)
- If CloudKit unavailable, app continues with existing local data
- First launch + offline = bootstrap data used
**Canonical Data Models** (in SwiftData, synced from CloudKit):
- `CanonicalStadium``Stadium` (domain)
- `CanonicalTeam``Team` (domain)
- `CanonicalGame``Game` (domain)
- `LeagueStructureModel`
- `TeamAlias`, `StadiumAlias`
**User Data Models** (local only, not synced):
- `SavedTrip`, `StadiumVisit`, `UserPreferences`, `Achievement`
**Correct Usage:**
```swift
// CORRECT - Use AppDataProvider
let stadiums = AppDataProvider.shared.stadiums
let teams = AppDataProvider.shared.teams
let games = try await AppDataProvider.shared.fetchGames(sports: sports, startDate: start, endDate: end)
let richGames = try await AppDataProvider.shared.fetchRichGames(...)
// WRONG - Never access CloudKit directly for reads
let stadiums = try await CloudKitService.shared.fetchStadiums() // NO!
// WRONG - Never fetch canonical data from SwiftData directly
let descriptor = FetchDescriptor<CanonicalStadium>()
let stadiums = try context.fetch(descriptor) // NO! (except in DataProvider/Sync/Bootstrap)
```
**Allowed Direct SwiftData Access:**
- `DataProvider.swift` - It IS the data provider
- `CanonicalSyncService.swift` - CloudKit → SwiftData sync
- `BootstrapService.swift` - Initial data population
- `StadiumIdentityService.swift` - Specialized identity resolution for stadium renames
- User data (e.g., `StadiumVisit`) - Not canonical data
### Key Data Flow