docs(03): create phase 3 visual flattening plans
Phase 03: Visual Flattening - 2 plans in 2 waves - Plan 01: Create ItineraryFlattener utility, refactor reloadData() - Plan 02: Add determinism tests for flattening behavior - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,8 +45,8 @@ Plans:
|
|||||||
**Plans:** 2 plans
|
**Plans:** 2 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 02-01-PLAN.md - Migrate XCTest constraint tests to Swift Testing
|
- [x] 02-01-PLAN.md - Migrate XCTest constraint tests to Swift Testing
|
||||||
- [ ] 02-02-PLAN.md - Add edge case tests and document constraint API
|
- [x] 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)
|
||||||
@@ -68,6 +68,12 @@ Plans:
|
|||||||
|
|
||||||
**Dependencies:** Phase 2 (constraints inform what items exist per day)
|
**Dependencies:** Phase 2 (constraints inform what items exist per day)
|
||||||
|
|
||||||
|
**Plans:** 2 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [ ] 03-01-PLAN.md - Create ItineraryFlattener utility and refactor reloadData()
|
||||||
|
- [ ] 03-02-PLAN.md - Add determinism tests for flattening behavior
|
||||||
|
|
||||||
**Requirements:**
|
**Requirements:**
|
||||||
- FLAT-01: Visual flattening sorts by sortOrder within each day
|
- FLAT-01: Visual flattening sorts by sortOrder within each day
|
||||||
- FLAT-02: Flattening is deterministic and stateless
|
- FLAT-02: Flattening is deterministic and stateless
|
||||||
@@ -109,12 +115,12 @@ 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 | Planned | 4 | 0 |
|
| 2 - Constraint Validation | Complete | 4 | 4 |
|
||||||
| 3 - Visual Flattening | Not Started | 3 | 0 |
|
| 3 - Visual Flattening | In Progress | 3 | 0 |
|
||||||
| 4 - Drag Interaction | Not Started | 8 | 0 |
|
| 4 - Drag Interaction | Not Started | 8 | 0 |
|
||||||
|
|
||||||
**Total:** 8/23 requirements completed
|
**Total:** 12/23 requirements completed
|
||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-01-18*
|
*Roadmap created: 2026-01-18*
|
||||||
|
|||||||
180
.planning/phases/03-visual-flattening/03-01-PLAN.md
Normal file
180
.planning/phases/03-visual-flattening/03-01-PLAN.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
phase: 03-visual-flattening
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- SportsTime/Core/Models/Domain/ItineraryFlattener.swift
|
||||||
|
- SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Items with lower sortOrder appear before items with higher sortOrder within each day"
|
||||||
|
- "Items with sortOrder < 0 appear before games"
|
||||||
|
- "Items with sortOrder >= 0 appear after or between games"
|
||||||
|
- "Day headers always appear first in their day's section"
|
||||||
|
artifacts:
|
||||||
|
- path: "SportsTime/Core/Models/Domain/ItineraryFlattener.swift"
|
||||||
|
provides: "Pure flatten function"
|
||||||
|
exports: ["flatten(days:itineraryItems:gamesByDay:)"]
|
||||||
|
- path: "SportsTime/Features/Trip/Views/ItineraryTableViewController.swift"
|
||||||
|
provides: "Refactored reloadData() using ItineraryFlattener"
|
||||||
|
contains: "ItineraryFlattener.flatten"
|
||||||
|
key_links:
|
||||||
|
- from: "ItineraryTableViewController.reloadData()"
|
||||||
|
to: "ItineraryFlattener.flatten()"
|
||||||
|
via: "function call"
|
||||||
|
pattern: "ItineraryFlattener\\.flatten"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create a pure ItineraryFlattener utility that transforms hierarchical day data into display rows sorted by sortOrder.
|
||||||
|
|
||||||
|
Purpose: Replace the bucket-based flattening (beforeGames/afterGames arrays) with pure sortOrder sorting, ensuring deterministic display order.
|
||||||
|
|
||||||
|
Output: New `ItineraryFlattener.swift` file and refactored `reloadData()` method using it.
|
||||||
|
</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/03-visual-flattening/03-RESEARCH.md
|
||||||
|
@.planning/phases/03-visual-flattening/03-CONTEXT.md
|
||||||
|
@SportsTime/Core/Models/Domain/SortOrderProvider.swift
|
||||||
|
@SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
|
||||||
|
@SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create ItineraryFlattener utility</name>
|
||||||
|
<files>SportsTime/Core/Models/Domain/ItineraryFlattener.swift</files>
|
||||||
|
<action>
|
||||||
|
Create a new `ItineraryFlattener.swift` file with a pure flatten function.
|
||||||
|
|
||||||
|
The function signature:
|
||||||
|
```swift
|
||||||
|
enum ItineraryFlattener {
|
||||||
|
static func flatten(
|
||||||
|
days: [ItineraryDayData],
|
||||||
|
itineraryItems: [ItineraryItem],
|
||||||
|
gamesByDay: [Int: [RichGame]]
|
||||||
|
) -> [ItineraryRowItem]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation logic:
|
||||||
|
1. For each day in order:
|
||||||
|
a. Add `.dayHeader` row (always first for each day)
|
||||||
|
b. Collect all positionable items with their sortOrder:
|
||||||
|
- Games: use `SortOrderProvider.initialSortOrder(forGameTime:)` for first game's time
|
||||||
|
- Custom items: use `item.sortOrder` directly
|
||||||
|
- Travel items: look up sortOrder from `itineraryItems` array by matching city names
|
||||||
|
c. Sort collected items by sortOrder (ascending)
|
||||||
|
d. Append sorted items to result
|
||||||
|
|
||||||
|
Key decisions (from CONTEXT.md):
|
||||||
|
- sortOrder < 0 appears BEFORE games (games have sortOrder 100-1540)
|
||||||
|
- sortOrder >= 0 appears AFTER/BETWEEN games
|
||||||
|
- Day header is NOT a positioned item - always first
|
||||||
|
|
||||||
|
Do NOT:
|
||||||
|
- Split into beforeGames/afterGames buckets
|
||||||
|
- Use hardcoded order assumptions
|
||||||
|
- Special-case travel with `travelBefore` property
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Build succeeds: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -5`
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
`ItineraryFlattener.swift` exists with `flatten()` function that:
|
||||||
|
- Sorts all items within a day by sortOrder
|
||||||
|
- Day headers appear first in each day
|
||||||
|
- Function is pure (no side effects, no instance state)
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Refactor reloadData() to use ItineraryFlattener</name>
|
||||||
|
<files>SportsTime/Features/Trip/Views/ItineraryTableViewController.swift</files>
|
||||||
|
<action>
|
||||||
|
Refactor the `reloadData()` method (currently lines 484-545) to use the new `ItineraryFlattener.flatten()` function.
|
||||||
|
|
||||||
|
Current flow to replace:
|
||||||
|
```swift
|
||||||
|
// OLD: Bucket-based approach
|
||||||
|
var beforeGames: [ItineraryRowItem] = []
|
||||||
|
var afterGames: [ItineraryRowItem] = []
|
||||||
|
for row in day.items {
|
||||||
|
if sortOrder < 0 { beforeGames.append(row) }
|
||||||
|
else { afterGames.append(row) }
|
||||||
|
}
|
||||||
|
flatItems.append(contentsOf: beforeGames)
|
||||||
|
flatItems.append(.games(...))
|
||||||
|
flatItems.append(contentsOf: afterGames)
|
||||||
|
```
|
||||||
|
|
||||||
|
New flow:
|
||||||
|
```swift
|
||||||
|
// NEW: Pure sortOrder-based approach
|
||||||
|
let gamesByDay = Dictionary(grouping: days.flatMap { ($0.games, $0.dayNumber) }) { ... }
|
||||||
|
flatItems = ItineraryFlattener.flatten(
|
||||||
|
days: days,
|
||||||
|
itineraryItems: allItineraryItems,
|
||||||
|
gamesByDay: gamesByDay
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The refactored method should:
|
||||||
|
1. Build `gamesByDay` dictionary: `[Int: [RichGame]]`
|
||||||
|
2. Call `ItineraryFlattener.flatten()` to get `flatItems`
|
||||||
|
3. Reload table view
|
||||||
|
|
||||||
|
Preserve:
|
||||||
|
- `self.travelValidRanges = travelValidRanges`
|
||||||
|
- `self.allItineraryItems = itineraryItems`
|
||||||
|
- `self.tripDayCount = days.count`
|
||||||
|
- `self.constraints = ItineraryConstraints(...)` rebuild
|
||||||
|
- `tableView.reloadData()` call at end
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- The manual flattening loop with beforeGames/afterGames buckets
|
||||||
|
- Any remaining `travelBefore` references in the flatten logic
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Build succeeds: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -5`
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
`reloadData()` method:
|
||||||
|
- Uses `ItineraryFlattener.flatten()` instead of manual loop
|
||||||
|
- No longer contains beforeGames/afterGames bucket logic
|
||||||
|
- Still updates travelValidRanges, allItineraryItems, tripDayCount, constraints
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Build succeeds with no errors
|
||||||
|
2. `ItineraryFlattener.swift` exists in `SportsTime/Core/Models/Domain/`
|
||||||
|
3. `reloadData()` in `ItineraryTableViewController.swift` calls `ItineraryFlattener.flatten()`
|
||||||
|
4. No `beforeGames` or `afterGames` variables remain in `reloadData()`
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- FLAT-01: Items sorted by sortOrder within each day (implemented in ItineraryFlattener)
|
||||||
|
- FLAT-03: sortOrder < 0 before games, >= 0 after (handled by natural sort + game sortOrder range 100-1540)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/03-visual-flattening/03-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
183
.planning/phases/03-visual-flattening/03-02-PLAN.md
Normal file
183
.planning/phases/03-visual-flattening/03-02-PLAN.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
---
|
||||||
|
phase: 03-visual-flattening
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["03-01"]
|
||||||
|
files_modified:
|
||||||
|
- SportsTimeTests/ItineraryFlattenerTests.swift
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Same semantic state produces identical row order on repeated flattening"
|
||||||
|
- "Item with sortOrder -1.0 appears before all games in that day"
|
||||||
|
- "Reordering an item and re-flattening preserves the new order"
|
||||||
|
artifacts:
|
||||||
|
- path: "SportsTimeTests/ItineraryFlattenerTests.swift"
|
||||||
|
provides: "Determinism and ordering tests"
|
||||||
|
min_lines: 80
|
||||||
|
key_links:
|
||||||
|
- from: "ItineraryFlattenerTests"
|
||||||
|
to: "ItineraryFlattener.flatten()"
|
||||||
|
via: "test assertions"
|
||||||
|
pattern: "ItineraryFlattener\\.flatten"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Add tests verifying ItineraryFlattener produces deterministic, sortOrder-based output.
|
||||||
|
|
||||||
|
Purpose: Ensure the three success criteria from ROADMAP.md are verifiable through automated tests.
|
||||||
|
|
||||||
|
Output: New test file `ItineraryFlattenerTests.swift` with tests covering all success criteria.
|
||||||
|
</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/03-visual-flattening/03-RESEARCH.md
|
||||||
|
@.planning/phases/03-visual-flattening/03-01-SUMMARY.md
|
||||||
|
@SportsTime/Core/Models/Domain/ItineraryFlattener.swift
|
||||||
|
@SportsTime/Core/Models/Domain/SortOrderProvider.swift
|
||||||
|
@SportsTimeTests/ItineraryConstraintsTests.swift
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create ItineraryFlattenerTests with determinism tests</name>
|
||||||
|
<files>SportsTimeTests/ItineraryFlattenerTests.swift</files>
|
||||||
|
<action>
|
||||||
|
Create a new test file `ItineraryFlattenerTests.swift` using Swift Testing (`@Suite`, `@Test`).
|
||||||
|
|
||||||
|
Test suite structure:
|
||||||
|
```swift
|
||||||
|
import Testing
|
||||||
|
@testable import SportsTime
|
||||||
|
|
||||||
|
@Suite("ItineraryFlattener")
|
||||||
|
struct ItineraryFlattenerTests {
|
||||||
|
// Test helper to create mock ItineraryDayData
|
||||||
|
// Test helper to create mock RichGame
|
||||||
|
// Test helper to create mock ItineraryItem
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Required tests (map directly to ROADMAP success criteria):
|
||||||
|
|
||||||
|
1. **success_negativeSortOrderAppearsBeforeGames** (Success Criterion 1)
|
||||||
|
- Create item with sortOrder = -1.0
|
||||||
|
- Create games with sortOrder ~820 (noon game)
|
||||||
|
- Flatten and verify custom item index < games index
|
||||||
|
- Assert: "Item with sortOrder -1.0 appears before all games in that day's section"
|
||||||
|
|
||||||
|
2. **success_sameStateProducesIdenticalOrder** (Success Criterion 2)
|
||||||
|
- Create fixed test data (2 days, games, travel, custom items)
|
||||||
|
- Flatten twice
|
||||||
|
- Compare resulting row IDs
|
||||||
|
- Assert: arrays are identical (not just equal length)
|
||||||
|
|
||||||
|
3. **success_reorderPreservesNewOrder** (Success Criterion 3)
|
||||||
|
- Create items with sortOrder 1.0, 2.0, 3.0
|
||||||
|
- Simulate reorder: change item from 1.0 to 2.5
|
||||||
|
- Flatten
|
||||||
|
- Verify new order is [2.0, 2.5, 3.0] not [1.0, 2.0, 3.0]
|
||||||
|
|
||||||
|
Additional tests for comprehensive coverage:
|
||||||
|
|
||||||
|
4. **flatten_dayHeaderAlwaysFirst**
|
||||||
|
- Verify first item for each day is `.dayHeader`
|
||||||
|
|
||||||
|
5. **flatten_sortsByDayThenSortOrder**
|
||||||
|
- Items on day 1 appear before items on day 2
|
||||||
|
- Within each day, items sorted by sortOrder
|
||||||
|
|
||||||
|
6. **flatten_gamesGetSortOrderFromTime**
|
||||||
|
- Game at 8:00 PM (sortOrder ~1180) appears after item with sortOrder 500
|
||||||
|
- Game at 1:00 PM (sortOrder ~880) appears before item with sortOrder 1000
|
||||||
|
|
||||||
|
7. **flatten_emptyDayHasOnlyHeader**
|
||||||
|
- Day with no games, no items produces only `.dayHeader` row
|
||||||
|
|
||||||
|
8. **flatten_multipleGamesOnSameDay**
|
||||||
|
- Day with 2 games: both included in single `.games` row
|
||||||
|
- Custom items around games sort correctly
|
||||||
|
|
||||||
|
Test helpers needed:
|
||||||
|
- `makeGame(id:, stadium:, dateTime:)` -> RichGame
|
||||||
|
- `makeItem(day:, sortOrder:, kind:)` -> ItineraryItem
|
||||||
|
- `makeDayData(dayNumber:, date:, games:, items:)` -> ItineraryDayData
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Run tests: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/ItineraryFlattenerTests test 2>&1 | grep -E "(Test Suite|passed|failed|error:)"`
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
`ItineraryFlattenerTests.swift` exists with:
|
||||||
|
- 3 success_* tests mapping to ROADMAP success criteria
|
||||||
|
- 5+ additional tests for comprehensive coverage
|
||||||
|
- All tests passing
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Verify existing behavior unchanged</name>
|
||||||
|
<files>SportsTimeTests/ItineraryFlattenerTests.swift</files>
|
||||||
|
<action>
|
||||||
|
Add integration-style tests that verify the refactored flattening produces expected behavior for realistic trip data.
|
||||||
|
|
||||||
|
Additional tests:
|
||||||
|
|
||||||
|
1. **flatten_travelWithNegativeSortOrderAppearsBeforeGames**
|
||||||
|
- Travel item with sortOrder = -5.0
|
||||||
|
- Should appear before games in same day
|
||||||
|
|
||||||
|
2. **flatten_travelWithPositiveSortOrderAppearsAfterGames**
|
||||||
|
- Travel item with sortOrder = 1500.0 (after noon game)
|
||||||
|
- Should appear after games in same day
|
||||||
|
|
||||||
|
3. **flatten_mixedItemTypes**
|
||||||
|
- Day with: travel (-5), custom (-1), games (820), custom (900), travel (1500)
|
||||||
|
- Verify exact order: header, travel, custom, games, custom, travel
|
||||||
|
|
||||||
|
4. **flatten_multiDayTrip**
|
||||||
|
- 3-day trip with different item configurations per day
|
||||||
|
- Verify each day's items are independent and sorted correctly
|
||||||
|
|
||||||
|
Run full test suite to ensure no regressions:
|
||||||
|
`xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Full test suite passes: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test 2>&1 | grep -E "(Test Suite|passed|failed)" | tail -10`
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- All ItineraryFlattenerTests passing (12+ tests)
|
||||||
|
- Full test suite passes (no regressions)
|
||||||
|
- Tests cover all three ROADMAP success criteria
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. All tests in `ItineraryFlattenerTests.swift` pass
|
||||||
|
2. Tests explicitly named `success_*` map to ROADMAP success criteria:
|
||||||
|
- success_negativeSortOrderAppearsBeforeGames -> Criterion 1
|
||||||
|
- success_sameStateProducesIdenticalOrder -> Criterion 2
|
||||||
|
- success_reorderPreservesNewOrder -> Criterion 3
|
||||||
|
3. Full test suite passes (no regressions from refactoring)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- FLAT-02: Flattening is deterministic and stateless (verified by success_sameStateProducesIdenticalOrder test)
|
||||||
|
- All three ROADMAP success criteria have corresponding tests
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/03-visual-flattening/03-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user