trails/openspec/changes/gpx-parser-robustness/tasks.md
Ullrich Schäfer 99278fd305
feat(gpx): lenient point parsing + route (<rte>) support
Task groups 1–2 of gpx-parser-robustness. The parser trusted its input:
`parseFloat(attr ?? "0")` turned a missing lat/lon into a 0,0 Null Island
point (which passes range validation) and garbage into NaN that poisoned
distance and gain/loss totals; `<rte>`/`rtept` files (Garmin courses, many
exporters) parsed to zero track points and were rejected.

- `parsePoint`: skip a trkpt/rtept whose lat/lon is missing or non-finite
  (no more 0,0 default); a non-finite `<ele>` becomes `undefined` so it
  never leaks NaN into totals. Parsing stays parseFloat-lenient (trailing
  junk like `471.0m` still accepted), gated by Number.isFinite.
- Drop segments left with fewer than 2 points (render nothing / break
  distance math).
- Parse `<rte>` as track segments appended after `<trk>` segments, rtept
  handled identically — route-only files now import.

No GpxData shape change; well-formed files parse identically. Updated the
geom single-point test to the new drop-invariant.

Verified: gpx typecheck + lint clean, 76/76 tests pass, journal + planner
typecheck unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:06:35 +02:00

2.5 KiB

1. Parser lenience

  • 1.1 Add a finite-number coordinate gate in packages/gpx/src/parse.ts: skip trkpt/rtept with missing or non-finite lat/lon; treat non-finite <ele> as undefined
  • 1.2 Drop segments with fewer than 2 surviving points
  • 1.3 Unit tests: missing-lat point skipped, garbage lat skipped with finite stats, garbage ele → undefined with finite gain/loss, single-point segment dropped, well-formed files byte-identical output to before

2. Route (<rte>) support

  • 2.1 Parse <rte>/rtept as track segments (appended after <trk> segments), reusing the same point parsing and lenience
  • 2.2 Unit tests: route-only file yields one segment and passes validateGpx; mixed trk+rte ordering; rtept with ele/time preserved

3. Timestamp repair

  • 3.1 Create packages/gpx/src/timestamp-repair.ts: per-segment pass — >50% invalid → drop all; else linear interpolation between valid neighbors with leading/trailing clamp; output ISO 8601 strings
  • 3.2 Wire the repair pass into parseTracks output in parse.ts
  • 3.3 Unit tests (timestamp-repair.test.ts): sparse invalid interpolated, >50% invalid dropped (and movingTime returns null), leading/trailing clamp, all-valid untouched, no-timestamps untouched

4. Metadata fallback

  • 4.1 Fall back to first <trk>/<rte> <name>/<desc> when <metadata> lacks them; merge distinct <cmt> into description with blank-line separator, dedup identical
  • 4.2 Unit tests: track-name fallback, metadata precedence, cmt merge, identical cmt deduped

5. Fixture corpus

  • 5.1 Create packages/gpx/fixtures/ with purpose-named files: route-only.gpx, missing-coords.gpx, garbage-ele.gpx, timestamps-partial.gpx, timestamps-mostly-invalid.gpx, multi-track.gpx, unicode-name.gpx, namespaced-extensions.gpx (Garmin/OsmAnd-style extensions)
  • 5.2 Add trimmed real-world exports for supported integrations (Komoot, Wahoo) with any personal data scrubbed
  • 5.3 Extend parse-node.test.ts to load every fixture from disk and assert expected repaired output (a fixture table test so new files are picked up automatically or fail loudly)

6. Verification

  • 6.1 Typecheck + run full gpx package suite; confirm GpxData shape unchanged and journal/planner compile without changes
  • 6.2 Manual check: import a route-only GPX and a partially-broken GPX through the journal import flow; confirm both save with sane stats
  • 6.3 Run pnpm typecheck && pnpm lint && pnpm test and the e2e suites