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*