M2-07: The track file — write it, and survive a crash #48

Merged
wk merged 1 commit from m2-07-track-file into main 2026-08-09 21:29:56 -04:00
Member

Closes part one of #34.

<base>_track.gpx beside the memo: the route written in roughly one-minute parts as the recording runs, joined at stop. The launch sweep that recovers a route a crash interrupted is a second change against the same issue — this one does not sweep, and the docs say so rather than promising it.

The split is here rather than at the writer because the other seam would have shipped a build that persists routes to Documents while the Settings screen still says "nothing is written to a file". That is the one state this feature may not have.

The format

GPX 1.1, recorded in stash-docs (m2-07-track-gpx-decision). Absolute UTC time and altitude are native; speed and course are Garmin's TrackPointExtension v2, whose units are already Core Location's; horizontal accuracy is <stash:hacc> in metres.

It is deliberately not <hdop>. HDOP is a dimensionless satellite-geometry factor and this is a radius in metres — different quantities, and a reader has no way to tell that a number under that name means something else. A rendered sample validates against both published schemas under xmllint, confirmed meaningful by breaking the extension deliberately and watching validation fail.

Ten seconds, not a minute

The plan proposed one write per closed part and the reasoning was wrong: the part at risk is the end of the walk, so writing only at the boundary loses everything since the last one — and a force-quit before the first boundary leaves no track at all, which this issue's own minute-20 checklist item would never have caught. The open part is rewritten atomically every ten seconds instead.

Withdrawing mid-recording removes the parts already on disk, oldest first. A withdrawal is several deletions and the process can die between them, so taking part 001 first means an interrupted withdrawal leaves a run that does not begin at one — a state the writer never produces, so recovery can refuse it rather than mistake it for the remains of an ordinary crash. No tombstone file.

LocationCollection and the sentence saying no route was saved are deleted, which is what that constant existed for.

What Codex caught that was a real defect

  • Adding a .track case to the parts-folder ownership rule as designed would have had track assembly delete transcript parts — the predicate treated every transcript part as owned before it looked at ownership.
  • Assembly filtered the writer's report of unwritten parts against what was on disk. The open part is checkpointed as it fills, so a part whose final write failed is still there under the right name: the filter erased the report, published a truncated route as complete, and deleted the folder holding the only remaining copy.
  • The "strict" reader only checked well-formedness, so any XML under an exact part name counted as an empty part and the folder was then cleared.
  • Assembly dated the track from the earliest fix rather than the recording start every part carries. The two differ by the time to first fix, and cue alignment is derived by subtracting it.
  • The size cap was checked on a path and the read happened on a path, which the Files app can race. One open descriptor now, with the kind asked of that descriptor.

Full dispositions in .agent/m2-07-track-file/SUMMARY.md.

Validated, and not

507 tests pass, make lint clean, make run launches.

Not tested on a device. The needs-hardware label is what this is waiting on; device steps are in SUMMARY.md.

No test covers AudioRecorder's three call sites into the collector. Deleting locationCollector.begin, end or finish leaves the whole suite green while the app respectively collects no route, keeps location running after a recording stops, or never writes the file. The recorder needs an audio engine StashTests deliberately has none of, and the rules that could be lifted out of it already have been. The device steps stand in, which is weaker than a test.

