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>
This commit is contained in:
Trey t
2026-01-18 14:48:10 -06:00
parent aae302cf5b
commit 51b6d46d84
3 changed files with 645 additions and 1 deletions

View File

@@ -42,6 +42,12 @@ Plans:
**Dependencies:** Phase 1 (semantic position model) **Dependencies:** Phase 1 (semantic position model)
**Plans:** 2 plans
Plans:
- [ ] 02-01-PLAN.md - Migrate XCTest constraint tests to Swift Testing
- [ ] 02-02-PLAN.md - Add edge case tests and document constraint API
**Requirements:** **Requirements:**
- CONS-01: Games cannot be moved (fixed by schedule) - CONS-01: Games cannot be moved (fixed by schedule)
- CONS-02: Travel segments constrained to valid day range - CONS-02: Travel segments constrained to valid day range
@@ -104,7 +110,7 @@ Plans:
| Phase | Status | Requirements | Completed | | Phase | Status | Requirements | Completed |
|-------|--------|--------------|-----------| |-------|--------|--------------|-----------|
| 1 - Semantic Position Model | ✓ Complete | 8 | 8 | | 1 - Semantic Position Model | ✓ Complete | 8 | 8 |
| 2 - Constraint Validation | Not Started | 4 | 0 | | 2 - Constraint Validation | Planned | 4 | 0 |
| 3 - Visual Flattening | Not Started | 3 | 0 | | 3 - Visual Flattening | Not Started | 3 | 0 |
| 4 - Drag Interaction | Not Started | 8 | 0 | | 4 - Drag Interaction | Not Started | 8 | 0 |

View File

