docs: add Phase 1 plans and codebase documentation

- 01-01-PLAN.md: core.py + mlb.py (executed)
- 01-02-PLAN.md: nba.py + nhl.py
- 01-03-PLAN.md: nfl.py + orchestrator refactor
- Codebase documentation for planning context

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-10 00:00:45 -06:00
parent 504187059f
commit 60b450d869
10 changed files with 1436 additions and 0 deletions

View File

@@ -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*

View File

@@ -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*

View File

@@ -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*

View File

@@ -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*

106
.planning/codebase/STACK.md Normal file
View File

@@ -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*

View File

@@ -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*

View File

@@ -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*

View File

@@ -0,0 +1,127 @@
---
phase: 01-script-architecture
plan: 01
type: execute
---
<objective>
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.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-phase.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: Create core.py shared module</name>
<files>Scripts/core.py</files>
<action>
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.
</action>
<verify>python3 -c "from Scripts.core import Game, Stadium, ScraperSource, rate_limit, fetch_page, scrape_with_fallback, assign_stable_ids, export_to_json; print('OK')"</verify>
<done>core.py exists, imports successfully, exports all shared utilities</done>
</task>
<task type="auto">
<name>Task 2: Create mlb.py sport module</name>
<files>Scripts/mlb.py</files>
<action>
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.
</action>
<verify>python3 -c "from Scripts.mlb import scrape_mlb_games, scrape_mlb_stadiums, MLB_GAME_SOURCES; print('OK')"</verify>
<done>mlb.py exists, imports from core.py, exports MLB scrapers and source configs</done>
</task>
</tasks>
<verification>
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
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/01-script-architecture/01-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,119 @@
---
phase: 01-script-architecture
plan: 02
type: execute
---
<objective>
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.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-phase.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: Create nba.py sport module</name>
<files>Scripts/nba.py</files>
<action>
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.
</action>
<verify>python3 -c "from Scripts.nba import scrape_nba_games, NBA_GAME_SOURCES; print('OK')"</verify>
<done>nba.py exists, imports from core.py, exports NBA scrapers</done>
</task>
<task type="auto">
<name>Task 2: Create nhl.py sport module</name>
<files>Scripts/nhl.py</files>
<action>
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.
</action>
<verify>python3 -c "from Scripts.nhl import scrape_nhl_games, NHL_GAME_SOURCES; print('OK')"</verify>
<done>nhl.py exists, imports from core.py, exports NHL scrapers</done>
</task>
</tasks>
<verification>
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)
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/01-script-architecture/01-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,147 @@
---
phase: 01-script-architecture
plan: 03
type: execute
---
<objective>
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`.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-phase.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: Create nfl.py sport module</name>
<files>Scripts/nfl.py</files>
<action>
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.
</action>
<verify>python3 -c "from Scripts.nfl import scrape_nfl_games, NFL_GAME_SOURCES; print('OK')"</verify>
<done>nfl.py exists, imports from core.py, exports NFL scrapers</done>
</task>
<task type="auto">
<name>Task 2: Refactor scrape_schedules.py to orchestrator</name>
<files>Scripts/scrape_schedules.py</files>
<action>
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.
</action>
<verify>cd Scripts && python3 scrape_schedules.py --help</verify>
<done>scrape_schedules.py is thin orchestrator, imports from sport modules, --help works</done>
</task>
</tasks>
<verification>
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
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/01-script-architecture/01-03-SUMMARY.md` with:
- Phase 1 complete
- Ready for Phase 2: Stadium Foundation
</output>