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:
147
.planning/codebase/ARCHITECTURE.md
Normal file
147
.planning/codebase/ARCHITECTURE.md
Normal 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*
|
||||
132
.planning/codebase/CONCERNS.md
Normal file
132
.planning/codebase/CONCERNS.md
Normal 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*
|
||||
150
.planning/codebase/CONVENTIONS.md
Normal file
150
.planning/codebase/CONVENTIONS.md
Normal 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*
|
||||
134
.planning/codebase/INTEGRATIONS.md
Normal file
134
.planning/codebase/INTEGRATIONS.md
Normal 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
106
.planning/codebase/STACK.md
Normal 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*
|
||||
151
.planning/codebase/STRUCTURE.md
Normal file
151
.planning/codebase/STRUCTURE.md
Normal 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*
|
||||
223
.planning/codebase/TESTING.md
Normal file
223
.planning/codebase/TESTING.md
Normal 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*
|
||||
127
.planning/phases/01-script-architecture/01-01-PLAN.md
Normal file
127
.planning/phases/01-script-architecture/01-01-PLAN.md
Normal 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>
|
||||
119
.planning/phases/01-script-architecture/01-02-PLAN.md
Normal file
119
.planning/phases/01-script-architecture/01-02-PLAN.md
Normal 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>
|
||||
147
.planning/phases/01-script-architecture/01-03-PLAN.md
Normal file
147
.planning/phases/01-script-architecture/01-03-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user