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:
@@ -12,6 +12,12 @@ Build a drag-and-drop itinerary editor for SportsTime using UITableView bridged
|
|||||||
|
|
||||||
**Dependencies:** None (foundation phase)
|
**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:**
|
**Requirements:**
|
||||||
- DATA-01: All movable items have semantic position `(day: Int, sortOrder: Double)`
|
- DATA-01: All movable items have semantic position `(day: Int, sortOrder: Double)`
|
||||||
- DATA-02: Travel segments are positioned items with their own sortOrder
|
- 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 |
|
| 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 |
|
| 2 - Constraint Validation | Not Started | 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 |
|
||||||
|
|||||||
155
.planning/phases/01-semantic-position-model/01-01-PLAN.md
Normal file
155
.planning/phases/01-semantic-position-model/01-01-PLAN.md
Normal 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>
|
||||||
199
.planning/phases/01-semantic-position-model/01-02-PLAN.md
Normal file
199
.planning/phases/01-semantic-position-model/01-02-PLAN.md
Normal 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>
|
||||||
Reference in New Issue
Block a user