docs(04): phase 4 drag interaction (broken)
This commit is contained in:
189
.planning/codebase/INTEGRATIONS.md
Normal file
189
.planning/codebase/INTEGRATIONS.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# External Integrations
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-01-18
|
||||||
|
|
||||||
|
## APIs & External Services
|
||||||
|
|
||||||
|
**Apple CloudKit (Primary Data Source):**
|
||||||
|
- Purpose: Sync game schedules, teams, stadiums, league structure to all users
|
||||||
|
- Container: `iCloud.com.sportstime.app`
|
||||||
|
- Database: Public (read-only for users, admin writes via Python uploader)
|
||||||
|
- Client: `Core/Services/CloudKitService.swift`
|
||||||
|
- Sync: `Core/Services/CanonicalSyncService.swift`
|
||||||
|
- Record types: `Game`, `Team`, `Stadium`, `LeagueStructure`, `TeamAlias`, `StadiumAlias`, `Sport`
|
||||||
|
- Auth: User iCloud account (automatic), server-to-server JWT (Python uploader)
|
||||||
|
|
||||||
|
**Apple MapKit:**
|
||||||
|
- Purpose: Geocoding, routing, map snapshots, EV charger search, POI search
|
||||||
|
- Services used:
|
||||||
|
- `MKLocalSearch` - Address and POI search (`LocationService.swift`)
|
||||||
|
- `MKDirections` - Driving routes and ETA (`LocationService.swift`)
|
||||||
|
- `MKMapSnapshotter` - Static map images for PDF (`Export/Services/MapSnapshotService.swift`)
|
||||||
|
- `MKLocalPointsOfInterestRequest` - EV chargers (`EVChargingService.swift`)
|
||||||
|
- Client: `Core/Services/LocationService.swift`, `Core/Services/EVChargingService.swift`
|
||||||
|
- Auth: Automatic via Apple frameworks
|
||||||
|
- Rate limits: Apple's standard MapKit limits
|
||||||
|
|
||||||
|
**Apple StoreKit 2:**
|
||||||
|
- Purpose: In-app subscriptions (Pro tier)
|
||||||
|
- Products: `com.sportstime.pro.monthly`, `com.sportstime.pro.annual`
|
||||||
|
- Client: `Core/Store/StoreManager.swift`
|
||||||
|
- Features gated: Trip limit (1 free, unlimited Pro)
|
||||||
|
|
||||||
|
**Sports League APIs (Official):**
|
||||||
|
|
||||||
|
| API | URL | Sport | Reliability | Client |
|
||||||
|
|-----|-----|-------|-------------|--------|
|
||||||
|
| MLB Stats API | `https://statsapi.mlb.com/api/v1` | MLB | Official | `ScoreAPIProviders/MLBStatsProvider.swift` |
|
||||||
|
| NHL Stats API | `https://api-web.nhle.com/v1` | NHL | Official | `ScoreAPIProviders/NHLStatsProvider.swift` |
|
||||||
|
| NBA Stats API | `https://stats.nba.com/stats` | NBA | Unofficial | `ScoreAPIProviders/NBAStatsProvider.swift` |
|
||||||
|
|
||||||
|
**NBA Stats API Notes:**
|
||||||
|
- Requires specific headers to avoid 403 (User-Agent, Referer, x-nba-stats-origin, x-nba-stats-token)
|
||||||
|
- May break without notice (unofficial)
|
||||||
|
|
||||||
|
**Sports Reference Sites (Scraping Fallback):**
|
||||||
|
|
||||||
|
| Site | URL Pattern | Sport | Client |
|
||||||
|
|------|-------------|-------|--------|
|
||||||
|
| Baseball-Reference | `baseball-reference.com/boxes/?month=M&day=D&year=Y` | MLB | `HistoricalGameScraper.swift` |
|
||||||
|
| Basketball-Reference | `basketball-reference.com/boxscores/?month=M&day=D&year=Y` | NBA | `HistoricalGameScraper.swift` |
|
||||||
|
| Hockey-Reference | `hockey-reference.com/boxscores/?month=M&day=D&year=Y` | NHL | `HistoricalGameScraper.swift` |
|
||||||
|
| Pro-Football-Reference | `pro-football-reference.com/boxscores/...` | NFL | `HistoricalGameScraper.swift` |
|
||||||
|
|
||||||
|
- Uses SwiftSoup for HTML parsing
|
||||||
|
- On-device scraping (no server costs, unlimited scale)
|
||||||
|
- Cached per-session to avoid redundant requests
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
**SwiftData (Local):**
|
||||||
|
- Purpose: Offline-first data storage, user trips, preferences
|
||||||
|
- Location: App sandbox (automatic)
|
||||||
|
- CloudKit sync: Disabled (`cloudKitDatabase: .none`)
|
||||||
|
- Schema: See `SportsTimeApp.swift` ModelContainer configuration
|
||||||
|
|
||||||
|
**CloudKit Public Database (Remote):**
|
||||||
|
- Purpose: Shared schedule data for all users
|
||||||
|
- Container: `iCloud.com.sportstime.app`
|
||||||
|
- Access: Read (all users), Write (admin via Python uploader)
|
||||||
|
- Sync method: Date-based delta sync (modificationDate filtering)
|
||||||
|
|
||||||
|
**URLCache (Images):**
|
||||||
|
- Purpose: Cache team logos and stadium photos
|
||||||
|
- Memory: 50 MB
|
||||||
|
- Disk: 100 MB
|
||||||
|
- Path: `ImageCache`
|
||||||
|
- Client: `Export/Services/RemoteImageService.swift`
|
||||||
|
|
||||||
|
**File Storage:**
|
||||||
|
- Local filesystem only for PDF export (temporary)
|
||||||
|
- Photo library access for visit photo imports
|
||||||
|
|
||||||
|
**Caching:**
|
||||||
|
- In-memory game score cache: `ScoreResolutionCache.swift`
|
||||||
|
- In-memory scraper cache: `HistoricalGameScraper.swift`
|
||||||
|
- URLSession cache: Team logos, stadium photos
|
||||||
|
|
||||||
|
## Authentication & Identity
|
||||||
|
|
||||||
|
**iCloud (Automatic):**
|
||||||
|
- Users authenticate via their iCloud account
|
||||||
|
- Required for: CloudKit sync, poll voting identity
|
||||||
|
- Optional: App works offline without iCloud
|
||||||
|
|
||||||
|
**CloudKit Server-to-Server (Python Uploader):**
|
||||||
|
- JWT authentication with ECDSA P-256 key
|
||||||
|
- Key ID and team ID from Apple Developer portal
|
||||||
|
- Used by `Scripts/sportstime_parser/uploaders/cloudkit.py`
|
||||||
|
|
||||||
|
## Monitoring & Observability
|
||||||
|
|
||||||
|
**Error Tracking:**
|
||||||
|
- None (no Sentry, Crashlytics, etc.)
|
||||||
|
- Errors logged to console via `os.Logger`
|
||||||
|
|
||||||
|
**Logs:**
|
||||||
|
- `os.Logger` subsystem: `com.sportstime.app`
|
||||||
|
- Categories: `BackgroundSyncManager`, `NetworkMonitor`
|
||||||
|
- Rich console output in Python scraper
|
||||||
|
|
||||||
|
## CI/CD & Deployment
|
||||||
|
|
||||||
|
**Hosting:**
|
||||||
|
- iOS App Store (planned)
|
||||||
|
- CloudKit public database (Apple infrastructure)
|
||||||
|
|
||||||
|
**CI Pipeline:**
|
||||||
|
- None configured (no GitHub Actions, Xcode Cloud, etc.)
|
||||||
|
|
||||||
|
**Data Pipeline:**
|
||||||
|
- Python CLI (`sportstime-parser`) scrapes schedules
|
||||||
|
- Uploads to CloudKit via CloudKit Web Services API
|
||||||
|
- iOS apps sync from CloudKit automatically
|
||||||
|
|
||||||
|
## Background Processing
|
||||||
|
|
||||||
|
**BGTaskScheduler:**
|
||||||
|
- Refresh task: `com.sportstime.app.refresh` (periodic CloudKit sync)
|
||||||
|
- Processing task: `com.sportstime.app.db-cleanup` (overnight heavy sync)
|
||||||
|
- Manager: `Core/Services/BackgroundSyncManager.swift`
|
||||||
|
|
||||||
|
**Network Monitoring:**
|
||||||
|
- `NWPathMonitor` triggers sync on connectivity restoration
|
||||||
|
- Debounce: 2.5 seconds (handles WiFi/cellular handoffs)
|
||||||
|
- Manager: `Core/Services/NetworkMonitor.swift`
|
||||||
|
|
||||||
|
**Push Notifications:**
|
||||||
|
- CloudKit subscriptions for data changes
|
||||||
|
- Silent notifications (`shouldSendContentAvailable`)
|
||||||
|
- Subscription IDs: `game-updates`, `league-structure-updates`, `team-alias-updates`, `stadium-alias-updates`
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
**Required Environment Variables:**
|
||||||
|
|
||||||
|
*iOS App:*
|
||||||
|
- None required (CloudKit container ID hardcoded)
|
||||||
|
|
||||||
|
*Python Uploader:*
|
||||||
|
- CloudKit key ID (or in config)
|
||||||
|
- CloudKit team ID (or in config)
|
||||||
|
- ECDSA private key path
|
||||||
|
|
||||||
|
**Secrets Location:**
|
||||||
|
- iOS: Entitlements file (`SportsTime.entitlements`)
|
||||||
|
- Python: Environment variables or local config file
|
||||||
|
|
||||||
|
## Webhooks & Callbacks
|
||||||
|
|
||||||
|
**Incoming:**
|
||||||
|
- CloudKit silent push notifications (background sync trigger)
|
||||||
|
- Deep links: `sportstime://poll/{shareCode}` (poll sharing)
|
||||||
|
|
||||||
|
**Outgoing:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
**Internal Rate Limiter:**
|
||||||
|
- `Core/Services/RateLimiter.swift`
|
||||||
|
- Per-provider keys: `mlb_stats`, `nba_stats`, `nhl_stats`
|
||||||
|
- Prevents API abuse when resolving historical game scores
|
||||||
|
|
||||||
|
**Provider Auto-Disable:**
|
||||||
|
- Official APIs: Never auto-disabled
|
||||||
|
- Unofficial APIs: Disabled after 3 failures (24h cooldown)
|
||||||
|
- Scraped sources: Disabled after 2 failures (24h cooldown)
|
||||||
|
|
||||||
|
## Polls (Group Trip Planning)
|
||||||
|
|
||||||
|
**CloudKit-Based Polling:**
|
||||||
|
- Record type: `TripPoll`, `PollVote`
|
||||||
|
- Share codes: 6-character uppercase alphanumeric
|
||||||
|
- Deep link: `sportstime://poll/{shareCode}`
|
||||||
|
- Service: `Core/Services/PollService.swift`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Integration audit: 2026-01-18*
|
||||||
128
.planning/codebase/STACK.md
Normal file
128
.planning/codebase/STACK.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Technology Stack
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-01-18
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
**Primary:**
|
||||||
|
- Swift 5.0 - iOS app (`SportsTime/`)
|
||||||
|
- Python 3.11+ - Data scraping and CloudKit uploading (`Scripts/sportstime_parser/`)
|
||||||
|
|
||||||
|
**Secondary:**
|
||||||
|
- JSON - Data interchange format, bundled bootstrap data, configuration
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
**iOS Environment:**
|
||||||
|
- iOS 26+ (iOS 26.2 targeted per Xcode 26.2)
|
||||||
|
- Swift 6 concurrency model enabled (`SWIFT_APPROACHABLE_CONCURRENCY = YES`, `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`)
|
||||||
|
- Deployment targets: iPhone and iPad (`TARGETED_DEVICE_FAMILY = "1,2"`)
|
||||||
|
|
||||||
|
**Python Environment:**
|
||||||
|
- Python 3.11, 3.12, or 3.13
|
||||||
|
- No lockfile (uses requirements.txt + pyproject.toml)
|
||||||
|
|
||||||
|
## Frameworks
|
||||||
|
|
||||||
|
**Core iOS Frameworks:**
|
||||||
|
- SwiftUI - UI framework (entire app)
|
||||||
|
- SwiftData - Local persistence (`Core/Models/Local/`)
|
||||||
|
- CloudKit - Remote data sync (`Core/Services/CloudKitService.swift`)
|
||||||
|
- StoreKit 2 - In-app purchases (`Core/Store/StoreManager.swift`)
|
||||||
|
- MapKit - Geocoding, routing, map snapshots, EV chargers
|
||||||
|
- CoreLocation - Location services
|
||||||
|
- PDFKit - PDF generation (`Export/PDFGenerator.swift`)
|
||||||
|
- BackgroundTasks - Background sync scheduling
|
||||||
|
- Network - Network path monitoring (`NWPathMonitor`)
|
||||||
|
- Photos/PhotosUI - Photo library access for visit imports
|
||||||
|
- CryptoKit - Hash generation for canonical data
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- XCTest - Unit and UI tests (`SportsTimeTests/`, `SportsTimeUITests/`)
|
||||||
|
- pytest 8.0+ - Python tests (`Scripts/sportstime_parser/tests/`)
|
||||||
|
|
||||||
|
**Build/Dev:**
|
||||||
|
- Xcode 26.2 - Build system
|
||||||
|
- Swift Package Manager - Dependency management (single package)
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
**iOS - Swift Package Manager:**
|
||||||
|
- SwiftSoup (HTML parsing) - Used in `HistoricalGameScraper.swift` for Sports-Reference scraping
|
||||||
|
|
||||||
|
**Python - pip:**
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| requests | 2.31.0+ | HTTP client for API calls |
|
||||||
|
| beautifulsoup4 | 4.12.0+ | HTML parsing |
|
||||||
|
| lxml | 5.0.0+ | XML/HTML parser backend |
|
||||||
|
| rapidfuzz | 3.5.0+ | Fuzzy string matching for team names |
|
||||||
|
| python-dateutil | 2.8.0+ | Date parsing |
|
||||||
|
| pytz | 2024.1+ | Timezone handling |
|
||||||
|
| rich | 13.7.0+ | CLI output formatting |
|
||||||
|
| pyjwt | 2.8.0+ | JWT token generation for CloudKit auth |
|
||||||
|
| cryptography | 42.0.0+ | ECDSA signing for CloudKit requests |
|
||||||
|
|
||||||
|
**Dev Dependencies (Python):**
|
||||||
|
- pytest 8.0+ - Test runner
|
||||||
|
- pytest-cov 4.1+ - Coverage reporting
|
||||||
|
- responses 0.25+ - HTTP mocking
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**Environment Variables:**
|
||||||
|
- CloudKit Web Services requires ECDSA private key for server-to-server auth
|
||||||
|
- iOS app uses iCloud container identifier: `iCloud.com.sportstime.app`
|
||||||
|
|
||||||
|
**Build Settings:**
|
||||||
|
| Setting | Value | Purpose |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| `SWIFT_VERSION` | 5.0 | Swift language version |
|
||||||
|
| `SWIFT_APPROACHABLE_CONCURRENCY` | YES | Modern concurrency |
|
||||||
|
| `SWIFT_DEFAULT_ACTOR_ISOLATION` | MainActor | Default isolation |
|
||||||
|
| `SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY` | YES | Swift 6 prep |
|
||||||
|
|
||||||
|
**Info.plist:**
|
||||||
|
- Background modes: `remote-notification`, `fetch`, `processing`
|
||||||
|
- Background task IDs: `com.sportstime.app.refresh`, `com.sportstime.app.db-cleanup`
|
||||||
|
- Photo library usage description for visit imports
|
||||||
|
|
||||||
|
**Entitlements:**
|
||||||
|
- CloudKit: `iCloud.com.sportstime.app`
|
||||||
|
- Push notifications: `aps-environment` (development)
|
||||||
|
|
||||||
|
## Platform Requirements
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- macOS with Xcode 26.2
|
||||||
|
- Python 3.11+ for data scraping
|
||||||
|
- iCloud account for CloudKit testing
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
- iOS 26+ (uses iOS 26 APIs, deprecated CLGeocoder annotations)
|
||||||
|
- iCloud account for data sync (optional, works offline)
|
||||||
|
- Network for CloudKit sync (offline-first architecture)
|
||||||
|
|
||||||
|
## Data Architecture
|
||||||
|
|
||||||
|
**Three-Tier Storage:**
|
||||||
|
1. **Bundled JSON** (`SportsTime/Resources/`) - Bootstrap data for first launch
|
||||||
|
2. **SwiftData** (local) - Runtime source of truth, offline-capable
|
||||||
|
3. **CloudKit** (remote) - Public database for schedule updates
|
||||||
|
|
||||||
|
**Key Data Files:**
|
||||||
|
- `games_canonical.json` - Pre-bundled game schedules
|
||||||
|
- `teams_canonical.json` - Team definitions
|
||||||
|
- `stadiums_canonical.json` - Stadium definitions
|
||||||
|
- `league_structure.json` - League/conference/division hierarchy
|
||||||
|
- `team_aliases.json` - Historical team name mappings
|
||||||
|
- `stadium_aliases.json` - Historical stadium name mappings
|
||||||
|
|
||||||
|
**SwiftData Models:**
|
||||||
|
- Canonical: `CanonicalStadium`, `CanonicalTeam`, `CanonicalGame`, `CanonicalSport`
|
||||||
|
- User: `SavedTrip`, `StadiumVisit`, `UserPreferences`, `Achievement`
|
||||||
|
- Sync: `SyncState`, `LeagueStructureModel`, `TeamAlias`, `StadiumAlias`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Stack analysis: 2026-01-18*
|
||||||
86
.planning/phases/02-constraint-validation/02-VERIFICATION.md
Normal file
86
.planning/phases/02-constraint-validation/02-VERIFICATION.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
phase: 02-constraint-validation
|
||||||
|
verified: 2026-01-18T21:30:00Z
|
||||||
|
status: passed
|
||||||
|
score: 6/6 must-haves verified
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 2: Constraint Validation Verification Report
|
||||||
|
|
||||||
|
**Phase Goal:** The system prevents invalid positions and enforces item-specific rules.
|
||||||
|
**Verified:** 2026-01-18T21:30:00Z
|
||||||
|
**Status:** passed
|
||||||
|
**Re-verification:** No - initial verification
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### Observable Truths
|
||||||
|
|
||||||
|
| # | Truth | Status | Evidence |
|
||||||
|
|---|-------|--------|----------|
|
||||||
|
| 1 | Games cannot be moved (CONS-01) | VERIFIED | `ItineraryConstraints.isValidPosition()` returns `false` for `.game` case (line 28-30). Tests `game_cannotBeMoved` and `success_gameNotDraggable` verify. |
|
||||||
|
| 2 | Travel segments constrained to valid day range (CONS-02) | VERIFIED | `validDayRange()` method (line 47-60) calculates range based on departure/arrival games. Tests `travel_validDayRange_simpleCase`, `travel_cannotGoOutsideValidDayRange` verify. |
|
||||||
|
| 3 | Travel must be after from-city games, before to-city games on same day (CONS-03) | VERIFIED | `isValidTravelPosition()` method (line 85-121) enforces sortOrder constraints. 5 tests cover this requirement. |
|
||||||
|
| 4 | Custom items have no constraints (CONS-04) | VERIFIED | `isValidPosition()` returns `true` for `.custom` case (line 40-43). Tests `custom_canGoOnAnyDay`, `custom_canGoBeforeOrAfterGames`, `success_customNotePlacedAnywhere` verify. |
|
||||||
|
| 5 | Tests use Swift Testing framework (@Test, @Suite) | VERIFIED | File uses `import Testing`, `@Suite("ItineraryConstraints")`, and `@Test` decorators. 22 tests total. |
|
||||||
|
| 6 | API documentation exists for Phase 4 integration | VERIFIED | `CONSTRAINT-API.md` documents `isValidPosition()`, `validDayRange()`, `barrierGames()` with usage examples. |
|
||||||
|
|
||||||
|
**Score:** 6/6 truths verified
|
||||||
|
|
||||||
|
### Required Artifacts
|
||||||
|
|
||||||
|
| Artifact | Expected | Status | Details |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| `SportsTimeTests/Domain/ItineraryConstraintsTests.swift` | Migrated constraint tests with @Suite | EXISTS + SUBSTANTIVE + WIRED | 398 lines, 22 @Test functions, uses Swift Testing, imports @testable SportsTime |
|
||||||
|
| `SportsTime/Core/Models/Domain/ItineraryConstraints.swift` | Implementation with isValidPosition | EXISTS + SUBSTANTIVE | 134 lines, 3 public methods, no stub patterns |
|
||||||
|
| `.planning/phases/02-constraint-validation/CONSTRAINT-API.md` | API documentation for Phase 4 | EXISTS + SUBSTANTIVE | 165 lines, documents all 3 public methods with examples |
|
||||||
|
|
||||||
|
### Key Link Verification
|
||||||
|
|
||||||
|
| From | To | Via | Status | Details |
|
||||||
|
|------|-----|-----|--------|---------|
|
||||||
|
| `ItineraryConstraintsTests.swift` | `ItineraryConstraints.swift` | `@testable import SportsTime` | WIRED | Line 11: `@testable import SportsTime` |
|
||||||
|
| `CONSTRAINT-API.md` | `ItineraryConstraints.swift` | documents public API | WIRED | Documents `isValidPosition`, `validDayRange`, `barrierGames` methods |
|
||||||
|
|
||||||
|
### Requirements Coverage
|
||||||
|
|
||||||
|
| Requirement | Status | Test Evidence |
|
||||||
|
|-------------|--------|---------------|
|
||||||
|
| CONS-01: Games cannot be moved | SATISFIED | `game_cannotBeMoved()`, `success_gameNotDraggable()` |
|
||||||
|
| CONS-02: Travel segments constrained to valid day range | SATISFIED | `travel_validDayRange_simpleCase()`, `travel_cannotGoOutsideValidDayRange()`, `edge_travelNoGamesInEitherCity_hasFullRange()` |
|
||||||
|
| CONS-03: Travel segments must be after from-city games, before to-city games on same day | SATISFIED | `travel_mustBeAfterDepartureGames()`, `travel_mustBeBeforeArrivalGames()`, `travel_canBeAnywhereOnRestDays()`, `travel_mustBeAfterAllDepartureGamesOnSameDay()`, `travel_mustBeBeforeAllArrivalGamesOnSameDay()` |
|
||||||
|
| CONS-04: Custom items have no constraints | SATISFIED | `custom_canGoOnAnyDay()`, `custom_canGoBeforeOrAfterGames()`, `success_customNotePlacedAnywhere()` |
|
||||||
|
|
||||||
|
### Success Criteria from Roadmap
|
||||||
|
|
||||||
|
| Criteria | Status | Evidence |
|
||||||
|
|----------|--------|----------|
|
||||||
|
| Attempting to drag a game row shows no drag interaction | VERIFIED | `isValidPosition()` returns `false` for all game positions. Test `success_gameNotDraggable()` verifies. |
|
||||||
|
| Travel segment between Chicago and Boston cannot be placed on Day 1 if Chicago games extend through Day 2 | VERIFIED | `validDayRange()` returns minDay based on last departure game. Test `travel_cannotGoOutsideValidDayRange()` verifies. |
|
||||||
|
| Custom note item can be placed before, between, or after games on any day | VERIFIED | Test `success_customNotePlacedAnywhere()` explicitly verifies all positions. |
|
||||||
|
| Invalid position attempt returns rejection (constraint checker returns false) | VERIFIED | Test `success_invalidPositionReturnsRejection()` verifies false return. |
|
||||||
|
|
||||||
|
### Anti-Patterns Found
|
||||||
|
|
||||||
|
| File | Line | Pattern | Severity | Impact |
|
||||||
|
|------|------|---------|----------|--------|
|
||||||
|
| None | - | - | - | No anti-patterns detected |
|
||||||
|
|
||||||
|
### Human Verification Required
|
||||||
|
|
||||||
|
None - all success criteria are verifiable through automated tests.
|
||||||
|
|
||||||
|
### Test Execution Summary
|
||||||
|
|
||||||
|
All 22 constraint tests pass:
|
||||||
|
- 2 tests for CONS-01 (games immutable)
|
||||||
|
- 3 tests for CONS-02 (travel day range)
|
||||||
|
- 5 tests for CONS-03 (travel sortOrder)
|
||||||
|
- 2 tests for CONS-04 (custom flexibility)
|
||||||
|
- 8 tests for edge cases (boundaries, impossible constraints)
|
||||||
|
- 3 tests for success criteria verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Verified: 2026-01-18T21:30:00Z_
|
||||||
|
_Verifier: Claude (gsd-verifier)_
|
||||||
101
.planning/phases/03-visual-flattening/03-01-SUMMARY.md
Normal file
101
.planning/phases/03-visual-flattening/03-01-SUMMARY.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
phase: 03-visual-flattening
|
||||||
|
plan: 01
|
||||||
|
subsystem: ui
|
||||||
|
tags: [uitableview, flattening, sortorder, swift]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 01-semantic-position-model
|
||||||
|
provides: SortOrderProvider for game time → sortOrder conversion
|
||||||
|
- phase: 02-constraint-validation
|
||||||
|
provides: ItineraryConstraints for drag validation
|
||||||
|
provides:
|
||||||
|
- ItineraryFlattener pure utility for deterministic display ordering
|
||||||
|
- Refactored reloadData() using sortOrder-based flattening
|
||||||
|
affects: [03-02-flattening-tests, 04-drag-drop]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [pure-function-for-flattening, sortorder-based-display-ordering]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- SportsTime/Core/Models/Domain/ItineraryFlattener.swift
|
||||||
|
modified:
|
||||||
|
- SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Day headers are not positioned items - always first in each day"
|
||||||
|
- "Games get sortOrder from SortOrderProvider.initialSortOrder(forGameTime:)"
|
||||||
|
- "Travel sortOrder looked up from itineraryItems by city name matching"
|
||||||
|
- "Pure flatten function enables unit testing without UITableView"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "ItineraryFlattener.flatten(): Pure function for hierarchical to flat conversion"
|
||||||
|
- "gamesByDay dictionary: Group games by day number before flattening"
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-01-18
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3 Plan 1: Visual Flattening Summary
|
||||||
|
|
||||||
|
**Pure ItineraryFlattener utility replacing bucket-based flattening with sortOrder-based deterministic display ordering**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-01-18T21:52:57Z
|
||||||
|
- **Completed:** 2026-01-18T21:55:13Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 2
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Created `ItineraryFlattener` enum with pure `flatten()` function
|
||||||
|
- Replaced bucket-based flattening (beforeGames/afterGames) with sortOrder sort
|
||||||
|
- Deterministic display order: same semantic state always produces same row order
|
||||||
|
- Updated architecture documentation to reflect sortOrder-based approach
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create ItineraryFlattener utility** - `fdfc912` (feat)
|
||||||
|
2. **Task 2: Refactor reloadData() to use ItineraryFlattener** - `dd1fd82` (refactor)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `SportsTime/Core/Models/Domain/ItineraryFlattener.swift` - Pure flatten function with sortOrder-based ordering
|
||||||
|
- `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` - Refactored reloadData() to use flattener
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- **Day headers not positioned:** Day headers are always first in each day, not part of the sortOrder-based ordering. This is a structural anchor.
|
||||||
|
- **Flattener is a pure function:** No instance state, no side effects. Enables easy unit testing and guarantees determinism.
|
||||||
|
- **gamesByDay dictionary:** Built in reloadData() to pass to flattener rather than having flattener access day.games directly.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- ItineraryFlattener ready for unit tests in plan 03-02
|
||||||
|
- reloadData() now uses pure flattening, enabling snapshot testing
|
||||||
|
- Phase 4 drag-drop can rely on deterministic flatten output
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 03-visual-flattening*
|
||||||
|
*Completed: 2026-01-18*
|
||||||
126
.planning/phases/03-visual-flattening/03-02-SUMMARY.md
Normal file
126
.planning/phases/03-visual-flattening/03-02-SUMMARY.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
phase: 03-visual-flattening
|
||||||
|
plan: 02
|
||||||
|
subsystem: testing
|
||||||
|
tags: [swift-testing, itinerary, flattening, determinism]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 03-visual-flattening
|
||||||
|
plan: 01
|
||||||
|
provides: ItineraryFlattener.flatten() pure function
|
||||||
|
provides:
|
||||||
|
- 13 tests verifying ItineraryFlattener determinism
|
||||||
|
- 3 success criteria tests mapping to ROADMAP requirements
|
||||||
|
affects: [04-drag-drop]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [success-criteria-test-naming, integration-style-testing]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- SportsTimeTests/Domain/ItineraryFlattenerTests.swift
|
||||||
|
modified: []
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "success_* prefix for tests mapping to ROADMAP success criteria"
|
||||||
|
- "Integration tests use realistic multi-day trip configurations"
|
||||||
|
- "Test helpers create minimal mock data matching production types"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "success_negativeSortOrderAppearsBeforeGames: Tests negative sortOrder positioning"
|
||||||
|
- "success_sameStateProducesIdenticalOrder: Tests determinism guarantee"
|
||||||
|
- "success_reorderPreservesNewOrder: Tests persistence after sortOrder change"
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 15min
|
||||||
|
completed: 2026-01-18
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3 Plan 2: Flattening Tests Summary
|
||||||
|
|
||||||
|
**13 tests verifying ItineraryFlattener produces deterministic, sortOrder-based output with explicit ROADMAP success criteria coverage**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 15 min
|
||||||
|
- **Started:** 2026-01-18T21:59:19Z
|
||||||
|
- **Completed:** 2026-01-18T22:13:59Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Tests added:** 13
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Created `ItineraryFlattenerTests.swift` with comprehensive test coverage
|
||||||
|
- 3 success criteria tests mapping directly to ROADMAP requirements
|
||||||
|
- 6 comprehensive coverage tests for edge cases
|
||||||
|
- 4 integration tests for realistic trip data configurations
|
||||||
|
- All tests pass, full test suite confirms no regressions
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create ItineraryFlattenerTests** - `378e0ad` (test)
|
||||||
|
2. **Task 2: Add integration tests** - `724b9f8` (test)
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
- `SportsTimeTests/Domain/ItineraryFlattenerTests.swift` - 765 lines, 13 tests
|
||||||
|
|
||||||
|
## Success Criteria Verification
|
||||||
|
|
||||||
|
The three ROADMAP success criteria now have explicit tests:
|
||||||
|
|
||||||
|
| Success Criterion | Test Name |
|
||||||
|
|-------------------|-----------|
|
||||||
|
| Negative sortOrder before games | `success_negativeSortOrderAppearsBeforeGames` |
|
||||||
|
| Deterministic flattening | `success_sameStateProducesIdenticalOrder` |
|
||||||
|
| Reorder preserves position | `success_reorderPreservesNewOrder` |
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
### Success Criteria Tests (3)
|
||||||
|
- `success_negativeSortOrderAppearsBeforeGames` - Item with sortOrder -1.0 appears before noon game
|
||||||
|
- `success_sameStateProducesIdenticalOrder` - Repeated flatten produces identical row IDs
|
||||||
|
- `success_reorderPreservesNewOrder` - Changed sortOrder results in new order after reflatten
|
||||||
|
|
||||||
|
### Comprehensive Coverage Tests (6)
|
||||||
|
- `flatten_dayHeaderAlwaysFirst` - Day header is structural anchor
|
||||||
|
- `flatten_sortsByDayThenSortOrder` - Items grouped by day, sorted within
|
||||||
|
- `flatten_gamesGetSortOrderFromTime` - 8pm game after item with sortOrder 500
|
||||||
|
- `flatten_gamesGetSortOrderFromTime_earlyGame` - 1pm game before item with sortOrder 1000
|
||||||
|
- `flatten_emptyDayHasOnlyHeader` - Rest day produces only header row
|
||||||
|
- `flatten_multipleGamesOnSameDay` - Multiple games in single games row
|
||||||
|
|
||||||
|
### Integration Tests (4)
|
||||||
|
- `flatten_travelWithNegativeSortOrderAppearsBeforeGames` - Travel at -5.0 before games
|
||||||
|
- `flatten_travelWithPositiveSortOrderAppearsAfterGames` - Travel at 1500.0 after games
|
||||||
|
- `flatten_mixedItemTypes` - Complex day with exact order verification
|
||||||
|
- `flatten_multiDayTrip` - 3-day trip with different configurations per day
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Phase 3 Complete
|
||||||
|
|
||||||
|
Phase 3 (Visual Flattening) is now complete:
|
||||||
|
- Plan 1: Created ItineraryFlattener pure utility
|
||||||
|
- Plan 2: Added comprehensive test coverage
|
||||||
|
|
||||||
|
Phase 4 (Drag-Drop) can now build on the verified, deterministic flattening behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 03-visual-flattening*
|
||||||
|
*Completed: 2026-01-18*
|
||||||
85
.planning/phases/03-visual-flattening/03-VERIFICATION.md
Normal file
85
.planning/phases/03-visual-flattening/03-VERIFICATION.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
phase: 03-visual-flattening
|
||||||
|
verified: 2026-01-18T22:30:00Z
|
||||||
|
status: passed
|
||||||
|
score: 7/7 must-haves verified
|
||||||
|
re_verification: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3: Visual Flattening Verification Report
|
||||||
|
|
||||||
|
**Phase Goal:** Semantic items flatten to display rows deterministically, sorted by sortOrder.
|
||||||
|
**Verified:** 2026-01-18T22:30:00Z
|
||||||
|
**Status:** passed
|
||||||
|
**Re-verification:** No - initial verification
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### Observable Truths
|
||||||
|
|
||||||
|
| # | Truth | Status | Evidence |
|
||||||
|
|---|-------|--------|----------|
|
||||||
|
| 1 | Items with lower sortOrder appear before items with higher sortOrder | VERIFIED | ItineraryFlattener.swift:93 sorts by sortOrder ascending |
|
||||||
|
| 2 | Items with sortOrder < 0 appear before games | VERIFIED | Test `success_negativeSortOrderAppearsBeforeGames` passes |
|
||||||
|
| 3 | Items with sortOrder >= 0 appear after or between games | VERIFIED | Tests `flatten_gamesGetSortOrderFromTime*` pass; games have sortOrder 100+minutes |
|
||||||
|
| 4 | Day headers always appear first in each day's section | VERIFIED | Test `flatten_dayHeaderAlwaysFirst` passes; ItineraryFlattener.swift:58 appends header first |
|
||||||
|
| 5 | Same semantic state produces identical row order | VERIFIED | Test `success_sameStateProducesIdenticalOrder` passes |
|
||||||
|
| 6 | Item with sortOrder -1.0 appears before all games | VERIFIED | Test `success_negativeSortOrderAppearsBeforeGames` explicitly tests this |
|
||||||
|
| 7 | Reordering an item and re-flattening preserves new order | VERIFIED | Test `success_reorderPreservesNewOrder` passes |
|
||||||
|
|
||||||
|
**Score:** 7/7 truths verified
|
||||||
|
|
||||||
|
### Required Artifacts
|
||||||
|
|
||||||
|
| Artifact | Expected | Status | Details |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| `SportsTime/Core/Models/Domain/ItineraryFlattener.swift` | Pure flatten function | VERIFIED | 124 lines, exports `flatten(days:itineraryItems:gamesByDay:)`, no stubs |
|
||||||
|
| `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` | Refactored reloadData() using ItineraryFlattener | VERIFIED | Line 444 calls `ItineraryFlattener.flatten()`, no beforeGames/afterGames buckets |
|
||||||
|
| `SportsTimeTests/Domain/ItineraryFlattenerTests.swift` | Determinism and ordering tests | VERIFIED | 765 lines, 13 tests, all pass |
|
||||||
|
|
||||||
|
### Key Link Verification
|
||||||
|
|
||||||
|
| From | To | Via | Status | Details |
|
||||||
|
|------|----|-----|--------|---------|
|
||||||
|
| `ItineraryTableViewController.reloadData()` | `ItineraryFlattener.flatten()` | function call | WIRED | Line 444 calls flattener with days, itineraryItems, gamesByDay |
|
||||||
|
| `ItineraryFlattenerTests` | `ItineraryFlattener.flatten()` | test assertions | WIRED | 13 tests call flatten and verify output |
|
||||||
|
|
||||||
|
### Requirements Coverage
|
||||||
|
|
||||||
|
| Requirement | Status | Evidence |
|
||||||
|
|-------------|--------|----------|
|
||||||
|
| FLAT-01: Visual flattening sorts by sortOrder within each day | SATISFIED | `positioned.sort { $0.sortOrder < $1.sortOrder }` at line 93 |
|
||||||
|
| FLAT-02: Flattening is deterministic and stateless | SATISFIED | Pure function enum with no instance state; test confirms identical output |
|
||||||
|
| FLAT-03: sortOrder < 0 for "before games", >= 0 for "after/between games" | SATISFIED | Games get sortOrder 100+minutes; negative sortOrder items sort before |
|
||||||
|
|
||||||
|
### Anti-Patterns Found
|
||||||
|
|
||||||
|
| File | Line | Pattern | Severity | Impact |
|
||||||
|
|------|------|---------|----------|--------|
|
||||||
|
| None | - | - | - | No anti-patterns detected |
|
||||||
|
|
||||||
|
Scanned files:
|
||||||
|
- `ItineraryFlattener.swift`: No TODO/FIXME/placeholder/stub patterns
|
||||||
|
- `ItineraryTableViewController.swift`: No beforeGames/afterGames bucket logic remains
|
||||||
|
|
||||||
|
### Human Verification Required
|
||||||
|
|
||||||
|
None - all phase goals are verifiable programmatically through:
|
||||||
|
1. Code inspection (sortOrder-based sorting logic)
|
||||||
|
2. Automated tests (13 passing tests covering all success criteria)
|
||||||
|
|
||||||
|
### Verification Summary
|
||||||
|
|
||||||
|
Phase 3 goal fully achieved:
|
||||||
|
|
||||||
|
1. **ItineraryFlattener utility created** - Pure function that transforms hierarchical day data into flat display rows sorted by sortOrder
|
||||||
|
2. **reloadData() refactored** - Now uses `ItineraryFlattener.flatten()` instead of bucket-based beforeGames/afterGames approach
|
||||||
|
3. **Determinism verified** - Same semantic state produces identical row order (tested)
|
||||||
|
4. **sortOrder conventions enforced** - Negative sortOrder items appear before games, positive after/between
|
||||||
|
|
||||||
|
The old bucket-based flattening code has been completely removed. The new implementation is pure, testable, and deterministic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Verified: 2026-01-18T22:30:00Z*
|
||||||
|
*Verifier: Claude (gsd-verifier)*
|
||||||
Reference in New Issue
Block a user