diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index b69a4ae..96f5a35 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -12,6 +12,12 @@ Build a drag-and-drop itinerary editor for SportsTime using UITableView bridged **Dependencies:** None (foundation phase) +**Plans:** 2 plans + +Plans: +- [ ] 01-01-PLAN.md - Create SortOrderProvider utility and Trip day derivation methods +- [ ] 01-02-PLAN.md - Create tests verifying semantic position persistence + **Requirements:** - DATA-01: All movable items have semantic position `(day: Int, sortOrder: Double)` - DATA-02: Travel segments are positioned items with their own sortOrder @@ -97,7 +103,7 @@ Build a drag-and-drop itinerary editor for SportsTime using UITableView bridged | Phase | Status | Requirements | Completed | |-------|--------|--------------|-----------| -| 1 - Semantic Position Model | Not Started | 8 | 0 | +| 1 - Semantic Position Model | Planned | 8 | 0 | | 2 - Constraint Validation | Not Started | 4 | 0 | | 3 - Visual Flattening | Not Started | 3 | 0 | | 4 - Drag Interaction | Not Started | 8 | 0 | diff --git a/.planning/phases/01-semantic-position-model/01-01-PLAN.md b/.planning/phases/01-semantic-position-model/01-01-PLAN.md new file mode 100644 index 0000000..863af5b --- /dev/null +++ b/.planning/phases/01-semantic-position-model/01-01-PLAN.md @@ -0,0 +1,155 @@ +--- +phase: 01-semantic-position-model +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - SportsTime/Core/Models/Domain/SortOrderProvider.swift + - SportsTime/Core/Models/Domain/Trip.swift +autonomous: true + +must_haves: + truths: + - "Games get sortOrder derived from game time (minutes since midnight + offset)" + - "Inserting between two items produces midpoint sortOrder" + - "Day number can be calculated from any date given trip start date" + artifacts: + - path: "SportsTime/Core/Models/Domain/SortOrderProvider.swift" + provides: "sortOrder calculation utilities" + exports: ["initialSortOrder(forGameTime:)", "sortOrderBetween(_:_:)", "sortOrderBefore(_:)", "sortOrderAfter(_:)", "needsNormalization(_:)", "normalize(_:)"] + - path: "SportsTime/Core/Models/Domain/Trip.swift" + provides: "Day derivation methods" + contains: "func dayNumber(for date: Date) -> Int" + key_links: + - from: "SortOrderProvider" + to: "ItineraryItem.sortOrder" + via: "Utilities compute values assigned to sortOrder property" + pattern: "sortOrder.*=.*SortOrderProvider" +--- + + +Create the sortOrder calculation utilities and day derivation methods that Phase 1 depends on. + +Purpose: Establish the foundational utilities for semantic position assignment. Games need sortOrder derived from time, travel/custom items need midpoint insertion, and items need day derivation from trip dates. + +Output: `SortOrderProvider.swift` with all sortOrder utilities, `Trip.swift` extended with day derivation methods. + + + +@~/.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/01-semantic-position-model/01-RESEARCH.md + +# Existing source files +@SportsTime/Core/Models/Domain/ItineraryItem.swift +@SportsTime/Core/Models/Domain/Trip.swift + + + + + + Task 1: Create SortOrderProvider utility + SportsTime/Core/Models/Domain/SortOrderProvider.swift + +Create a new file `SortOrderProvider.swift` with an enum containing static methods for sortOrder calculation. + +Include these methods (as specified in 01-RESEARCH.md): + +1. `initialSortOrder(forGameTime: Date) -> Double` + - Extract hour and minute from game time + - Calculate minutes since midnight + - Return 100.0 + minutesSinceMidnight (range: 100-1540) + - This ensures games sort by time and leaves room for negative sortOrder items + +2. `sortOrderBetween(_ above: Double, _ below: Double) -> Double` + - Return (above + below) / 2.0 + - Simple midpoint calculation + +3. `sortOrderBefore(_ first: Double) -> Double` + - Return first - 1.0 + - Creates space before the first item + +4. `sortOrderAfter(_ last: Double) -> Double` + - Return last + 1.0 + - Creates space after the last item + +5. `needsNormalization(_ items: [ItineraryItem]) -> Bool` + - Sort items by sortOrder + - Check if any adjacent gap is less than 1e-10 + - Return true if normalization needed + +6. `normalize(_ items: inout [ItineraryItem])` + - Sort by current sortOrder + - Reassign sortOrder as 1.0, 2.0, 3.0... (integer spacing) + - Updates items in place + +Use `Calendar.current` for date component extraction. Import Foundation only. + + +File exists at `SportsTime/Core/Models/Domain/SortOrderProvider.swift` with all 6 methods. Build succeeds: +```bash +xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20 +``` + + SortOrderProvider.swift exists with all 6 static methods, project builds without errors + + + + Task 2: Add day derivation methods to Trip + SportsTime/Core/Models/Domain/Trip.swift + +Extend the existing Trip struct with day derivation methods in a new extension at the bottom of the file. + +Add these methods: + +1. `func dayNumber(for date: Date) -> Int` + - Use Calendar.current to get startOfDay for both startDate and target date + - Calculate days between using dateComponents([.day], from:to:) + - Return days + 1 (1-indexed) + +2. `func date(forDay dayNumber: Int) -> Date?` + - Use Calendar.current to add (dayNumber - 1) days to startDate + - Return the resulting date + +Add a comment block explaining: +- Day 1 = trip.startDate +- Day 2 = startDate + 1 calendar day +- Games belong to their start date (even if running past midnight) + +These methods complement the existing `itineraryDays()` method but work with raw Date values rather than the Trip's stops structure. + + +Build succeeds and new methods are callable: +```bash +xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20 +``` + + Trip.swift has dayNumber(for:) and date(forDay:) methods, project builds without errors + + + + + +1. Both files exist and contain expected methods +2. `xcodebuild build` succeeds with no errors +3. SortOrderProvider methods are static and accessible as `SortOrderProvider.methodName()` +4. Trip extension methods are instance methods callable on any Trip value + + + +- SortOrderProvider.swift exists with 6 static methods for sortOrder calculation +- Trip.swift extended with dayNumber(for:) and date(forDay:) methods +- Project builds successfully +- No changes to existing ItineraryItem.swift (model already has correct fields) + + + +After completion, create `.planning/phases/01-semantic-position-model/01-01-SUMMARY.md` + diff --git a/.planning/phases/01-semantic-position-model/01-02-PLAN.md b/.planning/phases/01-semantic-position-model/01-02-PLAN.md new file mode 100644 index 0000000..b972b48 --- /dev/null +++ b/.planning/phases/01-semantic-position-model/01-02-PLAN.md @@ -0,0 +1,199 @@ +--- +phase: 01-semantic-position-model +plan: 02 +type: execute +wave: 2 +depends_on: ["01-01"] +files_modified: + - SportsTimeTests/SortOrderProviderTests.swift + - SportsTimeTests/SemanticPositionPersistenceTests.swift +autonomous: true + +must_haves: + truths: + - "User can persist an item's position, reload, and find it in the same location" + - "Moving travel segment to different day updates its day property" + - "Inserting between two items gets sortOrder between their values (e.g., 1.0 and 2.0 -> 1.5)" + - "Games remain fixed at their schedule-determined positions" + artifacts: + - path: "SportsTimeTests/SortOrderProviderTests.swift" + provides: "Unit tests for SortOrderProvider" + min_lines: 80 + - path: "SportsTimeTests/SemanticPositionPersistenceTests.swift" + provides: "Integration tests for position persistence" + min_lines: 100 + key_links: + - from: "SortOrderProviderTests" + to: "SortOrderProvider" + via: "Test imports and calls provider methods" + pattern: "SortOrderProvider\\." + - from: "SemanticPositionPersistenceTests" + to: "LocalItineraryItem" + via: "Creates and persists items via SwiftData" + pattern: "LocalItineraryItem" +--- + + +Create comprehensive tests verifying the semantic position model works correctly. + +Purpose: Prove that requirements DATA-01 through DATA-05 and PERS-01 through PERS-03 are satisfied. Tests must verify: sortOrder calculation correctness, midpoint insertion math, day derivation accuracy, and persistence survival across SwiftData reload. + +Output: Two test files covering unit tests for SortOrderProvider and integration tests for persistence behavior. + + + +@~/.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/01-semantic-position-model/01-RESEARCH.md +@.planning/phases/01-semantic-position-model/01-01-SUMMARY.md + +# Source files +@SportsTime/Core/Models/Domain/SortOrderProvider.swift +@SportsTime/Core/Models/Domain/ItineraryItem.swift +@SportsTime/Core/Models/Local/SavedTrip.swift + + + + + + Task 1: Create SortOrderProvider unit tests + SportsTimeTests/SortOrderProviderTests.swift + +Create a new test file `SortOrderProviderTests.swift` with tests for all SortOrderProvider methods. + +Test cases to include: + +**initialSortOrder tests:** +- `test_initialSortOrder_midnight_returns100`: 00:00 -> 100.0 +- `test_initialSortOrder_noon_returns820`: 12:00 -> 100 + 720 = 820.0 +- `test_initialSortOrder_7pm_returns1240`: 19:00 -> 100 + 1140 = 1240.0 +- `test_initialSortOrder_1159pm_returns1539`: 23:59 -> 100 + 1439 = 1539.0 + +**sortOrderBetween tests:** +- `test_sortOrderBetween_integers_returnsMidpoint`: (1.0, 2.0) -> 1.5 +- `test_sortOrderBetween_negativeAndPositive_returnsMidpoint`: (-1.0, 1.0) -> 0.0 +- `test_sortOrderBetween_fractionals_returnsMidpoint`: (1.5, 1.75) -> 1.625 + +**sortOrderBefore tests:** +- `test_sortOrderBefore_positive_returnsLower`: 1.0 -> 0.0 +- `test_sortOrderBefore_negative_returnsLower`: -1.0 -> -2.0 + +**sortOrderAfter tests:** +- `test_sortOrderAfter_positive_returnsHigher`: 1.0 -> 2.0 +- `test_sortOrderAfter_zero_returnsOne`: 0.0 -> 1.0 + +**needsNormalization tests:** +- `test_needsNormalization_wellSpaced_returnsFalse`: items with gaps > 1e-10 +- `test_needsNormalization_tinyGap_returnsTrue`: items with gap < 1e-10 +- `test_needsNormalization_empty_returnsFalse`: empty array +- `test_needsNormalization_singleItem_returnsFalse`: one item + +**normalize tests:** +- `test_normalize_reassignsIntegerSpacing`: after normalize, sortOrders are 1.0, 2.0, 3.0... +- `test_normalize_preservesOrder`: relative order unchanged after normalize + +Use `@testable import SportsTime` at top. + + +Tests compile and pass: +```bash +xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/SortOrderProviderTests test 2>&1 | grep -E "(Test Case|passed|failed)" +``` + + SortOrderProviderTests.swift exists with 16+ test cases, all tests pass + + + + Task 2: Create persistence integration tests + SportsTimeTests/SemanticPositionPersistenceTests.swift + +Create a new test file `SemanticPositionPersistenceTests.swift` with integration tests for semantic position persistence. + +These tests verify PERS-01, PERS-02, PERS-03 requirements. + +Test cases to include: + +**Position persistence (PERS-01):** +- `test_itineraryItem_positionSurvivesEncodeDecode`: Create ItineraryItem with specific day/sortOrder, encode to JSON, decode, verify day and sortOrder match exactly +- `test_localItineraryItem_positionSurvivesSwiftData`: Create LocalItineraryItem, save to SwiftData ModelContext, fetch back, verify day and sortOrder match + +**Semantic-only state (PERS-02):** +- `test_itineraryItem_allPositionPropertiesAreCodable`: Verify ItineraryItem.day and .sortOrder are included in Codable output (not transient) + +**Midpoint insertion (PERS-03):** +- `test_midpointInsertion_50Times_maintainsPrecision`: Insert 50 times between adjacent items, verify all sortOrders are distinct +- `test_midpointInsertion_producesCorrectValue`: Insert between sortOrder 1.0 and 2.0, verify result is 1.5 + +**Day property updates (DATA-02, DATA-05):** +- `test_travelItem_dayCanBeUpdated`: Create travel item with day=1, update to day=3, verify day property changed +- `test_item_belongsToExactlyOneDay`: Verify item.day is a single Int, not optional or array + +**Game immutability (DATA-03):** +- `test_gameItem_sortOrderDerivedFromTime`: Create game item for 7pm game, verify sortOrder is ~1240.0 (100 + 19*60) + +Use in-memory SwiftData ModelContainer for tests: +```swift +let config = ModelConfiguration(isStoredInMemoryOnly: true) +let container = try ModelContainer(for: LocalItineraryItem.self, configurations: config) +``` + +Import XCTest, SwiftData, and `@testable import SportsTime`. + + +Tests compile and pass: +```bash +xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/SemanticPositionPersistenceTests test 2>&1 | grep -E "(Test Case|passed|failed)" +``` + + SemanticPositionPersistenceTests.swift exists with 8+ test cases, all tests pass + + + + Task 3: Run full test suite to verify no regressions + + +Run the complete test suite to verify: +1. All new tests pass +2. No existing tests broken by new code +3. Build and test cycle completes successfully + +If any tests fail, investigate and fix before completing the plan. + + +```bash +xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test 2>&1 | tail -30 +``` +Look for "** TEST SUCCEEDED **" at the end. + + Full test suite passes with no failures, including all new and existing tests + + + + + +1. SortOrderProviderTests.swift exists with 16+ test methods covering all SortOrderProvider functions +2. SemanticPositionPersistenceTests.swift exists with 8+ test methods covering persistence requirements +3. All tests pass when run individually and as part of full suite +4. Tests verify the success criteria from ROADMAP.md Phase 1: + - Position survives reload (tested via encode/decode and SwiftData) + - Travel day update works (tested via day property mutation) + - Midpoint insertion works (tested via 50-iteration precision test) + - Games use time-based sortOrder (tested via initialSortOrder) + + + +- 24+ new test cases across 2 test files +- All tests pass +- Tests directly verify Phase 1 requirements DATA-01 through DATA-05 and PERS-01 through PERS-03 +- No regression in existing tests + + + +After completion, create `.planning/phases/01-semantic-position-model/01-02-SUMMARY.md` +