One file per feature, in the v2 shape that worked: goal, context, design, implementation, acceptance criteria, tests, risks, out of scope -- written before implementing so the risks are on paper rather than walked into. Three carry more weight than their size suggests. V3-01 ships the port's first real migration, and since the destructive fallback is gone, getting addColumn and a v1-database test right matters more than the feature. V3-08 forces a routing-engine decision with ongoing cost, so it sits behind a RoutingService interface mirroring what LocationSource did for GPS. V3-13 is not code at all -- it answers the three questions open since v2, and V3-15 may close unbuilt as a result, which is a legitimate outcome. Several tickets record constraints that are easy to lose: no activity picker in front of Start, because the founding premise is press-and-go with gloves; the Route-to-Trip foreign key must not cascade, or deleting an old plan deletes the ride; V3-14 will deliberately break the parity harness by adding GPX <type>, and that expectation should be updated rather than the check dropped; and V3-16 must not regress the explicit text colours that exist because of the black-on-black bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
2.2 KiB
Markdown
54 lines
2.2 KiB
Markdown
# V3-10 — Trip splitting
|
|
|
|
**Phase** Ride management · **Depends on** nothing · **Size** S · **Status** Not started
|
|
|
|
## Goal
|
|
Split one recorded ride into two at a chosen point. The natural counterpart to merge.
|
|
|
|
## Context
|
|
Merge exists and is well tested; split does not. The case is a rider who forgot to stop —
|
|
one "ride" that is really the trip out, lunch, and the trip home.
|
|
|
|
Merge already establishes the hard parts: re-parenting points and segments inside a
|
|
transaction, and recomputing aggregates rather than summing them.
|
|
|
|
## Design
|
|
Split at a **segment boundary** rather than an arbitrary point. Segments already mark where
|
|
the rider paused, which is exactly where a forgotten stop shows up — and it avoids
|
|
inventing a new boundary type or splitting a segment in half.
|
|
|
|
The original trip keeps the earlier segments; a new trip takes the later ones. Both get
|
|
aggregates recomputed from the points they actually own.
|
|
|
|
If a ride has only one segment there is nothing to split, and the UI should say so rather
|
|
than offering a dead control.
|
|
|
|
## Implementation
|
|
1. `TripRepository.splitTrip(tripId, atSegmentId)` inside a transaction:
|
|
create the new trip, re-parent segments and points from `atSegmentId` onward,
|
|
set `startedAt`/`endedAt` from the segments each trip now owns,
|
|
recompute aggregates for both
|
|
2. Trip detail: a split action listing segment boundaries with their times
|
|
3. Confirmation naming what the two resulting rides will be
|
|
|
|
## Acceptance criteria
|
|
- [ ] Splitting produces two trips whose point counts sum to the original
|
|
- [ ] Neither trip's distance includes the gap between them
|
|
- [ ] Both have plausible `startedAt`/`endedAt`
|
|
- [ ] Single-segment rides cannot be split, and the UI explains why
|
|
- [ ] Atomic — a failure part-way leaves the original intact
|
|
|
|
## Tests
|
|
- Point counts sum; no points orphaned
|
|
- Distance of the parts is less than the original by roughly the gap
|
|
- Split then merge returns to the original aggregates — a good round-trip property
|
|
- Rejects a single-segment trip
|
|
- Atomicity under a forced mid-transaction failure
|
|
|
|
## Risks
|
|
Getting `startedAt`/`endedAt` from the wrong source. Derive them from the segments each
|
|
trip owns, not from the original trip.
|
|
|
|
## Out of scope
|
|
Splitting mid-segment.
|