Closes part one of #34. `<base>_track.gpx` beside the memo: the route written in roughly one-minute parts as the recording runs, joined at stop. **The launch sweep that recovers a route a crash interrupted is a second change against the same issue** — this one does not sweep, and the docs say so rather than promising it. The split is here rather than at the writer because the other seam would have shipped a build that persists routes to Documents while the Settings screen still says "nothing is written to a file". That is the one state this feature may not have. ## The format GPX 1.1, recorded in `stash-docs` (`m2-07-track-gpx-decision`). Absolute UTC time and altitude are native; speed and course are Garmin's `TrackPointExtension` v2, whose units are already Core Location's; horizontal accuracy is `<stash:hacc>` in metres. It is deliberately **not** `<hdop>`. HDOP is a dimensionless satellite-geometry factor and this is a radius in metres — different quantities, and a reader has no way to tell that a number under that name means something else. A rendered sample validates against both published schemas under `xmllint`, confirmed meaningful by breaking the extension deliberately and watching validation fail. ## Ten seconds, not a minute The plan proposed one write per closed part and the reasoning was wrong: the part at risk is the *end* of the walk, so writing only at the boundary loses everything since the last one — and a force-quit before the first boundary leaves no track at all, which this issue's own minute-20 checklist item would never have caught. The open part is rewritten atomically every ten seconds instead. ## Consent Withdrawing mid-recording removes the parts already on disk, **oldest first**. A withdrawal is several deletions and the process can die between them, so taking part 001 first means an interrupted withdrawal leaves a run that does not begin at one — a state the writer never produces, so recovery can refuse it rather than mistake it for the remains of an ordinary crash. No tombstone file. `LocationCollection` and the sentence saying no route was saved are deleted, which is what that constant existed for. ## What Codex caught that was a real defect - Adding a `.track` case to the parts-folder ownership rule as designed would have had track assembly **delete transcript parts** — the predicate treated every transcript part as owned before it looked at ownership. - Assembly filtered the writer's report of unwritten parts against what was on disk. The open part is checkpointed as it fills, so a part whose final write failed is still there under the right name: the filter erased the report, published a truncated route as complete, and deleted the folder holding the only remaining copy. - The "strict" reader only checked well-formedness, so any XML under an exact part name counted as an empty part and the folder was then cleared. - Assembly dated the track from the earliest fix rather than the recording start every part carries. The two differ by the time to first fix, and cue alignment is derived by subtracting it. - The size cap was checked on a path and the read happened on a path, which the Files app can race. One open descriptor now, with the kind asked of that descriptor. Full dispositions in `.agent/m2-07-track-file/SUMMARY.md`. ## Validated, and not 507 tests pass, `make lint` clean, `make run` launches. **Not tested on a device.** The `needs-hardware` label is what this is waiting on; device steps are in `SUMMARY.md`. **No test covers `AudioRecorder`'s three call sites into the collector.** Deleting `locationCollector.begin`, `end` or `finish` leaves the whole suite green while the app respectively collects no route, keeps location running after a recording stops, or never writes the file. The recorder needs an audio engine `StashTests` deliberately has none of, and the rules that could be lifted out of it already have been. The device steps stand in, which is weaker than a test.
Writes `<base>_track.gpx` beside the memo: the route in roughly one-minute
parts as the recording runs, joined at stop.

GPX 1.1, decided in #34, with the layout recorded in stash-docs. Absolute UTC
time and altitude are native; speed and course are Garmin's TrackPointExtension
v2, whose units are already Core Location's; horizontal accuracy is
<stash:hacc> in metres and deliberately not <hdop>, which is a different
quantity. A rendered sample validates against both published schemas.

The open part is rewritten every ten seconds rather than only at its boundary,
because the open part is the end of the walk — a kill would otherwise take
everything since the last boundary, and one before the first boundary would
leave no track at all.

Withdrawing consent mid-recording removes the parts already on disk, oldest
first, so an interrupted removal leaves a run that cannot be mistaken for the
remains of an ordinary crash.

Assembly parses each part and renders the finished file in one pass, rather
than splicing XML as text. A missing or unreadable part becomes a segment
break, so a partial route cannot be read as a whole one.

LocationCollection and the sentence saying no route was saved are deleted,
which is what that constant existed for.

The launch sweep that recovers a route a crash interrupted is the second
change against this issue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wk merged commit 096f31365f into main 2026-08-09 21:29:56 -04:00
wk deleted branch m2-07-track-file 2026-08-09 21:29:57 -04:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios!48
No description provided.