diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md
new file mode 100644
index 0000000..61ace50
--- /dev/null
+++ b/.planning/codebase/ARCHITECTURE.md
@@ -0,0 +1,147 @@
+# Architecture
+
+**Analysis Date:** 2026-01-09
+
+## Pattern Overview
+
+**Overall:** Clean MVVM with Feature-Based Modules + Offline-First Data Architecture
+
+**Key Characteristics:**
+- Three-layer architecture (Presentation, Domain, Data)
+- Single source of truth (`AppDataProvider.shared`)
+- Offline-first with background CloudKit sync
+- Scenario-based trip planning (A/B/C discriminated scenarios)
+- Explicit failure handling (no silent errors)
+
+## Layers
+
+**Presentation Layer (`Features/`):**
+- Purpose: SwiftUI Views + @Observable ViewModels
+- Contains: Feature modules (Home, Trip, Schedule, Settings, Progress)
+- Depends on: Domain layer for planning, Data layer for models
+- Used by: App entry point
+
+**Domain Layer (`Planning/`):**
+- Purpose: Trip planning business logic
+- Contains: TripPlanningEngine, Scenario Planners, GameDAGRouter, TravelEstimator
+- Depends on: Domain models only (Game, Stadium, Team)
+- Used by: Presentation layer ViewModels
+
+**Data Layer (`Core/`):**
+- Purpose: Models, services, persistence
+- Contains: Domain models, SwiftData models, CloudKit sync, location services
+- Depends on: Foundation, SwiftData, CloudKit, MapKit
+- Used by: All layers
+
+**Export Layer (`Export/`):**
+- Purpose: PDF generation with asset prefetching
+- Contains: PDFGenerator, map snapshots, remote images, POI search
+- Depends on: Domain models, UIKit/PDFKit
+- Used by: TripDetailView
+
+## Data Flow
+
+**App Startup Sequence:**
+
+1. `SportsTimeApp.swift` (@main) → WindowGroup
+2. `BootstrappedContentView` initializes data:
+ - `BootstrapService.bootstrapIfNeeded()` - JSON → SwiftData (first launch)
+ - `AppDataProvider.configure(with: context)` - Sets ModelContext
+ - `AppDataProvider.loadInitialData()` - SwiftData → memory
+3. App immediately usable with local data
+4. Background: `CanonicalSyncService.syncAll()` - CloudKit → SwiftData (non-blocking)
+
+**Trip Planning Request:**
+
+1. User Input → `TripCreationView`
+2. `TripCreationViewModel` builds `PlanningRequest`
+3. `TripPlanningEngine.planItineraries(request:)`
+ - Detects scenario (A: date range, B: selected games, C: locations)
+ - Delegates to appropriate `ScenarioPlanner`
+4. `GameDAGRouter` finds routes with diversity
+5. Returns `ItineraryResult` (success with ranked options or explicit failure)
+6. User selects option → `SavedTrip` persisted
+
+**State Management:**
+- ViewModels use `@Observable` (not ObservableObject)
+- SwiftData `@Query` for saved trips
+- In-memory cache in `AppDataProvider` for canonical data
+
+## Key Abstractions
+
+**AppDataProvider:**
+- Purpose: Single source of truth for all canonical data reads
+- Location: `Core/Services/DataProvider.swift`
+- Pattern: Singleton with in-memory cache
+- Usage: `AppDataProvider.shared.stadiums`, `.teams`, `.fetchGames()`
+
+**ScenarioPlanner Protocol:**
+- Purpose: Encapsulates planning algorithm for each scenario type
+- Examples: `ScenarioAPlanner`, `ScenarioBPlanner`, `ScenarioCPlanner`
+- Pattern: Strategy pattern with factory (`ScenarioPlannerFactory`)
+
+**GameDAGRouter:**
+- Purpose: DAG-based route finding with multi-dimensional diversity
+- Location: `Planning/Engine/GameDAGRouter.swift`
+- Pattern: Beam search with diversity pruning
+
+**TripPlanningEngine:**
+- Purpose: Thin orchestrator delegating to scenario planners
+- Location: `Planning/Engine/TripPlanningEngine.swift`
+- Pattern: Facade wrapping scenario detection and filter application
+
+## Entry Points
+
+**App Entry:**
+- Location: `SportsTime/SportsTimeApp.swift`
+- Triggers: App launch
+- Responsibilities: SwiftData schema, ModelContainer, BootstrappedContentView
+
+**Main UI Entry:**
+- Location: `Features/Home/Views/HomeView.swift`
+- Triggers: After bootstrap completes
+- Responsibilities: TabView with 4 tabs (Home, Schedule, Trips, Progress)
+
+**Trip Creation:**
+- Location: `Features/Trip/Views/TripCreationView.swift`
+- Triggers: User taps "New Trip"
+- Responsibilities: Form input, calls TripCreationViewModel
+
+**Trip Display:**
+- Location: `Features/Trip/Views/TripDetailView.swift`
+- Triggers: User selects trip
+- Responsibilities: Itinerary display, conflict detection, PDF export
+
+## Error Handling
+
+**Strategy:** Explicit failure types with reasons, no silent errors
+
+**Patterns:**
+- `ItineraryResult` enum: `.success([ItineraryOption])` or `.failure(PlanningFailure)`
+- `PlanningFailure` contains `FailureReason` and user-friendly message
+- ViewModels expose `.error` state for UI display
+- Services use `throws` for recoverable errors
+
+## Cross-Cutting Concerns
+
+**Logging:**
+- `print()` with emoji prefixes for debugging
+- No production logging framework
+
+**Validation:**
+- Input validation in ViewModels before planning
+- `ConstraintViolation` type for planning constraints
+
+**Thread Safety:**
+- `@MainActor` for UI-bound services
+- `actor` types for planning engine components
+- Explicit isolation annotations throughout
+
+**Data Consistency:**
+- Canonical IDs (string) + UUIDs for stable identity
+- SwiftData ↔ Domain model conversion via `.toDomain()` methods
+
+---
+
+*Architecture analysis: 2026-01-09*
+*Update when major patterns change*
diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md
new file mode 100644
index 0000000..5d24968
--- /dev/null
+++ b/.planning/codebase/CONCERNS.md
@@ -0,0 +1,132 @@
+# Codebase Concerns
+
+**Analysis Date:** 2026-01-09
+
+## Tech Debt
+
+**Foundation Models disabled:**
+- Issue: On-device AI route descriptions disabled due to simulator bug
+- File: `SportsTime/Core/Services/RouteDescriptionGenerator.swift:30`
+- Why: iOS 26.2 simulator crashes with Foundation Models
+- Impact: Route descriptions not generated; feature incomplete
+- Fix approach: Re-enable when Apple fixes the framework (tracked via TODO)
+
+**Large service files:**
+- Issue: Some service files exceed 500-800 lines
+- Files: `SportsTime/Core/Services/SuggestedTripsGenerator.swift` (794 lines)
+- Why: Accumulated functionality over time
+- Impact: Harder to navigate and test in isolation
+- Fix approach: Extract helper methods to focused utilities
+
+## Known Bugs
+
+**No known bugs documented.**
+
+The codebase has comprehensive test coverage (180+ tests) and follows a regression test protocol for bug fixes.
+
+## Security Considerations
+
+**No hardcoded secrets:**
+- CloudKit uses entitlements (no API keys in code)
+- Sports APIs are public (no authentication required)
+- No .env files or credential storage
+
+**CloudKit data access:**
+- Risk: Public database readable by any app user
+- Current mitigation: Only non-sensitive schedule data in public DB
+- Recommendations: User data correctly uses private database
+
+**Force unwrap usage:**
+- Risk: Potential crashes from force unwraps (`!`)
+- Current state: Limited to test fixtures and controlled scenarios
+- Recommendations: Continue avoiding force unwraps in production code
+
+## Performance Bottlenecks
+
+**No significant bottlenecks detected.**
+
+- Route planning uses efficient DAG-based algorithms with beam search
+- Data loading is async and non-blocking
+- PDF export uses parallel asset prefetching (`PDFAssetPrefetcher.swift`)
+
+**Potential areas to monitor:**
+- Large game datasets (1000+ games) during planning
+- Map snapshot generation for long trips (10+ stops)
+
+## Fragile Areas
+
+**GameDAGRouter complexity:**
+- File: `SportsTime/Planning/Engine/GameDAGRouter.swift`
+- Why fragile: Complex beam search with diversity pruning
+- Common failures: Edge cases in route diversity calculations
+- Safe modification: Comprehensive test coverage exists (180+ tests total)
+- Test coverage: Good - multiple scenario planner test suites
+
+**Canonical data sync:**
+- Files: `SportsTime/Core/Services/CanonicalSyncService.swift`, `DataProvider.swift`
+- Why fragile: Multiple data sources (bundled JSON, SwiftData, CloudKit)
+- Common failures: Data inconsistency if sync partially completes
+- Safe modification: Follow existing patterns, test offline scenarios
+- Test coverage: Limited - manual testing recommended
+
+## Scaling Limits
+
+**CloudKit free tier:**
+- Current capacity: Standard CloudKit quotas
+- Limit: 10GB public database, rate limits on queries
+- Symptoms at limit: 429 errors, slow sync
+- Scaling path: Monitor usage; Apple provides generous free tier
+
+**In-memory data cache:**
+- Current capacity: All stadiums, teams loaded into memory (~few MB)
+- Limit: Not expected to hit limits with current data size
+- Symptoms at limit: Memory pressure on older devices
+- Scaling path: Implement lazy loading if data grows significantly
+
+## Dependencies at Risk
+
+**NBA Stats API (unofficial):**
+- Risk: Unofficial API that may break without notice
+- File: `SportsTime/Core/Services/ScoreAPIProviders/NBAStatsProvider.swift`
+- Impact: NBA game scores unavailable if API changes
+- Migration plan: Multi-provider fallback system in `FreeScoreAPI.swift`
+
+**iOS 26+ requirement:**
+- Risk: Limits user base to newest iOS version
+- Impact: Users on older devices cannot use app
+- Migration plan: Monitor adoption; consider lowering deployment target later
+
+## Missing Critical Features
+
+**None blocking current functionality.**
+
+Future phases documented in `docs/MARKET_RESEARCH.md`:
+- Phase 2: AI-powered natural language planning
+- Phase 3: Stadium bucket list with achievements (partially implemented)
+- Phase 4: Group trip coordination
+- Phase 5: Fan community features
+
+## Test Coverage Gaps
+
+**CloudKit sync integration:**
+- What's not tested: Full CloudKit → SwiftData → memory refresh cycle
+- Risk: Sync issues not caught before production
+- Priority: Medium
+- Difficulty to test: Requires CloudKit test containers or mocks
+
+**PDF generation:**
+- What's not tested: PDFGenerator output (visual testing)
+- Risk: PDF layout issues not caught automatically
+- Priority: Low (manual QA sufficient)
+- Difficulty to test: Would need snapshot testing
+
+**UI tests:**
+- What's not tested: Limited UI test coverage
+- Risk: UI regressions
+- Priority: Low (app is relatively simple UI)
+- Difficulty to test: Standard Xcode UI testing
+
+---
+
+*Concerns audit: 2026-01-09*
+*Update as issues are fixed or new ones discovered*
diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md
new file mode 100644
index 0000000..e34ddbc
--- /dev/null
+++ b/.planning/codebase/CONVENTIONS.md
@@ -0,0 +1,150 @@
+# Coding Conventions
+
+**Analysis Date:** 2026-01-09
+
+## Naming Patterns
+
+**Files:**
+- PascalCase for all Swift files: `TripDetailView.swift`, `DataProvider.swift`
+- Views: `*View.swift` (e.g., `HomeView.swift`, `TripCreationView.swift`)
+- ViewModels: `*ViewModel.swift` (e.g., `TripCreationViewModel.swift`)
+- Services: `*Service.swift` (e.g., `LocationService.swift`)
+- Tests: `*Tests.swift` (e.g., `TravelEstimatorTests.swift`)
+
+**Functions:**
+- camelCase for all functions: `loadInitialData()`, `planItineraries()`
+- No special prefix for async functions
+- Handlers: `handle*` pattern not heavily used; actions named directly
+
+**Variables:**
+- camelCase: `selectedSports`, `startDate`, `gamesOnThisDay`
+- No underscore prefix for private (Swift convention)
+- Constants: camelCase (no UPPER_SNAKE_CASE)
+
+**Types:**
+- PascalCase for all types: `Stadium`, `TripPreferences`, `PlanningRequest`
+- No I prefix for protocols: `ScenarioPlanner` (not `IScenarioPlanner`)
+- Enums: PascalCase name, camelCase cases: `Sport.mlb`, `FailureReason.noGamesFound`
+
+## Code Style
+
+**Formatting:**
+- 4-space indentation (inferred from code)
+- No SwiftLint or SwiftFormat configuration
+- Follows standard Swift conventions organically
+
+**Section Organization:**
+- `// MARK: -` for major sections (560 occurrences across codebase)
+- Pattern: `// MARK: - Section Name`
+- Example sections: `// MARK: - Properties`, `// MARK: - Public API`, `// MARK: - Private`
+
+**File Headers:**
+```swift
+//
+// FileName.swift
+// SportsTime
+//
+// Optional description line.
+//
+```
+
+## Import Organization
+
+**Order:**
+1. Foundation/Swift standard library
+2. Apple frameworks (SwiftUI, SwiftData, MapKit)
+3. Project imports (`@testable import SportsTime` in tests)
+
+**Grouping:**
+- No blank lines between import groups
+- Alphabetical not enforced
+
+**Path Aliases:**
+- None used (no module aliasing)
+
+## Error Handling
+
+**Patterns:**
+- Explicit result types: `ItineraryResult` enum with `.success` / `.failure`
+- `throws` for recoverable service errors
+- Async functions use `async throws`
+
+**Error Types:**
+- `PlanningFailure` with `FailureReason` enum and user message
+- `ConstraintViolation` for planning constraint issues
+- SwiftData errors propagated via `try`
+
+**Async:**
+- `async/await` throughout (no completion handlers)
+- `try await` pattern for async throwing functions
+
+## Logging
+
+**Framework:**
+- `print()` with emoji prefixes for debugging
+- No production logging framework (Sentry, etc.)
+
+**Patterns:**
+- Warning: `print("⚠️ Warning message")`
+- Info: `print("ℹ️ Info message")`
+- Error: `print("❌ Error: \(error)")`
+- Debug only; no structured logging
+
+## Comments
+
+**When to Comment:**
+- Explain why, not what
+- Document business logic and edge cases
+- Complex algorithms get explanatory comments
+
+**Documentation Comments:**
+- Triple-slash `///` for public APIs (487 occurrences)
+- Example:
+ ```swift
+ /// Main entry point for trip planning.
+ /// - Parameter request: The planning request containing all inputs
+ /// - Returns: Ranked itineraries on success, or explicit failure
+ func planItineraries(request: PlanningRequest) -> ItineraryResult
+ ```
+
+**TODO Comments:**
+- Format: `// TODO: description`
+- Currently only 1 TODO in codebase: `RouteDescriptionGenerator.swift:30`
+
+## Function Design
+
+**Size:**
+- No strict line limit enforced
+- Large files exist (800+ lines in some services)
+- Complex logic extracted to private helpers
+
+**Parameters:**
+- Default parameters used extensively
+- Options objects for complex configuration: `PlanningRequest`, `TripPreferences`
+
+**Return Values:**
+- Explicit returns
+- Result types for operations that can fail
+- Optional for lookups that may not find data
+
+## Module Design
+
+**Exports:**
+- No barrel files (Swift doesn't use this pattern)
+- Public API via `public`/`internal` access control
+
+**Access Control:**
+- `private` for implementation details
+- `internal` (default) for module-internal
+- `public` for Codable conformances and cross-module APIs
+
+**Property Wrappers:**
+- `@Observable` for ViewModels (modern pattern)
+- `@Model` for SwiftData entities
+- `@MainActor` for UI-bound services
+- `@Query` for SwiftData queries in views
+
+---
+
+*Convention analysis: 2026-01-09*
+*Update when patterns change*
diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md
new file mode 100644
index 0000000..cd36a56
--- /dev/null
+++ b/.planning/codebase/INTEGRATIONS.md
@@ -0,0 +1,134 @@
+# External Integrations
+
+**Analysis Date:** 2026-01-09
+
+## APIs & External Services
+
+**Sports Data - MLB Stats API:**
+- Endpoint: `https://statsapi.mlb.com/api/v1`
+- Integration: `Core/Services/ScoreAPIProviders/MLBStatsProvider.swift`
+- Purpose: Official MLB schedule and game scores
+- Auth: None required (public API)
+- Reliability: Official, documented
+
+**Sports Data - NHL API:**
+- Endpoint: `https://api-web.nhle.com/v1`
+- Integration: `Core/Services/ScoreAPIProviders/NHLStatsProvider.swift`
+- Purpose: Official NHL schedule and game scores
+- Auth: None required (public API)
+- Reliability: Official, documented
+
+**Sports Data - NBA Stats API:**
+- Endpoint: `https://stats.nba.com/stats`
+- Integration: `Core/Services/ScoreAPIProviders/NBAStatsProvider.swift`
+- Purpose: NBA stats and schedules (unofficial)
+- Auth: Requires specific User-Agent headers
+- Reliability: Unofficial, may break without notice
+
+**Score Resolution Facade:**
+- Integration: `Core/Services/FreeScoreAPI.swift`
+- Purpose: Multi-provider score resolution with fallback
+- Features: Rate limiting, caching, provider tiers
+- Cache: `Core/Services/ScoreResolutionCache.swift`
+
+## Data Storage
+
+**CloudKit (iCloud):**
+- Container: `iCloud.com.sportstime.app`
+- Integration: `Core/Services/CloudKitService.swift`
+- Purpose: Remote sync for canonical data, photo backup
+- Database: Public (schedules), Private (user photos)
+- Records:
+ - `CanonicalStadium`, `CanonicalTeam`, `CanonicalGame`
+ - `LeagueStructure`, `StadiumAlias`, `TeamAlias`
+- Sync Service: `Core/Services/CanonicalSyncService.swift`
+
+**SwiftData (Local):**
+- Integration: `Core/Models/Local/*.swift`
+- Purpose: Local persistence, offline-first
+- Models:
+ - Canonical: `CanonicalStadium`, `CanonicalTeam`, `CanonicalGame`
+ - User: `SavedTrip`, `StadiumVisit`, `UserPreferences`, `Achievement`
+
+**Bundled JSON:**
+- Location: `SportsTime/Resources/*.json`
+- Purpose: Bootstrap data for offline-first experience
+- Files: `stadiums_canonical.json`, `teams_canonical.json`, `games_canonical.json`, `league_structure.json`
+
+## Location & Maps Services
+
+**Apple Maps (MapKit):**
+- Geocoding: `Core/Services/LocationService.swift` - Address→Coordinates
+- Reverse Geocoding: Coordinates→Address lookup
+- Routing: `MKDirections` for travel time/distance
+- POI Search: `Export/Services/POISearchService.swift` - Restaurants, attractions
+- EV Charging: `Core/Services/EVChargingService.swift` - Charging station search
+- Map Snapshots: `Export/Services/MapSnapshotService.swift` - Static map images
+
+**CoreLocation:**
+- Purpose: Coordinate types, user location (if permitted)
+- No active GPS tracking; uses user-provided locations
+
+## Photo Library Integration
+
+**PhotosPicker:**
+- Integration: `Features/Progress/ViewModels/PhotoImportViewModel.swift`
+- Purpose: Import photos to match with stadium visits
+- Metadata: `Core/Services/PhotoMetadataExtractor.swift` - EXIF extraction
+
+**Visit Photos:**
+- Integration: `Core/Services/VisitPhotoService.swift`
+- Storage: Thumbnails in SwiftData, full images in CloudKit private database
+- Backup: Automatic CloudKit sync for photo preservation
+
+## AI/ML Integration
+
+**Apple Foundation Models:**
+- Integration: `Core/Services/RouteDescriptionGenerator.swift`
+- Purpose: On-device AI for natural language route descriptions
+- Status: Disabled due to iOS 26.2 simulator bug
+- Requirement: iOS 26+, Apple Silicon
+
+## Environment Configuration
+
+**Development:**
+- Required env vars: None
+- Secrets location: CloudKit container in entitlements
+- Mock/stub services: Uses bundled JSON data
+
+**Production:**
+- CloudKit: Production container (automatic via entitlements)
+- APIs: All public endpoints, no API keys required
+- Background Modes: remote-notification, fetch, processing
+
+## Data Pipeline (Scripts/)
+
+**Schedule Scraping:**
+- Script: `Scripts/scrape_schedules.py`
+- Sources: Basketball-Reference, Baseball-Reference, Hockey-Reference
+- Rate Limiting: 3-second delay per domain
+- Output: JSON files for processing
+
+**Data Processing:**
+- `Scripts/canonicalize_stadiums.py` - Normalize stadium identities
+- `Scripts/canonicalize_teams.py` - Normalize team identities
+- `Scripts/canonicalize_games.py` - Normalize game records
+- `Scripts/generate_canonical_data.py` - Generate bundled JSON
+
+**CloudKit Import:**
+- Script: `Scripts/cloudkit_import.py`
+- Purpose: Upload canonical data to CloudKit public database
+- Auth: CloudKit server-to-server authentication (via cryptography)
+
+## Webhooks & Callbacks
+
+**Incoming:**
+- None (no server-side components)
+
+**Outgoing:**
+- None (all data fetched on-demand)
+
+---
+
+*Integration audit: 2026-01-09*
+*Update when adding/removing external services*
diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md
new file mode 100644
index 0000000..181adc3
--- /dev/null
+++ b/.planning/codebase/STACK.md
@@ -0,0 +1,106 @@
+# Technology Stack
+
+**Analysis Date:** 2026-01-09
+
+## Languages
+
+**Primary:**
+- Swift 5.0 - All iOS application code (`SportsTime/*.swift`)
+
+**Secondary:**
+- Python 3 - Data scraping and CloudKit import scripts (`Scripts/*.py`)
+
+## Runtime
+
+**Environment:**
+- iOS 26.2 deployment target (`SportsTime.xcodeproj/project.pbxproj`)
+- Apple Silicon + Intel support
+
+**Package Manager:**
+- None (native Xcode project, no SPM/CocoaPods/Carthage)
+- Python: pip with `Scripts/requirements.txt`
+
+## Frameworks
+
+**Core:**
+- SwiftUI - Primary UI framework (`Features/**/*.swift`)
+- UIKit - PDF generation, graphics (`Export/PDFGenerator.swift`)
+- Observation - Modern reactive state (`@Observable` ViewModels)
+
+**Data:**
+- SwiftData - Local persistence (`Core/Models/Local/*.swift`)
+- CloudKit - Remote sync, public database (`Core/Services/CloudKitService.swift`)
+
+**Location & Maps:**
+- MapKit - Routing, search, snapshots (`Core/Services/LocationService.swift`, `Export/Services/MapSnapshotService.swift`)
+- CoreLocation - Coordinates, geocoding support
+
+**Media:**
+- PDFKit - Document generation (`Export/PDFGenerator.swift`)
+- Photos/PhotosUI - Photo library access (`Features/Progress/ViewModels/PhotoImportViewModel.swift`)
+- ImageIO - Image encoding/decoding
+
+**AI/ML:**
+- FoundationModels - On-device AI for route descriptions (`Core/Services/RouteDescriptionGenerator.swift` - currently disabled)
+
+**Security:**
+- CryptoKit - Cryptographic operations (`Core/Services/BootstrapService.swift`)
+
+**Testing:**
+- Swift Testing - Primary test framework (`SportsTimeTests/*.swift`)
+- XCTest - UI tests (`SportsTimeUITests/*.swift`)
+
+**Build/Dev:**
+- Xcode 16+ - Build system
+- xcodebuild - CLI builds and tests
+
+## Key Dependencies
+
+**Critical:**
+- SwiftData - Local data persistence and caching
+- CloudKit - Schedule sync and photo backup
+- MapKit - Core trip planning (routing, EV charging, POI search)
+
+**Infrastructure:**
+- Foundation URLSession - HTTP networking for sports APIs
+- Combine - Reactive patterns alongside Observation
+
+**Python Pipeline:**
+- requests>=2.28.0 - HTTP client for web scraping
+- beautifulsoup4>=4.11.0 - HTML parsing
+- pandas>=2.0.0 - Data manipulation
+- lxml>=4.9.0 - XML/HTML parsing backend
+- cryptography>=41.0.0 - CloudKit import (optional)
+
+## Configuration
+
+**Environment:**
+- No .env files required
+- CloudKit container configured in entitlements
+- Bundle ID: `com.t-t.SportsTime`
+
+**Build:**
+- `SportsTime.xcodeproj` - Native Xcode project
+- `Info.plist` - App configuration with background modes
+
+**Background Modes:**
+- `remote-notification` - Push notifications
+- `fetch` - Background refresh
+- `processing` - Background tasks
+
+## Platform Requirements
+
+**Development:**
+- macOS with Xcode 16+
+- iOS Simulator (iPhone 17, iOS 26.2)
+- No external dependencies
+
+**Production:**
+- iOS 26.2+ deployment target
+- CloudKit entitlement required
+- Location services permission
+
+---
+
+*Stack analysis: 2026-01-09*
+*Update after major dependency changes*
diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md
new file mode 100644
index 0000000..ee21788
--- /dev/null
+++ b/.planning/codebase/STRUCTURE.md
@@ -0,0 +1,151 @@
+# Codebase Structure
+
+**Analysis Date:** 2026-01-09
+
+## Directory Layout
+
+```
+SportsTime/
+├── SportsTimeApp.swift # @main entry point
+├── Features/ # Presentation layer
+│ ├── Home/ # Dashboard, suggested trips
+│ ├── Trip/ # Creation & detail views
+│ ├── Schedule/ # Browse games
+│ ├── Settings/ # User preferences
+│ └── Progress/ # Stadium visits, achievements
+├── Planning/ # Domain layer
+│ ├── Engine/ # Planning algorithms
+│ └── Models/ # Planning-specific types
+├── Core/ # Data layer
+│ ├── Models/ # Domain + Local + CloudKit models
+│ ├── Services/ # Data providers, sync, APIs
+│ ├── Theme/ # Design system
+│ ├── Extensions/ # Swift extensions
+│ └── Utilities/ # Helpers
+├── Export/ # PDF generation
+│ ├── PDFGenerator.swift # Main generator
+│ └── Services/ # Asset services
+├── Resources/ # Assets, bundled JSON
+└── Info.plist # App configuration
+
+SportsTimeTests/ # Unit tests
+SportsTimeUITests/ # UI tests
+Scripts/ # Python data pipeline
+docs/ # Documentation
+```
+
+## Directory Purposes
+
+**Features/ (Presentation):**
+- Purpose: SwiftUI Views + @Observable ViewModels
+- Contains: Feature-organized modules
+- Key files: `HomeView.swift`, `TripCreationView.swift`, `TripDetailView.swift`
+- Subdirectories: Views/, ViewModels/ per feature
+
+**Planning/ (Domain):**
+- Purpose: Trip planning business logic
+- Contains: Engine algorithms, planning models
+- Key files: `TripPlanningEngine.swift`, `GameDAGRouter.swift`, `PlanningModels.swift`
+- Subdirectories: Engine/, Models/
+
+**Core/ (Data):**
+- Purpose: Models, services, infrastructure
+- Contains: Domain structs, SwiftData models, services
+- Key files: `DataProvider.swift`, `CloudKitService.swift`, `LocationService.swift`
+- Subdirectories: Models/Domain/, Models/Local/, Models/CloudKit/, Services/
+
+**Export/ (PDF):**
+- Purpose: PDF generation with parallel asset fetching
+- Contains: PDF generator, asset services
+- Key files: `PDFGenerator.swift`, `MapSnapshotService.swift`, `PDFAssetPrefetcher.swift`
+- Subdirectories: Services/
+
+## Key File Locations
+
+**Entry Points:**
+- `SportsTime/SportsTimeApp.swift` - App entry, SwiftData schema
+- `SportsTime/Features/Home/Views/HomeView.swift` - Main TabView
+
+**Configuration:**
+- `SportsTime/Info.plist` - App configuration, background modes
+- `SportsTime.xcodeproj/project.pbxproj` - Build settings
+
+**Core Logic:**
+- `SportsTime/Core/Services/DataProvider.swift` - Single source of truth
+- `SportsTime/Planning/Engine/TripPlanningEngine.swift` - Planning orchestrator
+- `SportsTime/Planning/Engine/GameDAGRouter.swift` - Route finding
+
+**Testing:**
+- `SportsTimeTests/TravelEstimatorTests.swift` - 50+ tests
+- `SportsTimeTests/ScenarioAPlannerSwiftTests.swift` - Scenario A tests
+- `SportsTimeTests/ScenarioBPlannerTests.swift` - Scenario B tests
+- `SportsTimeTests/ScenarioCPlannerTests.swift` - Scenario C tests
+
+**Documentation:**
+- `CLAUDE.md` - Project instructions for Claude Code
+- `docs/MARKET_RESEARCH.md` - Competitive analysis
+
+## Naming Conventions
+
+**Files:**
+- PascalCase for Swift files: `TripDetailView.swift`, `DataProvider.swift`
+- Pattern: `{TypeName}.swift` matches primary type
+- Views: `*View.swift`
+- ViewModels: `*ViewModel.swift`
+- Services: `*Service.swift`
+
+**Directories:**
+- PascalCase for feature directories: `Features/Trip/`
+- Plural for collections: `Models/`, `Services/`, `Views/`
+
+**Special Patterns:**
+- `index.ts` equivalent: None (Swift doesn't use barrel files)
+- Test files: `*Tests.swift` in `SportsTimeTests/`
+
+## Where to Add New Code
+
+**New Feature:**
+- Primary code: `Features/{FeatureName}/Views/` and `ViewModels/`
+- Tests: `SportsTimeTests/{FeatureName}Tests.swift`
+- Config if needed: Update `SportsTimeApp.swift` schema if adding models
+
+**New Service:**
+- Implementation: `Core/Services/{ServiceName}.swift`
+- Tests: `SportsTimeTests/{ServiceName}Tests.swift`
+
+**New Planning Algorithm:**
+- Definition: `Planning/Engine/{PlannerName}.swift`
+- Protocol: Implement `ScenarioPlanner` protocol
+- Tests: `SportsTimeTests/{PlannerName}Tests.swift`
+
+**New Domain Model:**
+- Domain struct: `Core/Models/Domain/{Model}.swift`
+- SwiftData model (if persisted): `Core/Models/Local/{Model}.swift`
+- Add to `SportsTimeApp.swift` schema
+
+**Utilities:**
+- Shared helpers: `Core/Utilities/`
+- Extensions: `Core/Extensions/`
+
+## Special Directories
+
+**Resources/ (Bundled Data):**
+- Purpose: Bootstrap data for offline-first
+- Source: Generated by `Scripts/generate_canonical_data.py`
+- Contents: `stadiums_canonical.json`, `teams_canonical.json`, `games_canonical.json`
+- Committed: Yes
+
+**Scripts/ (Python Pipeline):**
+- Purpose: Data scraping, canonicalization, CloudKit import
+- Contents: `scrape_schedules.py`, `cloudkit_import.py`, `canonicalize_*.py`
+- Committed: Yes
+
+**.planning/ (Project Planning):**
+- Purpose: GSD workflow documents
+- Contents: STATE.md, PLAN.md, codebase/
+- Committed: Yes
+
+---
+
+*Structure analysis: 2026-01-09*
+*Update when directory structure changes*
diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md
new file mode 100644
index 0000000..001b0ac
--- /dev/null
+++ b/.planning/codebase/TESTING.md
@@ -0,0 +1,223 @@
+# Testing Patterns
+
+**Analysis Date:** 2026-01-09
+
+## Test Framework
+
+**Runner:**
+- Swift Testing (Apple's new testing framework, iOS 26+)
+- Config: Built into Xcode, no separate config file
+
+**Assertion Library:**
+- `#expect()` macro (289 occurrences)
+- Replaces XCTest's `XCTAssertEqual`, etc.
+
+**Run Commands:**
+```bash
+# Run all tests
+xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
+
+# Run specific test suite
+xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TravelEstimatorTests test
+
+# Run single test
+xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TestClassName/testMethodName test
+```
+
+## Test File Organization
+
+**Location:**
+- `SportsTimeTests/*.swift` - Unit tests
+- `SportsTimeUITests/*.swift` - UI tests (XCTest-based)
+
+**Naming:**
+- Unit tests: `{Component}Tests.swift`
+- No integration/e2e distinction in filename
+
+**Structure:**
+```
+SportsTimeTests/
+├── TravelEstimatorTests.swift # 50+ tests
+├── SportsTimeTests.swift # DayCard tests (11+), regression tests
+├── ScenarioAPlannerSwiftTests.swift # 28 tests
+├── ScenarioBPlannerTests.swift # 44 tests
+├── ScenarioCPlannerTests.swift # 49 tests
+└── (total: 180+ unit tests)
+```
+
+## Test Structure
+
+**Suite Organization:**
+```swift
+import Testing
+import Foundation
+@testable import SportsTime
+
+@Suite("ScenarioBPlanner Tests")
+struct ScenarioBPlannerTests {
+
+ // MARK: - Test Fixtures
+
+ private func makeStadium(...) -> Stadium { ... }
+ private func makeGame(...) -> Game { ... }
+
+ // MARK: - Tests
+
+ @Test("handles empty game list")
+ func emptyGameList() {
+ // arrange
+ // act
+ // assert with #expect()
+ }
+}
+```
+
+**Patterns:**
+- `@Suite("Description")` for grouping related tests
+- `@Test("Description")` for individual tests (not `func test...`)
+- `#expect()` for assertions
+- Private `make*` factory functions for test fixtures
+
+## Mocking
+
+**Framework:**
+- No external mocking framework
+- Manual test doubles via protocol conformance
+
+**Patterns:**
+```swift
+// Factory functions create test data
+private func makeGame(
+ id: UUID = UUID(),
+ stadiumId: UUID,
+ date: Date
+) -> Game {
+ Game(
+ id: id,
+ homeTeamId: UUID(),
+ awayTeamId: UUID(),
+ stadiumId: stadiumId,
+ dateTime: date,
+ sport: .mlb,
+ season: "2026"
+ )
+}
+```
+
+**What to Mock:**
+- External services (CloudKit, network)
+- Date/time (use fixed dates in tests)
+
+**What NOT to Mock:**
+- Pure functions (TravelEstimator calculations)
+- Domain models
+
+## Fixtures and Factories
+
+**Test Data:**
+```swift
+// Factory pattern in test structs
+private func makeStadium(
+ id: UUID = UUID(),
+ name: String,
+ city: String,
+ state: String,
+ latitude: Double,
+ longitude: Double,
+ sport: Sport = .mlb
+) -> Stadium { ... }
+
+private func date(_ string: String) -> Date {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy-MM-dd HH:mm"
+ formatter.timeZone = TimeZone(identifier: "America/Los_Angeles")
+ return formatter.date(from: string)!
+}
+
+private func defaultConstraints() -> DrivingConstraints { ... }
+```
+
+**Location:**
+- Factory functions: Defined in each test struct under `// MARK: - Test Fixtures`
+- No shared fixtures directory
+
+## Coverage
+
+**Requirements:**
+- No enforced coverage target
+- Focus on critical paths (planning engine, travel estimation)
+
+**Configuration:**
+- Xcode built-in coverage via scheme settings
+- No separate coverage tool
+
+## Test Types
+
+**Unit Tests (SportsTimeTests/):**
+- Test single function/component in isolation
+- Pure logic tests (no network, no persistence)
+- Fast: milliseconds per test
+- Examples: `TravelEstimatorTests`, `ScenarioAPlannerTests`
+
+**UI Tests (SportsTimeUITests/):**
+- XCTest-based (older framework)
+- Test user flows end-to-end
+- Slower, requires simulator
+
+## Common Patterns
+
+**Async Testing:**
+```swift
+@Test("async operation succeeds")
+func asyncOperation() async {
+ let result = await asyncFunction()
+ #expect(result == expected)
+}
+```
+
+**Error Testing:**
+```swift
+@Test("throws on invalid input")
+func invalidInput() throws {
+ #expect(throws: SomeError.self) {
+ try functionThatThrows()
+ }
+}
+```
+
+**Known Distance Testing:**
+```swift
+@Test("LA to SF distance is approximately 350 miles")
+func laToSfDistance() {
+ let distance = TravelEstimator.haversineDistanceMiles(
+ from: Coordinate(latitude: 34.05, longitude: -118.24),
+ to: Coordinate(latitude: 37.77, longitude: -122.42)
+ )
+ // Known distance is ~350 miles
+ #expect(distance > 340 && distance < 360)
+}
+```
+
+**Regression Test Pattern:**
+```swift
+// Regression test for handling duplicate game IDs without crashing
+@Test("deduplicates games with same ID")
+func duplicateGameHandling() {
+ // Setup with duplicate IDs
+ // Verify first occurrence preserved
+ // Verify no crash
+}
+```
+
+## Bug Fix Protocol
+
+From `CLAUDE.md`:
+1. Write failing test that reproduces bug
+2. Fix the bug
+3. Verify test passes along with all existing tests
+4. Name tests descriptively: `test_Component_Condition_Expected`
+
+---
+
+*Testing analysis: 2026-01-09*
+*Update when test patterns change*
diff --git a/.planning/phases/01-script-architecture/01-01-PLAN.md b/.planning/phases/01-script-architecture/01-01-PLAN.md
new file mode 100644
index 0000000..c4e9b13
--- /dev/null
+++ b/.planning/phases/01-script-architecture/01-01-PLAN.md
@@ -0,0 +1,127 @@
+---
+phase: 01-script-architecture
+plan: 01
+type: execute
+---
+
+
+Create shared core module and extract MLB scrapers as the first sport module.
+
+Purpose: Establish the modular pattern that subsequent sports will follow.
+Output: `Scripts/core.py` with shared utilities, `Scripts/mlb.py` with MLB scrapers.
+
+
+
+@~/.claude/get-shit-done/workflows/execute-phase.md
+@~/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+
+**Source file:**
+@Scripts/scrape_schedules.py
+
+**Codebase context:**
+@.planning/codebase/CONVENTIONS.md
+
+**Tech stack:** Python 3, requests, beautifulsoup4, pandas, lxml
+**Established patterns:** dataclasses, type hints, docstrings
+
+
+
+
+
+ Task 1: Create core.py shared module
+ Scripts/core.py
+
+Create `Scripts/core.py` containing:
+
+1. Imports: argparse, json, time, re, datetime, timedelta, pathlib, dataclasses, typing, requests, BeautifulSoup, pandas
+
+2. Rate limiting utilities:
+ - `REQUEST_DELAY` constant (3.0)
+ - `last_request_time` dict
+ - `rate_limit(domain: str)` function
+ - `fetch_page(url: str, domain: str) -> Optional[BeautifulSoup]` function
+
+3. Data classes:
+ - `@dataclass Game` with all fields (id, sport, season, date, time, home_team, away_team, etc.)
+ - `@dataclass Stadium` with all fields (id, name, city, state, latitude, longitude, etc.)
+
+4. Multi-source fallback system:
+ - `@dataclass ScraperSource`
+ - `scrape_with_fallback(sport, season, sources, verbose)` function
+ - `@dataclass StadiumScraperSource`
+ - `scrape_stadiums_with_fallback(sport, sources, verbose)` function
+
+5. ID generation:
+ - `assign_stable_ids(games, sport, season)` function
+
+6. Export utilities:
+ - `export_to_json(games, stadiums, output_dir)` function
+ - `cross_validate_sources(games_by_source)` function
+
+Keep exact function signatures and logic from scrape_schedules.py. Use `__all__` to explicitly export public API.
+
+ python3 -c "from Scripts.core import Game, Stadium, ScraperSource, rate_limit, fetch_page, scrape_with_fallback, assign_stable_ids, export_to_json; print('OK')"
+ core.py exists, imports successfully, exports all shared utilities
+
+
+
+ Task 2: Create mlb.py sport module
+ Scripts/mlb.py
+
+Create `Scripts/mlb.py` containing:
+
+1. Import from core:
+ ```python
+ from core import Game, Stadium, ScraperSource, StadiumScraperSource, fetch_page, scrape_with_fallback, scrape_stadiums_with_fallback
+ ```
+
+2. MLB game scrapers (copy exact logic):
+ - `scrape_mlb_baseball_reference(season: int) -> list[Game]`
+ - `scrape_mlb_statsapi(season: int) -> list[Game]`
+ - `scrape_mlb_espn(season: int) -> list[Game]`
+
+3. MLB stadium scrapers:
+ - `scrape_mlb_stadiums_scorebot() -> list[Stadium]`
+ - `scrape_mlb_stadiums_geojson() -> list[Stadium]`
+ - `scrape_mlb_stadiums_hardcoded() -> list[Stadium]`
+ - `scrape_mlb_stadiums() -> list[Stadium]` (combines above with fallback)
+
+4. Source configurations:
+ - `MLB_GAME_SOURCES` list of ScraperSource
+ - `MLB_STADIUM_SOURCES` list of StadiumScraperSource
+
+5. Convenience function:
+ - `scrape_mlb_games(season: int) -> list[Game]` - uses fallback system
+
+Use `__all__` to export public API. Keep all team abbreviation mappings, venue name normalizations, and parsing logic intact.
+
+ python3 -c "from Scripts.mlb import scrape_mlb_games, scrape_mlb_stadiums, MLB_GAME_SOURCES; print('OK')"
+ mlb.py exists, imports from core.py, exports MLB scrapers and source configs
+
+
+
+
+
+Before declaring plan complete:
+- [ ] `Scripts/core.py` exists and imports cleanly
+- [ ] `Scripts/mlb.py` exists and imports from core
+- [ ] No syntax errors: `python3 -m py_compile Scripts/core.py Scripts/mlb.py`
+- [ ] Type hints present on all public functions
+
+
+
+- core.py contains all shared utilities extracted from scrape_schedules.py
+- mlb.py contains all MLB-specific scrapers
+- Both files import without errors
+- Original scrape_schedules.py unchanged (we're creating new files first)
+
+
+
diff --git a/.planning/phases/01-script-architecture/01-02-PLAN.md b/.planning/phases/01-script-architecture/01-02-PLAN.md
new file mode 100644
index 0000000..b2e6fbd
--- /dev/null
+++ b/.planning/phases/01-script-architecture/01-02-PLAN.md
@@ -0,0 +1,119 @@
+---
+phase: 01-script-architecture
+plan: 02
+type: execute
+---
+
+
+Extract NBA and NHL scrapers to dedicated sport modules.
+
+Purpose: Continue the modular pattern established in Plan 01.
+Output: `Scripts/nba.py` and `Scripts/nhl.py` with respective scrapers.
+
+
+
+@~/.claude/get-shit-done/workflows/execute-phase.md
+@~/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+
+**Prior work:**
+@.planning/phases/01-script-architecture/01-01-SUMMARY.md
+
+**Source files:**
+@Scripts/core.py
+@Scripts/scrape_schedules.py
+
+
+
+
+
+ Task 1: Create nba.py sport module
+ Scripts/nba.py
+
+Create `Scripts/nba.py` following the mlb.py pattern:
+
+1. Import from core:
+ ```python
+ from core import Game, Stadium, ScraperSource, StadiumScraperSource, fetch_page, scrape_with_fallback, scrape_stadiums_with_fallback
+ ```
+
+2. NBA game scrapers:
+ - `scrape_nba_basketball_reference(season: int) -> list[Game]`
+ - `scrape_nba_espn(season: int) -> list[Game]`
+ - `scrape_nba_cbssports(season: int) -> list[Game]`
+
+3. NBA stadium scrapers:
+ - `scrape_nba_stadiums() -> list[Stadium]` (from generate_stadiums_from_teams or hardcoded)
+
+4. Source configurations:
+ - `NBA_GAME_SOURCES` list of ScraperSource
+ - `NBA_STADIUM_SOURCES` list of StadiumScraperSource
+
+5. Convenience functions:
+ - `scrape_nba_games(season: int) -> list[Game]`
+ - `get_nba_season_string(season: int) -> str` - returns "2024-25" format
+
+Copy exact parsing logic including team abbreviations and venue mappings from scrape_schedules.py.
+
+ python3 -c "from Scripts.nba import scrape_nba_games, NBA_GAME_SOURCES; print('OK')"
+ nba.py exists, imports from core.py, exports NBA scrapers
+
+
+
+ Task 2: Create nhl.py sport module
+ Scripts/nhl.py
+
+Create `Scripts/nhl.py` following the same pattern:
+
+1. Import from core:
+ ```python
+ from core import Game, Stadium, ScraperSource, StadiumScraperSource, fetch_page, scrape_with_fallback, scrape_stadiums_with_fallback
+ ```
+
+2. NHL game scrapers:
+ - `scrape_nhl_hockey_reference(season: int) -> list[Game]`
+ - `scrape_nhl_api(season: int) -> list[Game]`
+ - `scrape_nhl_espn(season: int) -> list[Game]`
+
+3. NHL stadium scrapers:
+ - `scrape_nhl_stadiums() -> list[Stadium]`
+
+4. Source configurations:
+ - `NHL_GAME_SOURCES` list of ScraperSource
+ - `NHL_STADIUM_SOURCES` list of StadiumScraperSource
+
+5. Convenience functions:
+ - `scrape_nhl_games(season: int) -> list[Game]`
+ - `get_nhl_season_string(season: int) -> str` - returns "2024-25" format
+
+Copy exact parsing logic from scrape_schedules.py.
+
+ python3 -c "from Scripts.nhl import scrape_nhl_games, NHL_GAME_SOURCES; print('OK')"
+ nhl.py exists, imports from core.py, exports NHL scrapers
+
+
+
+
+
+Before declaring plan complete:
+- [ ] `Scripts/nba.py` exists and imports cleanly
+- [ ] `Scripts/nhl.py` exists and imports cleanly
+- [ ] No syntax errors: `python3 -m py_compile Scripts/nba.py Scripts/nhl.py`
+- [ ] Both import from core.py (not duplicating shared utilities)
+
+
+
+- nba.py contains all NBA-specific scrapers
+- nhl.py contains all NHL-specific scrapers
+- Both follow the pattern established in mlb.py
+- All files import without errors
+
+
+
diff --git a/.planning/phases/01-script-architecture/01-03-PLAN.md b/.planning/phases/01-script-architecture/01-03-PLAN.md
new file mode 100644
index 0000000..1009edc
--- /dev/null
+++ b/.planning/phases/01-script-architecture/01-03-PLAN.md
@@ -0,0 +1,147 @@
+---
+phase: 01-script-architecture
+plan: 03
+type: execute
+---
+
+
+Extract NFL scrapers and refactor scrape_schedules.py to be a thin orchestrator.
+
+Purpose: Complete the modular architecture and update the main entry point.
+Output: `Scripts/nfl.py` and refactored `Scripts/scrape_schedules.py`.
+
+
+
+@~/.claude/get-shit-done/workflows/execute-phase.md
+@~/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+
+**Prior work:**
+@.planning/phases/01-script-architecture/01-01-SUMMARY.md
+@.planning/phases/01-script-architecture/01-02-SUMMARY.md
+
+**Source files:**
+@Scripts/core.py
+@Scripts/mlb.py
+@Scripts/nba.py
+@Scripts/nhl.py
+@Scripts/scrape_schedules.py
+
+
+
+
+
+ Task 1: Create nfl.py sport module
+ Scripts/nfl.py
+
+Create `Scripts/nfl.py` following the established pattern:
+
+1. Import from core:
+ ```python
+ from core import Game, Stadium, ScraperSource, StadiumScraperSource, fetch_page, scrape_with_fallback, scrape_stadiums_with_fallback
+ ```
+
+2. NFL game scrapers:
+ - `scrape_nfl_espn(season: int) -> list[Game]`
+ - `scrape_nfl_pro_football_reference(season: int) -> list[Game]`
+ - `scrape_nfl_cbssports(season: int) -> list[Game]`
+
+3. NFL stadium scrapers:
+ - `scrape_nfl_stadiums_scorebot() -> list[Stadium]`
+ - `scrape_nfl_stadiums_geojson() -> list[Stadium]`
+ - `scrape_nfl_stadiums_hardcoded() -> list[Stadium]`
+ - `scrape_nfl_stadiums() -> list[Stadium]`
+
+4. Source configurations:
+ - `NFL_GAME_SOURCES` list of ScraperSource
+ - `NFL_STADIUM_SOURCES` list of StadiumScraperSource
+
+5. Convenience functions:
+ - `scrape_nfl_games(season: int) -> list[Game]`
+ - `get_nfl_season_string(season: int) -> str` - returns "2025-26" format
+
+Copy exact parsing logic from scrape_schedules.py.
+
+ python3 -c "from Scripts.nfl import scrape_nfl_games, NFL_GAME_SOURCES; print('OK')"
+ nfl.py exists, imports from core.py, exports NFL scrapers
+
+
+
+ Task 2: Refactor scrape_schedules.py to orchestrator
+ Scripts/scrape_schedules.py
+
+Rewrite `Scripts/scrape_schedules.py` as a thin orchestrator:
+
+1. Replace inline scrapers with imports:
+ ```python
+ from core import Game, Stadium, assign_stable_ids, export_to_json
+ from mlb import scrape_mlb_games, scrape_mlb_stadiums, MLB_GAME_SOURCES
+ from nba import scrape_nba_games, scrape_nba_stadiums, NBA_GAME_SOURCES, get_nba_season_string
+ from nhl import scrape_nhl_games, scrape_nhl_stadiums, NHL_GAME_SOURCES, get_nhl_season_string
+ from nfl import scrape_nfl_games, scrape_nfl_stadiums, NFL_GAME_SOURCES, get_nfl_season_string
+ ```
+
+2. Keep the main() function with argparse for CLI
+
+3. Update sport scraping blocks to use new imports:
+ - `if args.sport in ['nba', 'all']:` uses `scrape_nba_games(season)`
+ - `if args.sport in ['mlb', 'all']:` uses `scrape_mlb_games(season)`
+ - etc.
+
+4. Keep stadium scraping with the new module imports
+
+5. For non-core sports (WNBA, MLS, NWSL, CBB), keep them inline for now with a `# TODO: Extract to separate modules` comment
+
+6. Update file header docstring to explain the modular structure:
+ ```python
+ """
+ Sports Schedule Scraper Orchestrator
+
+ This script coordinates scraping across sport-specific modules:
+ - core.py: Shared utilities, data classes, fallback system
+ - mlb.py: MLB scrapers
+ - nba.py: NBA scrapers
+ - nhl.py: NHL scrapers
+ - nfl.py: NFL scrapers
+
+ Usage:
+ python scrape_schedules.py --sport nba --season 2026
+ python scrape_schedules.py --sport all --season 2026
+ """
+ ```
+
+Target: ~500 lines (down from 3359) for the orchestrator, with sport logic in modules.
+
+ cd Scripts && python3 scrape_schedules.py --help
+ scrape_schedules.py is thin orchestrator, imports from sport modules, --help works
+
+
+
+
+
+Before declaring phase complete:
+- [ ] All sport modules exist: core.py, mlb.py, nba.py, nhl.py, nfl.py
+- [ ] `python3 -m py_compile Scripts/*.py` passes for all files
+- [ ] `cd Scripts && python3 scrape_schedules.py --help` shows usage
+- [ ] scrape_schedules.py is significantly smaller (~500 lines vs 3359)
+- [ ] No circular imports between modules
+
+
+
+- Phase 1: Script Architecture complete
+- All 4 core sports have dedicated modules
+- Shared utilities in core.py
+- scrape_schedules.py is thin orchestrator
+- CLI unchanged (backward compatible)
+
+
+