createRoute, updateRoute, createActivity, createRouteFromActivity, and
the demo-bot each re-implemented the same choreography after
validateGpx: flatten tracks into [lon, lat] coords for writeGeom and
derive distance / elevation / dayBreaks / description / start time.
The stat derivation lived in a private computeRouteStats in
routes.server.ts that activities couldn't reach, so the two sides had
drifted (activities re-derived inline, with its own start-time logic).
processGpx() in gpx-save.server.ts now owns the whole step: parse +
validate (GpxValidationError as before), coords extraction, and stat
derivation, returning the parsed GpxData so nothing re-parses.
Callers keep their own precedence rules between derived and
caller-supplied stats — routes let explicit input win wholesale, the
activities importer prefers GPX distance unless it is zero. Extends
ADR-0006: the gpx-save module remains the only place that understands
GPX-to-database derivation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eliminates the silent-failure pattern where a route/activity row could
be committed with gpx IS NOT NULL but geom IS NULL if the PostGIS write
failed after the row insert.
- New gpx-save.server.ts owns all GPX validation (GpxValidationError,
validateGpx) and PostGIS geometry writes (writeGeom, tx-aware)
- createRoute, updateRoute, createActivity, createRouteFromActivity all
wrapped in db.transaction() covering row + geom + version snapshot
- demo-bot uses createRoute/createActivity instead of raw inserts;
errors propagate loudly
- Callback endpoint returns 400 for GpxValidationError instead of 401
- ADR-0006 documents the invariant for future explorers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>