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