Files
SportstimeAPI/.planning/phases/02-constraint-validation/02-02-PLAN.md
Trey t 51b6d46d84 docs(02): create phase 2 constraint validation plans
Phase 2: Constraint Validation
- 2 plans in 1 wave (parallel)
- Both plans autonomous (no checkpoints)

Plan 02-01: Migrate 13 XCTest tests to Swift Testing
Plan 02-02: Add edge case tests and document constraint API

Note: ItineraryConstraints is already fully implemented.
This phase verifies and standardizes tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 14:48:10 -06:00

450 lines
14 KiB
Markdown

---
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"
---
<objective>
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.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: Add edge case tests</name>
<files>SportsTimeTests/Domain/ItineraryConstraintsTests.swift</files>
<action>
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))
}
```
</action>
<verify>
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).
</verify>
<done>10 additional edge case tests added and passing</done>
</task>
<task type="auto">
<name>Task 2: Create API documentation for Phase 4</name>
<files>.planning/phases/02-constraint-validation/CONSTRAINT-API.md</files>
<action>
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<Int>?`
Get the valid day range for a travel item (for visual feedback).
```swift
func validDayRange(for item: ItineraryItem) -> ClosedRange<Int>?
```
**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<Int>`: 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)*
```
</action>
<verify>File exists and contains all three public methods with usage examples</verify>
<done>CONSTRAINT-API.md created with complete API documentation</done>
</task>
<task type="auto">
<name>Task 3: Run full test suite and commit</name>
<files>None (verification only)</files>
<action>
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 <noreply@anthropic.com>"
```
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 <noreply@anthropic.com>"
```
</action>
<verify>Full test suite passes with no regressions</verify>
<done>Edge case tests and API documentation committed</done>
</task>
</tasks>
<verification>
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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/02-constraint-validation/02-02-SUMMARY.md`
</output>