@@ -0,0 +1,189 @@
---
phase: 02-constraint-validation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- SportsTimeTests/ItineraryConstraintsTests.swift
autonomous: true
user_setup: []
must_haves:
truths:
- "All CONS-01 through CONS-04 requirements have corresponding passing tests"
- "Tests use Swift Testing framework (@Test, @Suite) matching Phase 1 patterns"
- "ItineraryConstraints API is fully tested with no coverage gaps"
artifacts:
- path: "SportsTimeTests/Domain/ItineraryConstraintsTests.swift"
provides: "Migrated constraint validation tests"
contains: "@Suite"
min_lines: 200
key_links:
- from: "SportsTimeTests/Domain/ItineraryConstraintsTests.swift"
to: "SportsTime/Core/Models/Domain/ItineraryConstraints.swift"
via: "import @testable SportsTime"
pattern: "@testable import SportsTime"
---
<objective>
Migrate the 13 existing XCTest constraint tests to Swift Testing and move them to the Domain test folder.
Purpose: Standardize test patterns across the project. Phase 1 established Swift Testing as the project standard; constraint tests should follow.
Output: `SportsTimeTests/Domain/ItineraryConstraintsTests.swift` with all tests passing using @Test/@Suite syntax.
</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
# Pattern reference from Phase 1
@SportsTimeTests/Domain/SortOrderProviderTests.swift
@SportsTimeTests/Domain/SemanticPositionPersistenceTests.swift
# Source test file to migrate
@SportsTimeTests/ItineraryConstraintsTests.swift
# Implementation being tested
@SportsTime/Core/Models/Domain/ItineraryConstraints.swift
</context>
<tasks>
<task type="auto">
<name>Task 1: Verify requirements coverage in existing tests</name>
<files>SportsTimeTests/ItineraryConstraintsTests.swift</files>
<action>
Read the existing 13 XCTest tests and map them to requirements:
| Requirement | Test(s) | Coverage |
|-------------|---------|----------|
| CONS-01 (games cannot move) | test_gameItem_cannotBeMoved | Verify complete |
| CONS-02 (travel day range) | test_travel_validDayRange_simpleCase, test_travel_cannotGoOutsideValidDayRange | Verify complete |
| CONS-03 (travel sortOrder on game days) | test_travel_mustBeAfterDepartureGames, test_travel_mustBeBeforeArrivalGames, test_travel_mustBeAfterAllDepartureGamesOnSameDay, test_travel_mustBeBeforeAllArrivalGamesOnSameDay, test_travel_canBeAnywhereOnRestDays | Verify complete |
| CONS-04 (custom no constraints) | test_customItem_canGoOnAnyDay, test_customItem_canGoBeforeOrAfterGames | Verify complete |
Document any gaps found. If all requirements are covered, proceed to migration.
</action>
<verify>Requirements coverage table is complete with no gaps</verify>
<done>All CONS-01 through CONS-04 requirements map to at least one existing test</done>
</task>
<task type="auto">
<name>Task 2: Migrate tests to Swift Testing</name>
<files>SportsTimeTests/Domain/ItineraryConstraintsTests.swift, SportsTimeTests/ItineraryConstraintsTests.swift</files>
<action>
1. Create new file at `SportsTimeTests/Domain/ItineraryConstraintsTests.swift`
2. Convert XCTest syntax to Swift Testing:
- `final class ItineraryConstraintsTests: XCTestCase` -> `@Suite("ItineraryConstraints") struct ItineraryConstraintsTests`
- `func test_*()` -> `@Test("description") func *()` (preserve test names, add descriptive strings)
- `XCTAssertTrue(x)` -> `#expect(x == true)` or `#expect(x)`
- `XCTAssertFalse(x)` -> `#expect(x == false)` or `#expect(!x)`
- `XCTAssertEqual(a, b)` -> `#expect(a == b)`
- `XCTAssertNil(x)` -> `#expect(x == nil)`
- `import XCTest` -> `import Testing`
3. Organize tests into logical groups using MARK comments:
- `// MARK: - Custom Item Tests (CONS-04)`
- `// MARK: - Travel Day Range Tests (CONS-02)`
- `// MARK: - Travel SortOrder Tests (CONS-03)`
- `// MARK: - Game Immutability Tests (CONS-01)`
- `// MARK: - Edge Cases`
- `// MARK: - Barrier Games`
- `// MARK: - Helpers`
4. Preserve all helper methods (makeConstraints, makeGameItem, makeTravelItem, makeCustomItem)
5. Delete the old file at `SportsTimeTests/ItineraryConstraintsTests.swift`
Pattern reference - follow SortOrderProviderTests.swift style:
```swift
import Testing
import Foundation
@testable import SportsTime
@Suite("ItineraryConstraints")
struct ItineraryConstraintsTests {
// MARK: - Custom Item Tests (CONS-04)
@Test("custom: can go on any day")
func custom_canGoOnAnyDay() {
let constraints = makeConstraints(tripDays: 5, gameDays: [1, 5])
let customItem = makeCustomItem(day: 1, sortOrder: 50)
for day in 1...5 {
#expect(constraints.isValidPosition(for: customItem, day: day, sortOrder: 50))
}
}
// ...
}
```
</action>
<verify>
Run 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 13 tests pass.
</verify>
<done>New file at Domain/ItineraryConstraintsTests.swift passes all 13 tests, old file deleted</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 migration:
```
git add SportsTimeTests/Domain/ItineraryConstraintsTests.swift
git rm SportsTimeTests/ItineraryConstraintsTests.swift
git commit -m "test(02-01): migrate ItineraryConstraints tests to Swift Testing
Migrate 13 XCTest tests to Swift Testing framework:
- Move to Domain/ folder to match project structure
- Convert XCTestCase to @Suite/@Test syntax
- Update assertions to #expect macros
- Verify all CONS-01 through CONS-04 requirements covered
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
```
</action>
<verify>Full test suite passes with no regressions</verify>
<done>Migration committed, all tests pass including existing 34 Phase 1 tests</done>
</task>
</tasks>
<verification>
After all tasks:
1. `SportsTimeTests/Domain/ItineraryConstraintsTests.swift` exists with @Suite/@Test syntax
2. Old `SportsTimeTests/ItineraryConstraintsTests.swift` is deleted
3. All 13 constraint tests pass
4. Full test suite passes (no regressions)
5. Tests organized by requirement (CONS-01 through CONS-04)
</verification>
<success_criteria>
- 13 tests migrated from XCTest to Swift Testing
- Tests use @Test/@Suite syntax matching Phase 1 patterns
- All CONS-01 through CONS-04 requirements have corresponding tests
- Full test suite passes
</success_criteria>
<output>
After completion, create `.planning/phases/02-constraint-validation/02-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,449 @@
---
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>