docs(01): create phase 1 plans

Phase 01: Semantic Position Model
- 2 plans in 2 waves
- Wave 1: SortOrderProvider + Trip day derivation (parallel-ready)
- Wave 2: Tests verifying persistence behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-18 13:43:35 -06:00
parent eae3a1d7f7
commit 6bce0481bd
3 changed files with 361 additions and 1 deletions

View File

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

View File

@@ -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"
---
<objective>
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.
</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/01-semantic-position-model/01-RESEARCH.md
# Existing source files
@SportsTime/Core/Models/Domain/ItineraryItem.swift
@SportsTime/Core/Models/Domain/Trip.swift
</context>
<tasks>
<task type="auto">
<name>Task 1: Create SortOrderProvider utility</name>
<files>SportsTime/Core/Models/Domain/SortOrderProvider.swift</files>
<action>
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.
</action>
<verify>
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
```
</verify>
<done>SortOrderProvider.swift exists with all 6 static methods, project builds without errors</done>
</task>
<task type="auto">
<name>Task 2: Add day derivation methods to Trip</name>
<files>SportsTime/Core/Models/Domain/Trip.swift</files>
<action>
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.
</action>
<verify>
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
```
</verify>
<done>Trip.swift has dayNumber(for:) and date(forDay:) methods, project builds without errors</done>
</task>
</tasks>
<verification>
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
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/01-semantic-position-model/01-01-SUMMARY.md`
</output>

View File

@@ -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"
---
<objective>
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.
</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/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
</context>
<tasks>
<task type="auto">
<name>Task 1: Create SortOrderProvider unit tests</name>
<files>SportsTimeTests/SortOrderProviderTests.swift</files>
<action>
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.
</action>
<verify>
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)"
```
</verify>
<done>SortOrderProviderTests.swift exists with 16+ test cases, all tests pass</done>
</task>
<task type="auto">
<name>Task 2: Create persistence integration tests</name>
<files>SportsTimeTests/SemanticPositionPersistenceTests.swift</files>
<action>
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`.
</action>
<verify>
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)"
```
</verify>
<done>SemanticPositionPersistenceTests.swift exists with 8+ test cases, all tests pass</done>
</task>
<task type="auto">
<name>Task 3: Run full test suite to verify no regressions</name>
<files></files>
<action>
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.
</action>
<verify>
```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.
</verify>
<done>Full test suite passes with no failures, including all new and existing tests</done>
</task>
</tasks>
<verification>
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)
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/01-semantic-position-model/01-02-SUMMARY.md`
</output>