Break v3 into 16 tickets

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>
This commit is contained in:
2026-08-17 10:41:35 -05:00
parent 82754de9c4
commit 342a8f8382
19 changed files with 982 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
# V3-14 — GPX interoperability
**Phase** Quality · **Depends on** V3-01 for `<type>` · **Size** S · **Status** Not started
## Goal
Confirm an exported ride actually imports into Strava, Garmin Connect and Google Earth —
and add the activity type so it lands as the right kind of activity.
## Context
Export is well tested: 15 tests, parsed with a real XML parser, and **byte-identical to the
Kotlin original** under the parity harness. But every one of those tests proves *structural
validity*, and structural validity does not mean a consumer accepts the file. That gap has
been open since v2.
## Design
Two parts.
**Verification** — export a real ride and import it into each of Strava, Garmin Connect and
Google Earth. Record what each does with pauses, elevation and timestamps. Pauses are the
interesting case: `<trkseg>` per segment is the correct GPX representation, but consumers
vary in whether they honour it.
**`<type>` on `<trk>`** — Strava and Garmin read it to decide the activity. Without it a
bicycle ride may import as a run. Needs V3-01's activity, mapped to each consumer's
vocabulary (Strava uses `ride`, `run`, and so on).
## Implementation
1. Add `<type>` to the `<trk>` element, from the trip's activity
2. Map the internal enum to GPX conventions; document the mapping in the code
3. Export a real multi-segment ride and import it into all three consumers
4. Write the findings into this file, including anything that surprises
## Acceptance criteria
- [ ] A real ride imports into Strava with the right activity type
- [ ] It imports into Garmin Connect
- [ ] It opens in Google Earth with the path in the right place
- [ ] Pause behaviour in each consumer is documented, whatever it turns out to be
- [ ] Existing export tests still pass, including the byte-identical parity check —
**this one will need updating, since `<type>` changes the output**
## Tests
- `<type>` present and correct per activity
- Absent, not empty, when the activity is `other`
- **The parity harness will now differ from Kotlin here. That is expected and correct —
update its expectation and note why, rather than dropping the check.**
## Risks
Silently breaking the parity harness by changing export output. Update it deliberately.
## Out of scope
GPX import into Rippr. FIT and TCX formats.