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

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous user_setup must_haves
02-constraint-validation 02 execute 1
SportsTimeTests/Domain/ItineraryConstraintsTests.swift
true
truths artifacts key_links
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
path provides contains min_lines
SportsTimeTests/Domain/ItineraryConstraintsTests.swift Complete constraint test suite with edge cases Edge Cases 280
path provides contains
.planning/phases/02-constraint-validation/CONSTRAINT-API.md API documentation for Phase 4 isValidPosition
from to via pattern
.planning/phases/02-constraint-validation/CONSTRAINT-API.md SportsTime/Core/Models/Domain/ItineraryConstraints.swift documents public API 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.

<execution_context> @/.claude/get-shit-done/workflows/execute-plan.md @/.claude/get-shit-done/templates/summary.md </execution_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

Task 1: Add edge case tests SportsTimeTests/Domain/ItineraryConstraintsTests.swift Add the following edge case tests to the `// MARK: - Edge Cases` section:
// 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:

// 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:
# 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.

func isValidPosition(for item: ItineraryItem, day: Int, sortOrder: Double) -> Bool

Usage during drag:

// 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).

func validDayRange(for item: ItineraryItem) -> ClosedRange<Int>?

Usage at drag start:

// 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).

func barrierGames(for item: ItineraryItem) -> [ItineraryItem]

Usage for visual feedback:

// 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)

// 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>