From e8ae80e02bb94379316f4eaf1e521a80b179c1f5 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sun, 18 Jan 2026 15:49:49 -0600 Subject: [PATCH] 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 --- .planning/ROADMAP.md | 18 +- .../phases/03-visual-flattening/03-01-PLAN.md | 180 +++++++++++++++++ .../phases/03-visual-flattening/03-02-PLAN.md | 183 ++++++++++++++++++ 3 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 .planning/phases/03-visual-flattening/03-01-PLAN.md create mode 100644 .planning/phases/03-visual-flattening/03-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 302ff17..7fcf942 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -45,8 +45,8 @@ Plans: **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 +- [x] 02-01-PLAN.md - Migrate XCTest constraint tests to Swift Testing +- [x] 02-02-PLAN.md - Add edge case tests and document constraint API **Requirements:** - CONS-01: Games cannot be moved (fixed by schedule) @@ -68,6 +68,12 @@ Plans: **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:** - FLAT-01: Visual flattening sorts by sortOrder within each day - FLAT-02: Flattening is deterministic and stateless @@ -109,12 +115,12 @@ Plans: | Phase | Status | Requirements | Completed | |-------|--------|--------------|-----------| -| 1 - Semantic Position Model | ✓ Complete | 8 | 8 | -| 2 - Constraint Validation | Planned | 4 | 0 | -| 3 - Visual Flattening | Not Started | 3 | 0 | +| 1 - Semantic Position Model | Complete | 8 | 8 | +| 2 - Constraint Validation | Complete | 4 | 4 | +| 3 - Visual Flattening | In Progress | 3 | 0 | | 4 - Drag Interaction | Not Started | 8 | 0 | -**Total:** 8/23 requirements completed +**Total:** 12/23 requirements completed --- *Roadmap created: 2026-01-18* diff --git a/.planning/phases/03-visual-flattening/03-01-PLAN.md b/.planning/phases/03-visual-flattening/03-01-PLAN.md new file mode 100644 index 0000000..aef12ea --- /dev/null +++ b/.planning/phases/03-visual-flattening/03-01-PLAN.md @@ -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" +--- + + +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. + + + +@~/.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/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 + + + + + + Task 1: Create ItineraryFlattener utility + SportsTime/Core/Models/Domain/ItineraryFlattener.swift + +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 + + +Build succeeds: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -5` + + +`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) + + + + + Task 2: Refactor reloadData() to use ItineraryFlattener + SportsTime/Features/Trip/Views/ItineraryTableViewController.swift + +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 + + +Build succeeds: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -5` + + +`reloadData()` method: +- Uses `ItineraryFlattener.flatten()` instead of manual loop +- No longer contains beforeGames/afterGames bucket logic +- Still updates travelValidRanges, allItineraryItems, tripDayCount, constraints + + + + + + +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()` + + + +- 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) + + + +After completion, create `.planning/phases/03-visual-flattening/03-01-SUMMARY.md` + diff --git a/.planning/phases/03-visual-flattening/03-02-PLAN.md b/.planning/phases/03-visual-flattening/03-02-PLAN.md new file mode 100644 index 0000000..01f9440 --- /dev/null +++ b/.planning/phases/03-visual-flattening/03-02-PLAN.md @@ -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" +--- + + +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. + + + +@~/.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/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 + + + + + + Task 1: Create ItineraryFlattenerTests with determinism tests + SportsTimeTests/ItineraryFlattenerTests.swift + +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 + + +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:)"` + + +`ItineraryFlattenerTests.swift` exists with: +- 3 success_* tests mapping to ROADMAP success criteria +- 5+ additional tests for comprehensive coverage +- All tests passing + + + + + Task 2: Verify existing behavior unchanged + SportsTimeTests/ItineraryFlattenerTests.swift + +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` + + +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` + + +- All ItineraryFlattenerTests passing (12+ tests) +- Full test suite passes (no regressions) +- Tests cover all three ROADMAP success criteria + + + + + + +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) + + + +- FLAT-02: Flattening is deterministic and stateless (verified by success_sameStateProducesIdenticalOrder test) +- All three ROADMAP success criteria have corresponding tests + + + +After completion, create `.planning/phases/03-visual-flattening/03-02-SUMMARY.md` +