# 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<Sport>, startDate: Date, endDate: Date) async throws -> [Game]
```

To:
```swift
/// Filter games from SwiftData within date range
func filterGames(sports: Set<Sport>, 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<Sport>, 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<Sport>, 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<Sport>) async throws -> [Game] {
    guard let context = modelContext else {
        throw DataProviderError.contextNotConfigured
    }

    let sportStrings = sports.map { $0.rawValue }

    let descriptor = FetchDescriptor<CanonicalGame>(
        predicate: #Predicate<CanonicalGame> { 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<Sport>) 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<Sport>, 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<Sport>, 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<Sport>) 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<Sport>) 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 |
