# Delta Sync Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Remove arbitrary date restrictions from game browsing and implement proper delta sync using CloudKit modificationDate. **Architecture:** Update CloudKitService to query by `modificationDate` instead of game `dateTime`. First sync fetches all records, subsequent syncs fetch only modified records. Rename DataProvider methods to clarify local vs network semantics. **Tech Stack:** Swift, SwiftData, CloudKit, XCTest --- ## Task 1: Update CloudKitService.fetchStadiumsForSync **Files:** - Modify: `SportsTime/Core/Services/CloudKitService.swift:217-231` **Step 1: Update method signature and implementation** Change from: ```swift func fetchStadiumsForSync() async throws -> [SyncStadium] ``` To: ```swift /// Fetch stadiums for sync operations /// - Parameter lastSync: If nil, fetches all stadiums. If provided, fetches only stadiums modified since that date. func fetchStadiumsForSync(since lastSync: Date?) async throws -> [SyncStadium] { let predicate: NSPredicate if let lastSync = lastSync { predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate) } else { predicate = NSPredicate(value: true) } let query = CKQuery(recordType: CKRecordType.stadium, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) return results.compactMap { result -> SyncStadium? in guard case .success(let record) = result.1 else { return nil } let ckStadium = CKStadium(record: record) guard let stadium = ckStadium.stadium, let canonicalId = ckStadium.canonicalId else { return nil } return SyncStadium(stadium: stadium, canonicalId: canonicalId) } } ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/CloudKitService.swift git commit -m "feat(sync): add lastSync parameter to fetchStadiumsForSync" ``` --- ## Task 2: Update CloudKitService.fetchTeamsForSync **Files:** - Modify: `SportsTime/Core/Services/CloudKitService.swift:233-249` **Step 1: Update method signature and implementation** Change from per-sport to all teams with delta sync: ```swift /// Fetch teams for sync operations /// - Parameter lastSync: If nil, fetches all teams. If provided, fetches only teams modified since that date. func fetchTeamsForSync(since lastSync: Date?) async throws -> [SyncTeam] { let predicate: NSPredicate if let lastSync = lastSync { predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate) } else { predicate = NSPredicate(value: true) } let query = CKQuery(recordType: CKRecordType.team, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) return results.compactMap { result -> SyncTeam? in guard case .success(let record) = result.1 else { return nil } let ckTeam = CKTeam(record: record) guard let team = ckTeam.team, let canonicalId = ckTeam.canonicalId, let stadiumCanonicalId = ckTeam.stadiumCanonicalId else { return nil } return SyncTeam(team: team, canonicalId: canonicalId, stadiumCanonicalId: stadiumCanonicalId) } } ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/CloudKitService.swift git commit -m "feat(sync): change fetchTeamsForSync to delta sync (all teams, not per-sport)" ``` --- ## Task 3: Update CloudKitService.fetchGamesForSync **Files:** - Modify: `SportsTime/Core/Services/CloudKitService.swift:251-301` **Step 1: Update method signature and implementation** Change from date range to delta sync: ```swift /// Fetch games for sync operations /// - Parameter lastSync: If nil, fetches all games. If provided, fetches only games modified since that date. func fetchGamesForSync(since lastSync: Date?) async throws -> [SyncGame] { let predicate: NSPredicate if let lastSync = lastSync { predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate) } else { predicate = NSPredicate(value: true) } let query = CKQuery(recordType: CKRecordType.game, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) return results.compactMap { result -> SyncGame? in guard case .success(let record) = result.1 else { return nil } let ckGame = CKGame(record: record) guard let canonicalId = ckGame.canonicalId, let homeTeamCanonicalId = ckGame.homeTeamCanonicalId, let awayTeamCanonicalId = ckGame.awayTeamCanonicalId, let stadiumCanonicalId = ckGame.stadiumCanonicalId else { return nil } guard let game = ckGame.game( homeTeamId: homeTeamCanonicalId, awayTeamId: awayTeamCanonicalId, stadiumId: stadiumCanonicalId ) else { return nil } return SyncGame( game: game, canonicalId: canonicalId, homeTeamCanonicalId: homeTeamCanonicalId, awayTeamCanonicalId: awayTeamCanonicalId, stadiumCanonicalId: stadiumCanonicalId ) }.sorted { $0.game.dateTime < $1.game.dateTime } } ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/CloudKitService.swift git commit -m "feat(sync): change fetchGamesForSync to delta sync by modificationDate" ``` --- ## Task 4: Update CanonicalSyncService.syncStadiums **Files:** - Modify: `SportsTime/Core/Services/CanonicalSyncService.swift:208-236` **Step 1: Pass lastSync to CloudKit fetch** Change line 214 from: ```swift let syncStadiums = try await cloudKitService.fetchStadiumsForSync() ``` To: ```swift let syncStadiums = try await cloudKitService.fetchStadiumsForSync(since: lastSync) ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/CanonicalSyncService.swift git commit -m "feat(sync): pass lastSync to stadium sync for delta updates" ``` --- ## Task 5: Update CanonicalSyncService.syncTeams **Files:** - Modify: `SportsTime/Core/Services/CanonicalSyncService.swift:238-271` **Step 1: Simplify to single CloudKit call** Replace the for-loop that calls per-sport: ```swift @MainActor private func syncTeams( context: ModelContext, since lastSync: Date? ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) { // Single call for all teams (no per-sport loop) let allSyncTeams = try await cloudKitService.fetchTeamsForSync(since: lastSync) var updated = 0 var skippedIncompatible = 0 var skippedOlder = 0 for syncTeam in allSyncTeams { let result = try mergeTeam( syncTeam.team, canonicalId: syncTeam.canonicalId, stadiumCanonicalId: syncTeam.stadiumCanonicalId, context: context ) switch result { case .applied: updated += 1 case .skippedIncompatible: skippedIncompatible += 1 case .skippedOlder: skippedOlder += 1 } } return (updated, skippedIncompatible, skippedOlder) } ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/CanonicalSyncService.swift git commit -m "feat(sync): simplify team sync to single CloudKit call with delta" ``` --- ## Task 6: Update CanonicalSyncService.syncGames **Files:** - Modify: `SportsTime/Core/Services/CanonicalSyncService.swift:273-311` **Step 1: Remove date range, use delta sync** Replace lines 278-286: ```swift @MainActor private func syncGames( context: ModelContext, since lastSync: Date? ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) { // Delta sync: nil = all games, Date = only modified since let syncGames = try await cloudKitService.fetchGamesForSync(since: lastSync) var updated = 0 var skippedIncompatible = 0 var skippedOlder = 0 for syncGame in syncGames { let result = try mergeGame( syncGame.game, canonicalId: syncGame.canonicalId, homeTeamCanonicalId: syncGame.homeTeamCanonicalId, awayTeamCanonicalId: syncGame.awayTeamCanonicalId, stadiumCanonicalId: syncGame.stadiumCanonicalId, context: context ) switch result { case .applied: updated += 1 case .skippedIncompatible: skippedIncompatible += 1 case .skippedOlder: skippedOlder += 1 } } return (updated, skippedIncompatible, skippedOlder) } ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/CanonicalSyncService.swift git commit -m "feat(sync): remove date range from game sync, use modificationDate delta" ``` --- ## Task 7: Rename DataProvider.fetchGames to filterGames **Files:** - Modify: `SportsTime/Core/Services/DataProvider.swift:121-144` **Step 1: Rename method** Change: ```swift /// Fetch games from SwiftData within date range func fetchGames(sports: Set, startDate: Date, endDate: Date) async throws -> [Game] ``` To: ```swift /// Filter games from SwiftData within date range func filterGames(sports: Set, startDate: Date, endDate: Date) async throws -> [Game] ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/DataProvider.swift git commit -m "refactor: rename fetchGames to filterGames (clarify local query)" ``` --- ## Task 8: Rename DataProvider.fetchRichGames to filterRichGames **Files:** - Modify: `SportsTime/Core/Services/DataProvider.swift:163-175` **Step 1: Rename method and update internal call** Change: ```swift /// Fetch games with full team and stadium data func fetchRichGames(sports: Set, startDate: Date, endDate: Date) async throws -> [RichGame] { let games = try await fetchGames(sports: sports, startDate: startDate, endDate: endDate) ``` To: ```swift /// Filter games with full team and stadium data within date range func filterRichGames(sports: Set, startDate: Date, endDate: Date) async throws -> [RichGame] { let games = try await filterGames(sports: sports, startDate: startDate, endDate: endDate) ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/DataProvider.swift git commit -m "refactor: rename fetchRichGames to filterRichGames" ``` --- ## Task 9: Add DataProvider.allGames method **Files:** - Modify: `SportsTime/Core/Services/DataProvider.swift` (add after filterGames, around line 145) **Step 1: Add new method** ```swift /// Get all games for specified sports (no date filtering) func allGames(for sports: Set) async throws -> [Game] { guard let context = modelContext else { throw DataProviderError.contextNotConfigured } let sportStrings = sports.map { $0.rawValue } let descriptor = FetchDescriptor( predicate: #Predicate { game in game.deprecatedAt == nil }, sortBy: [SortDescriptor(\.dateTime)] ) let canonicalGames = try context.fetch(descriptor) return canonicalGames.compactMap { canonical -> Game? in guard sportStrings.contains(canonical.sport) else { return nil } return canonical.toDomain() } } ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/DataProvider.swift git commit -m "feat: add allGames method for unfiltered game access" ``` --- ## Task 10: Add DataProvider.allRichGames method **Files:** - Modify: `SportsTime/Core/Services/DataProvider.swift` (add after allGames) **Step 1: Add new method** ```swift /// Get all games with full team and stadium data (no date filtering) func allRichGames(for sports: Set) async throws -> [RichGame] { let games = try await allGames(for: sports) return games.compactMap { game in guard let homeTeam = teamsById[game.homeTeamId], let awayTeam = teamsById[game.awayTeamId], let stadium = stadiumsById[game.stadiumId] else { return nil } return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium) } } ``` **Step 2: Commit** ```bash git add SportsTime/Core/Services/DataProvider.swift git commit -m "feat: add allRichGames method for unfiltered rich game access" ``` --- ## Task 11: Update TripCreationViewModel.loadGamesForBrowsing **Files:** - Modify: `SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift:491-497` **Step 1: Remove 90-day limit, use allGames** Change from: ```swift // Fetch games for next 90 days for browsing let browseEndDate = Calendar.current.date(byAdding: .day, value: 90, to: Date()) ?? endDate games = try await dataProvider.fetchGames( sports: selectedSports, startDate: Date(), endDate: browseEndDate ) ``` To: ```swift // Fetch ALL available games for browsing (no date restrictions) games = try await dataProvider.allGames(for: selectedSports) ``` **Step 2: Commit** ```bash git add SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift git commit -m "feat: remove 90-day limit from game browsing, show all games" ``` --- ## Task 12: Update MockCloudKitService sync methods **Files:** - Modify: `SportsTimeTests/Mocks/MockCloudKitService.swift:165-206` **Step 1: Update fetchStadiumsForSync** ```swift func fetchStadiumsForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncStadium] { try await simulateNetwork() let filtered = lastSync == nil ? stadiums : stadiums // Mock doesn't track modificationDate return filtered.map { stadium in CloudKitService.SyncStadium( stadium: stadium, canonicalId: stadium.id ) } } ``` **Step 2: Update fetchTeamsForSync** ```swift func fetchTeamsForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncTeam] { try await simulateNetwork() return teams.map { team in CloudKitService.SyncTeam( team: team, canonicalId: team.id, stadiumCanonicalId: team.stadiumId ) } } ``` **Step 3: Update fetchGamesForSync** ```swift func fetchGamesForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncGame] { try await simulateNetwork() return games.map { game in CloudKitService.SyncGame( game: game, canonicalId: game.id, homeTeamCanonicalId: game.homeTeamId, awayTeamCanonicalId: game.awayTeamId, stadiumCanonicalId: game.stadiumId ) } } ``` **Step 4: Commit** ```bash git add SportsTimeTests/Mocks/MockCloudKitService.swift git commit -m "test: update MockCloudKitService signatures for delta sync" ``` --- ## Task 13: Update MockAppDataProvider methods **Files:** - Modify: `SportsTimeTests/Mocks/MockAppDataProvider.swift:157-189` **Step 1: Rename fetchGames to filterGames** ```swift func filterGames(sports: Set, startDate: Date, endDate: Date) async throws -> [Game] { fetchGamesCallCount += 1 await simulateLatency() if config.shouldFailOnFetch { throw DataProviderError.contextNotConfigured } return games.filter { game in sports.contains(game.sport) && game.dateTime >= startDate && game.dateTime <= endDate }.sorted { $0.dateTime < $1.dateTime } } ``` **Step 2: Rename fetchRichGames to filterRichGames** ```swift func filterRichGames(sports: Set, startDate: Date, endDate: Date) async throws -> [RichGame] { fetchRichGamesCallCount += 1 let filteredGames = try await filterGames(sports: sports, startDate: startDate, endDate: endDate) return filteredGames.compactMap { game in richGame(from: game) } } ``` **Step 3: Add allGames method** ```swift func allGames(for sports: Set) async throws -> [Game] { fetchGamesCallCount += 1 await simulateLatency() if config.shouldFailOnFetch { throw DataProviderError.contextNotConfigured } return games.filter { game in sports.contains(game.sport) }.sorted { $0.dateTime < $1.dateTime } } ``` **Step 4: Add allRichGames method** ```swift func allRichGames(for sports: Set) async throws -> [RichGame] { fetchRichGamesCallCount += 1 let allGames = try await allGames(for: sports) return allGames.compactMap { game in richGame(from: game) } } ``` **Step 5: Commit** ```bash git add SportsTimeTests/Mocks/MockAppDataProvider.swift git commit -m "test: update MockAppDataProvider with renamed and new methods" ``` --- ## Task 14: Fix all callers of renamed methods **Files:** - Search and update all files calling the old method names **Step 1: Find all usages** Run: ```bash grep -r "fetchGames\|fetchRichGames" --include="*.swift" SportsTime/ SportsTimeTests/ | grep -v "Mock" ``` **Step 2: Update each caller** For each file found: - `fetchGames(` → `filterGames(` (when using date parameters) - `fetchRichGames(` → `filterRichGames(` (when using date parameters) Common files likely to need updates: - `GameMatcher.swift` - `ScheduleMatcher.swift` - `TripPlanningEngine.swift` - Various test files **Step 3: Commit** ```bash git add -A git commit -m "refactor: update all callers to use renamed filter methods" ``` --- ## Task 15: Run tests and fix any failures **Step 1: Run full test suite** ```bash xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test ``` **Step 2: Fix any compilation errors or test failures** Most likely issues: - Missing method implementations - Signature mismatches between mock and real implementations - Tests expecting old method names **Step 3: Commit fixes** ```bash git add -A git commit -m "fix: resolve test failures from delta sync refactor" ``` --- ## Task 16: Final verification **Step 1: Verify CloudKit sync logic** Review the sync flow: 1. First launch: `lastSync == nil` → fetches ALL records 2. Subsequent syncs: `lastSync == Date` → fetches only modified records **Step 2: Verify game browsing** 1. Build and run app 2. Go to "By Games" mode 3. Select MLB 4. Verify Houston Astros shows full season (160+ games) **Step 3: Final commit** ```bash git add -A git commit -m "feat: complete delta sync and unlimited game browsing implementation" ``` --- ## Summary of Changes | File | Changes | |------|---------| | `CloudKitService.swift` | 3 methods updated to use `since: Date?` parameter | | `CanonicalSyncService.swift` | 3 methods updated to pass `lastSync` | | `DataProvider.swift` | 2 methods renamed, 2 methods added | | `TripCreationViewModel.swift` | 1 method updated to use `allGames` | | `MockCloudKitService.swift` | 3 methods updated to match new signatures | | `MockAppDataProvider.swift` | 2 methods renamed, 2 methods added | | Various callers | Updated to use new method names |