--- phase: 02-constraint-validation plan: 02 type: execute wave: 1 depends_on: [] files_modified: - SportsTimeTests/Domain/ItineraryConstraintsTests.swift autonomous: true user_setup: [] must_haves: truths: - "Edge cases are tested (empty trip, single-day trip, boundary sortOrders)" - "Success criteria from roadmap are verifiable by tests" - "Phase 4 has clear API documentation for drag-drop integration" artifacts: - path: "SportsTimeTests/Domain/ItineraryConstraintsTests.swift" provides: "Complete constraint test suite with edge cases" contains: "Edge Cases" min_lines: 280 - path: ".planning/phases/02-constraint-validation/CONSTRAINT-API.md" provides: "API documentation for Phase 4" contains: "isValidPosition" key_links: - from: ".planning/phases/02-constraint-validation/CONSTRAINT-API.md" to: "SportsTime/Core/Models/Domain/ItineraryConstraints.swift" via: "documents public API" pattern: "ItineraryConstraints" --- Add edge case tests and create API documentation for Phase 4 integration. Purpose: Ensure constraint system handles boundary conditions and provide clear reference for drag-drop implementation. Output: Enhanced test suite with edge cases, API documentation for Phase 4. @~/.claude/get-shit-done/workflows/execute-plan.md @~/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-constraint-validation/02-RESEARCH.md # Implementation being tested @SportsTime/Core/Models/Domain/ItineraryConstraints.swift # Test file (will be enhanced) @SportsTimeTests/Domain/ItineraryConstraintsTests.swift Task 1: Add edge case tests SportsTimeTests/Domain/ItineraryConstraintsTests.swift Add the following edge case tests to the `// MARK: - Edge Cases` section: ```swift // MARK: - Edge Cases @Test("edge: single-day trip accepts valid positions") func edge_singleDayTrip_acceptsValidPositions() { let constraints = makeConstraints(tripDays: 1, gameDays: []) let custom = makeCustomItem(day: 1, sortOrder: 50) #expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 50)) #expect(!constraints.isValidPosition(for: custom, day: 0, sortOrder: 50)) #expect(!constraints.isValidPosition(for: custom, day: 2, sortOrder: 50)) } @Test("edge: day 0 is always invalid") func edge_day0_isAlwaysInvalid() { let constraints = makeConstraints(tripDays: 5, gameDays: []) let custom = makeCustomItem(day: 1, sortOrder: 50) #expect(!constraints.isValidPosition(for: custom, day: 0, sortOrder: 50)) } @Test("edge: day beyond trip is invalid") func edge_dayBeyondTrip_isInvalid() { let constraints = makeConstraints(tripDays: 3, gameDays: []) let custom = makeCustomItem(day: 1, sortOrder: 50) #expect(!constraints.isValidPosition(for: custom, day: 4, sortOrder: 50)) #expect(!constraints.isValidPosition(for: custom, day: 100, sortOrder: 50)) } @Test("edge: travel at exact game sortOrder boundary is invalid") func edge_travelAtExactGameSortOrder_isInvalid() { // Game at sortOrder 100 let constraints = makeConstraints( tripDays: 3, games: [makeGameItem(city: "Chicago", day: 1, sortOrder: 100)] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 100) // Exactly AT game sortOrder should be invalid (must be AFTER) #expect(!constraints.isValidPosition(for: travel, day: 1, sortOrder: 100)) // Just after should be valid #expect(constraints.isValidPosition(for: travel, day: 1, sortOrder: 100.001)) } @Test("edge: travel with no games in either city has full range") func edge_travelNoGamesInEitherCity_hasFullRange() { let constraints = makeConstraints(tripDays: 5, games: []) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50) // Valid on any day for day in 1...5 { #expect(constraints.isValidPosition(for: travel, day: day, sortOrder: 50)) } // Full range #expect(constraints.validDayRange(for: travel) == 1...5) } @Test("edge: negative sortOrder is valid for custom items") func edge_negativeSortOrder_validForCustomItems() { let constraints = makeConstraints(tripDays: 3, gameDays: []) let custom = makeCustomItem(day: 2, sortOrder: -100) #expect(constraints.isValidPosition(for: custom, day: 2, sortOrder: -100)) } @Test("edge: very large sortOrder is valid for custom items") func edge_veryLargeSortOrder_validForCustomItems() { let constraints = makeConstraints(tripDays: 3, gameDays: []) let custom = makeCustomItem(day: 2, sortOrder: 10000) #expect(constraints.isValidPosition(for: custom, day: 2, sortOrder: 10000)) } ``` Also add a test verifying the roadmap success criteria are testable: ```swift // MARK: - Success Criteria Verification @Test("success: game row shows no drag interaction (game not draggable)") func success_gameNotDraggable() { // Games return false for ANY position, making them non-draggable let game = makeGameItem(city: "Chicago", day: 2, sortOrder: 100) let constraints = makeConstraints(tripDays: 5, games: [game]) // Same position #expect(!constraints.isValidPosition(for: game, day: 2, sortOrder: 100)) // Different day #expect(!constraints.isValidPosition(for: game, day: 1, sortOrder: 100)) // Different sortOrder #expect(!constraints.isValidPosition(for: game, day: 2, sortOrder: 50)) } @Test("success: custom note can be placed anywhere") func success_customNotePlacedAnywhere() { // Custom items can be placed before, between, or after games on any day let constraints = makeConstraints( tripDays: 3, games: [ makeGameItem(city: "Chicago", day: 1, sortOrder: 100), makeGameItem(city: "Chicago", day: 1, sortOrder: 200), makeGameItem(city: "Detroit", day: 3, sortOrder: 100) ] ) let custom = makeCustomItem(day: 1, sortOrder: 50) // Before games on day 1 #expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 50)) // Between games on day 1 #expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 150)) // After games on day 1 #expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 250)) // On rest day (day 2) #expect(constraints.isValidPosition(for: custom, day: 2, sortOrder: 50)) // On day 3 with different city games #expect(constraints.isValidPosition(for: custom, day: 3, sortOrder: 50)) } @Test("success: invalid position returns false (rejection)") func success_invalidPositionReturnsRejection() { // Travel segment cannot be placed before departure game let constraints = makeConstraints( tripDays: 3, games: [makeGameItem(city: "Chicago", day: 2, sortOrder: 100)] ) let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50) // Day 1 is before Chicago game on Day 2, so invalid #expect(!constraints.isValidPosition(for: travel, day: 1, sortOrder: 50)) } ``` Run updated tests: ``` xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/ItineraryConstraintsTests test 2>&1 | grep -E "(Test Suite|Executed|passed|failed)" ``` All tests pass (original 13 + 10 new edge cases = 23 total). 10 additional edge case tests added and passing Task 2: Create API documentation for Phase 4 .planning/phases/02-constraint-validation/CONSTRAINT-API.md Create API documentation that Phase 4 can reference for drag-drop integration: ```markdown # ItineraryConstraints API **Location:** `SportsTime/Core/Models/Domain/ItineraryConstraints.swift` **Verified by:** 23 tests in `SportsTimeTests/Domain/ItineraryConstraintsTests.swift` ## Overview `ItineraryConstraints` validates item positions during drag-drop operations. It enforces: - **Games cannot move** (CONS-01) - **Travel segments have day range limits** (CONS-02) - **Travel segments must respect game sortOrder on same day** (CONS-03) - **Custom items have no constraints** (CONS-04) ## Construction ```swift let constraints = ItineraryConstraints( tripDayCount: days.count, items: allItineraryItems // All items including games ) ``` **Parameters:** - `tripDayCount`: Total days in trip (1-indexed, so a 5-day trip has days 1-5) - `items`: All itinerary items (games, travel, custom). Games are used to calculate constraints for travel items. ## Public API ### `isValidPosition(for:day:sortOrder:) -> Bool` Check if a specific position is valid for an item. ```swift func isValidPosition(for item: ItineraryItem, day: Int, sortOrder: Double) -> Bool ``` **Usage during drag:** ```swift // On each drag position update let dropPosition = calculateDropPosition(at: touchLocation) let isValid = constraints.isValidPosition( for: draggedItem, day: dropPosition.day, sortOrder: dropPosition.sortOrder ) if isValid { showValidDropIndicator() } else { showInvalidDropIndicator() } ``` **Returns:** - `true`: Position is valid, allow drop - `false`: Position is invalid, reject drop (snap back) **Rules by item type:** | Item Type | Day Constraint | SortOrder Constraint | |-----------|----------------|----------------------| | `.game` | Always `false` | Always `false` | | `.travel` | Within valid day range | After departure games, before arrival games | | `.custom` | Any day 1...tripDayCount | Any sortOrder | ### `validDayRange(for:) -> ClosedRange?` Get the valid day range for a travel item (for visual feedback). ```swift func validDayRange(for item: ItineraryItem) -> ClosedRange? ``` **Usage at drag start:** ```swift // When drag begins, precompute valid range guard case .travel = draggedItem.kind, let validRange = constraints.validDayRange(for: draggedItem) else { // Not a travel item or impossible constraints return } // Use range to dim invalid days for day in 1...tripDayCount { if !validRange.contains(day) { dimDay(day) } } ``` **Returns:** - `ClosedRange`: Valid day range (e.g., `2...4`) - `nil`: Constraints are impossible (e.g., departure game after arrival game) ### `barrierGames(for:) -> [ItineraryItem]` Get games that constrain a travel item (for visual highlighting). ```swift func barrierGames(for item: ItineraryItem) -> [ItineraryItem] ``` **Usage for visual feedback:** ```swift // Highlight barrier games during drag let barriers = constraints.barrierGames(for: travelItem) for barrier in barriers { highlightAsBarrier(barrier) // e.g., gold border } ``` **Returns:** - Array of game items: Last departure city game + first arrival city game - Empty array: Not a travel item or no constraining games ## Integration Points ### ItineraryTableViewController (existing) ```swift // In reloadData() self.constraints = ItineraryConstraints(tripDayCount: tripDayCount, items: itineraryItems) // In drag handling if constraints.isValidPosition(for: draggedItem, day: targetDay, sortOrder: targetSortOrder) { // Allow drop } else { // Reject drop, snap back } ``` ### Phase 4 Implementation Notes 1. **Drag Start:** - Check `item.isReorderable` (games return `false`) - Call `validDayRange(for:)` to precompute valid days - Call `barrierGames(for:)` to identify visual barriers 2. **Drag Move:** - Calculate target (day, sortOrder) from touch position - Call `isValidPosition(for:day:sortOrder:)` for real-time feedback - Update insertion line (valid) or red indicator (invalid) 3. **Drag End:** - Final `isValidPosition(for:day:sortOrder:)` check - Valid: Update item's day/sortOrder, animate settle - Invalid: Animate snap back, haptic feedback ## Test Coverage | Requirement | Tests | Verified | |-------------|-------|----------| | CONS-01 (games cannot move) | 2 | Yes | | CONS-02 (travel day range) | 5 | Yes | | CONS-03 (travel sortOrder) | 5 | Yes | | CONS-04 (custom flexibility) | 4 | Yes | | Edge cases | 7 | Yes | | **Total** | **23** | **100%** | --- *API documented: Phase 02* *Ready for: Phase 04 (Drag Interaction)* ``` File exists and contains all three public methods with usage examples CONSTRAINT-API.md created with complete API documentation Task 3: Run full test suite and commit None (verification only) 1. Run full test suite to verify no regressions: ``` xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test 2>&1 | grep -E "(Test Suite|Executed|passed|failed)" ``` 2. Commit the edge case tests: ``` git add SportsTimeTests/Domain/ItineraryConstraintsTests.swift git commit -m "test(02-02): add edge case tests for constraint validation Add 10 edge case tests: - Single-day trip boundaries - Day 0 and beyond-trip validation - Exact sortOrder boundary behavior - Travel with no games in cities - Negative and large sortOrders - Success criteria verification tests Total: 23 constraint tests Co-Authored-By: Claude Opus 4.5 " ``` 3. Commit the API documentation: ``` git add .planning/phases/02-constraint-validation/CONSTRAINT-API.md git commit -m "docs(02-02): document ItineraryConstraints API for Phase 4 Document public API for drag-drop integration: - isValidPosition() for position validation - validDayRange() for precomputing valid days - barrierGames() for visual highlighting - Integration patterns for ItineraryTableViewController Co-Authored-By: Claude Opus 4.5 " ``` Full test suite passes with no regressions Edge case tests and API documentation committed After all tasks: 1. 23 total constraint tests pass (13 migrated + 10 edge cases) 2. Full test suite passes (no regressions) 3. CONSTRAINT-API.md exists with complete documentation 4. All commits follow project conventions - Edge cases tested: single-day, day boundaries, sortOrder boundaries, no-games scenarios - Roadmap success criteria are verifiable by tests - API documentation complete for Phase 4 integration - All tests pass After completion, create `.planning/phases/02-constraint-validation/02-02-SUMMARY.md`