Adds Activity (motorcycle/bicycle/scooter/skateboard/running/walking/other) as
a column on Trip, and threads it through as real behaviour rather than a label:
ActivityProfile (domain/activity_profile.dart) carries a noise floor, accuracy
gate, histogram bucket width, and elevation smoothing window/threshold per
activity, consumed by sanitizeSpeedKmh, isUsableFix, computeSummary, Accumulator
and speedHistogram. The motorcycle profile reproduces the exact constants the
app shipped with before this existed, and a test asserts they never drift apart.
This is the port's first real migration: schemaVersion 1 -> 2,
m.addColumn(trips, trips.activity) with a motorcycle default so every existing
row survives unmodified. Proven with a hand-built v1 SQLite file (raw sqlite3,
not drift_dev's schema tooling) that a real database with real rides upgrades
and keeps every trip, segment and point.
No picker in front of Start: a new trip defaults to whichever activity was most
recently used, derived live from the trips table rather than duplicated into a
Config field. Editable afterwards on trip detail, which recomputes aggregates
under the new profile immediately -- a walking pace that reads as noise under a
motorcycle's floor reads as real movement once the activity is corrected, and
there's a test proving exactly that transition.
The widget-test pass for the activity-picker sheet caught a real overflow bug:
seven options overflowed a Column-based bottom sheet the same way the record
screen once did. Fixed with a scrollable ListView + isScrollControlled, same
shape as that earlier fix.
188 tests (171 -> 188): 2 migration, 5 ActivityProfile, 3 profile-threading
proofs in computeSummary, 5 repository, 2 widget. Analyze clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All lifecycle transitions inside Drift transactions, each idempotent: start
(adopting an active trip rather than duplicating it), pause, resume, complete,
discard, rename, delete, merge, and the authoritative aggregate recomputation.
Room.withTransaction maps onto Drift's transaction() almost exactly, so this was
the most mechanical port so far. 26 tests, green first run.
Merge keeps its hard-won properties: segments are never joined, so the boundary
between two merged rides stays a segment boundary like a pause, and aggregates
are recomputed rather than summed because distance is not additive across the
gap. The regression test puts the two rides a degree of latitude apart and
asserts the ~111km gap never reaches distanceM.
Dropped a mergeTableUpdates helper before committing -- Drift's update() already
notifies dependent streams, so it was scaffolding that earned nothing.
TripRepositoryTest, MergeTest and SchemaTest were 666 lines of instrumented
tests needing a booted emulator. All three now run as unit tests in ~2 seconds.
Phase 2 done: 121 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>