list_design_systems reveals 3 distinct design systems in this Stitch project. The one requested (asset e73325c5365a4a56b93823236777e95a, screen ID asset-stub-assets_e73325c5365a4a56b93823236777e95a) is 'Modern Professional Dark' -- charcoal/navy, Professional Blue primary, Inter throughout, 8px rounding, tonal-layer depth with glow accents.
The earlier commit incorrectly saved get_project's top-level designTheme (the neon-green 'Rippr' HUD system) as design-system.md and called the export summary's description of a blue theme 'stale.' It wasn't -- confirmed by hex-matching every color in the downloaded screen HTML against the two candidate systems: all match Modern Professional Dark (#0a0a0c, #131313, #16161a, #4090fe...) exactly, none match the neon system. Renamed the neon file to design-system-rippr-neon.md (kept for reference, flagged as unused by these screens) and added the correct file.
Pulled via the Stitch MCP from project 8187200995279352789: the design system spec (neon-HUD dark theme -- Sora/Inter/JetBrains Mono, electric green primary, cyan secondary) plus HTML/screenshot for Map HUD, Plan Screen, Route Planning, and Rides History (all Dark Mode), and the project's own export summary.
Raw reference material only, not yet wired into the app. Flags a real discrepancy in README.md: the export summary describes a different ('Modern Professional Dark', blue) design system than the one actually attached to the project and used by every screen fetched -- treat design-system.md as current, export-summary.md as stale.
v4 was always just the server-dependent programme (group rides, accounts, paid backup) plus V3-17. Renaming it to ROADMAP since it's about to become the home for an incoming list of bugs and features that need triage ahead of the next v3-style ticket batch -- that triage takes priority over the existing server-dependent items, none of which are blocking.
Two build-blocking issues fixed to get a release APK building at all:
- flutter_local_notifications (V3-06) requires core library desugaring below API 33; enabled it and added the desugar_jdk_libs dependency.
- sentry_flutter 8.14.2's bundled Kotlin plugin declares languageVersion 1.6, incompatible with this project's Kotlin 2.4.0 toolchain. Bumped to sentry_flutter ^9.0.0 (resolved 9.27.0), which builds clean; left three copyWith deprecation infos from the v9 API as a follow-up, not blocking.
Signed with the debug keystore (release signingConfig still points at debug -- no release signing config exists yet, matches the existing TODO in build.gradle.kts). app-release.apk committed directly (68.7MB, force-added past .gitignore's build/ exclusion) per request, so it can be pushed once a remote exists.
Per request: V3-08's routing-engine direction is decided (self-hosted OSRM over a hosted API or the public demo instance) but standing one up is split into its own ticket, V3-17, since provisioning a server is different work from the app code that calls it. V3-08 and V3-09 are marked Deferred pending V3-17.
V3-17 also names a real tension worth flagging rather than quietly ignoring: this backlog's own organizing principle is 'v3 is everything buildable with no server,' and a self-hosted OSRM instance is a server -- so V3-08/V3-09 sit oddly under docs/v3/ once V3-17 exists. Left unresolved by design (a documentation call for later), noted in both V3-17 and BACKLOG.md's v4 section rather than silently renumbering tickets that are already cross-referenced throughout the docs.
Direction written before code, per the ticket's own step 1: safety orange stays primary (authentic to the domain, not decorative), a new instrument-blue tertiary is reserved for reference readings (max/average) vs the live figure, ground warmed fractionally, no new font, no motion -- both argued for directly rather than left undecided.
Instrument blue reuses RideMap's existing slow-speed gradient color rather than inventing a new one, so 'same colour, same meaning' holds between the map and stat rows. contrastRatio() implements WCAG 2.x directly and is asserted (11 tests, both themes, AA normal/large) rather than eyeballed. Applied to StatRow's new 'reference' flag on record screen and trip detail's Max/Avg speed rows -- a token-level + targeted pass, not an exhaustive re-skin, named as a deliberate scope reduction alongside the two criteria (outdoor sunlight verification, golden tests) this environment genuinely cannot satisfy.
Three pure modules: tile_math.dart (tilesForBounds/tilesAlongRoute, hard cap enforced by throwing), tile_cache.dart (FileTileCache with LRU eviction before any write that would exceed the cap), tile_downloader.dart (sequential, rate-limited, cooperatively cancellable, one bad tile doesn't abort the rest). CachedTileProvider wires the cache into flutter_map via a custom ImageProvider and now backs RideMap and RoutePlannerScreen's tile layers, so ordinary viewing write-throughs into the same capped cache.
RoutePlannerScreen gained a route-corridor download action (no rectangle-selection UI -- the ticket names the corridor as strictly better and V3-07 already exists to hang it off of), with a count/size confirmation before any request and a cancellable progress dialog. Fixed a real bug before shipping: a StatefulBuilder-based progress dialog would have started a new overlapping download subscription on every single progress tick; moved to a dedicated StatefulWidget that subscribes once in initState.
Marked partially done: aeroplane-mode verification on a real device is the ticket's own acceptance criterion and needs a phone this environment doesn't have.
shouldInitializeCrashReporting() and scrubExtra() are pure, fully unit-tested decision/scrubbing functions wired into sentry_flutter via beforeSend/beforeBreadcrumb. Config.crashReportingEnabled defaults off; the DSN is a compile-time --dart-define, not a preference. RecordingEngine gained an injected onUnexpectedStop callback, firing once when restoreAfterProcessDeath finds a trip still 'recording' at launch -- the process died without a user stop.
Marked partially done: the ticket's real acceptance criteria (a forced crash landing in a real Sentry project from a release build, inspecting a real payload) need a Sentry account and a release build this environment can't produce.
TripRepository.splitTrip mirrors mergeTrips' transaction shape: splits at a segment boundary, moves the target segment and everything after it to a new trip, recomputes both trips' aggregates from scratch. startedAt/endedAt derive from each trip's actual remaining segments, not copied from the pre-split row. Trip detail gained a split action with a segment-boundary picker and a naming confirmation; a single-segment ride explains why it can't split instead of offering a dead control.
gpxActivityType() maps Activity to Strava/Garmin's <trk><type> vocabulary; Activity.other omits the element rather than writing it empty. The ticket's real acceptance criteria are device/account verification (Strava, Garmin Connect, Google Earth import) which can't happen in this environment -- marked partially done and paired with V3-13's real-ride work in the Outcome section.
Route plans are their own entity (RoutePlan/Waypoint, schema v3), never joined into ride totals or the rides list. RoutePlanRepository recomputes straight-line distance after every waypoint mutation. RoutePlannerScreen supports tap-to-add, tap-to-delete, and hand-rolled drag-to-move via MapCamera's screen/latlng conversions.
Fixed a real bug found by testing: FlutterMap's initialCenter/initialZoom are read once at construction, so building the map before the waypoints stream's first emission froze the camera on null-island. Also documents a genuine ten-minute test hang traced to Stream.first on a Drift watch() query inside testWidgets, which needs a pump-driven Timer that flutter_test's fake zone never advances on its own.
RideNotificationController seam over flutter_local_notifications; RideNotificationCoordinator wires TripRepository.watchActiveTrip() to it and routes Pause/Resume actions back into RecordingEngine. Instantiated eagerly at app root rather than screen-owned, since a pocketed ride has no visible widget tree. Documents an unresolved risk: geolocator's own foreground-service notification cannot be suppressed, so two notifications may be visible until verified on a device.
Wake lock via a WakelockController seam (mirrors LocationSource), gated strictly to actual recording and released on stop/discard/navigate-away/dispose. High-contrast ripprMountedTheme() applied only to the record screen. BigStat gained a scale param for the headline figure rather than a blanket TextTheme.apply, which crashes on Material 3's default theme.
Feeds RideMap from watchPointsForTrip/watchSegmentsForTrip via autoDispose.family providers. RecordingEngine stays map-free (enforced by architecture_test.dart). RideMap gained lifecycle-aware tile teardown and follow-mode, both shared with the trip-detail map.
Built together because V3-02's settings screen needed something real for
V3-03's units to control -- executed in reverse of the ticket numbering, but
both tickets are independently complete.
V3-03: UnitSystem (metric/imperial) lives in domain/models.dart alongside
Activity, defaulting from Platform.localeName on first launch. Storage stays
SI everywhere -- conversion happens only in ui/format.dart, at the last
possible moment, which is now documented as the file's central invariant.
Threaded through all three screens plus the speed histogram's bucket labels,
which relabel for display without changing how speedHistogram itself bins.
Export was deliberately left untouched: gpx()/geoJson() take no UnitSystem
parameter at all, a stronger guarantee than validating one would be.
V3-02: SettingsScreen reachable from the record screen. Map render toggle
(nothing previously exposed mapEnabledProvider to the user, despite it
existing since T15 -- there was nothing to "move off trip detail" as drafted),
unit selector, upload endpoint with http(s) validation, a copyable device id,
and a licences page via Flutter's built-in showLicensePage. Skipped
package_info_plus (static version string instead) and a privacy-policy link
(none published yet) as disproportionate to an S-sized ticket.
Neither ticket needed the ConfigNotifier the implementation notes proposed:
unitSystemProvider reuses the exact StateProvider-seeded-from-Config pattern
mapEnabledProvider already established, since Config mutates its own backing
SharedPreferences in place and re-assigning the same instance would never
notify a watcher anyway.
A real locale-dependent flake was caught, not just anticipated: a test
asserting a fresh Config defaults to metric failed, because this machine's own
locale resolves to a region in the imperial set. Fixed by seeding an explicit
value before asserting, and config_test.dart's locale test was written from
the start to only prove the fallback path doesn't throw, not to assert which
value it returns.
221 tests (211 -> 221): 13 format_test, 9 config_test, 9 settings_screen_test,
1 confirming the record screen's settings button is genuinely wired. Analyze
clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
Route drawing promoted to a full section. The key point is that road-snapped
shortest path needs a routing engine, and that is the whole decision -- OSRM,
GraphHopper or Valhalla self-hosted, or commercial with a key in the app.
Profiles matter more than usual here, since a motorcycle route and a bicycle
route between the same pins genuinely differ, which is where activity type pays
off. Also flagged that a routing engine's ETA is based on posted limits, not on
how fast the rider actually goes -- and the app already stores enough history to
do better.
Group rides, accounts and paid cloud backup moved out to a new v4 section. They
are one programme, not three: each needs a server, identity and a privacy stance,
and none is worth building alone. Recorded that live positions want a websocket
alongside the existing batched uploader rather than replacing it, since the batch
path is what guarantees no fix is lost -- and that location sharing is a consent
question, not a feature.
Renamed V3-BACKLOG.md to BACKLOG.md, since it now covers both, and made the
split explicit up front: v3 is everything buildable with no server, v4 is
everything that cannot be. That means v3 can proceed indefinitely without anyone
deciding to run infrastructure or hold other people's location data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It turned out to be a positioning decision rather than a feature -- it decides the
store category, the screenshots and who finds the app, and cyclists are a much
larger audience than motorcyclists. Far cheaper to settle before a store listing
exists.
Specified the schema (a text enum on Trip), and flagged that this is the first
real migration the port will ship: the destructive fallback is gone, so it needs
addColumn with a motorcycle default, schemaVersion 2, and a test that opens a v1
database and asserts the rides survive. Getting that path right matters more than
the feature.
The value is in driving per-activity defaults rather than labels: the 1.5 km/h
noise floor is wrong for walking, 10 km/h histogram buckets are useless for
running, and the elevation smoothing window was tuned for 2 Hz road speed.
Also recorded a UI constraint: no picker in front of Start. The founding premise
is press-and-go with gloves, so default to the last activity used and make it
editable on trip detail afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Written for where the app actually is -- you and friends riding -- rather than
jumping straight to a store listing.
Stage 0 is today: on Android, messaging friends the APK is a complete answer and
may always be. On iOS it is not, because free signing gives 7-day builds and needs
your Mac each time, so anyone riding with an iPhone forces the next stage.
Stage 1 is the one that fits a riding group: $99/yr for TestFlight turns install
into one tap with 90-day builds, and $25 once covers Play internal testing. Both
need a privacy policy URL before any tester touches them, since the app records
location.
Stage 2 covers public listings, and flags that background location is the common
rejection on both stores -- pre-empt it with a demo video of a real ride.
Stage 3 lays out monetization honestly: paid up front, a one-time unlock for local
extras, or a subscription for cloud features. The last one is the only model that
justifies recurring money and the only one that brings servers, accounts, GDPR
deletion and Apple's in-app account deletion requirement. Recommendation is to
stay free now, and reach for a one-time unlock before a subscription.
Also noted a positioning question worth settling before any store listing: the
pipeline is activity-agnostic, and whether Rippr is a motorcycle app or a general
ride recorder decides the category, the screenshots and the audience.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Added integration_test/ride_simulation_test.dart, which drives a whole recording
against a simulator that is genuinely moving, then reads the stored ride back
through the provider graph -- the app is uninstalled when a run ends, so anything
worth knowing has to be reported from inside the test.
Attacked an assumption and it survived. simctl location start --speed=15
interpolates between waypoints over time, unlike Android's teleporting geo fix,
so it looked like speed might finally be testable locally. A probe captured 8
fixes, every one 0.0 m/s. The recorded ride shows the consequence plainly: 245.5m
of distance with zero moving time, because every sample sits under the 1.5 km/h
noise floor.
What that leaves verified locally: the app builds and launches, the real UI
renders, Drift opens against iOS storage, plugins register, CoreLocation
delivers fixes, distance accumulates from real movement, trips persist and list,
the detail screen and its map render, and navigation works. Also confirmed
visually that the permission dialog shows the specific usage string written for
Guideline 5.1.1.
What still needs a phone: speed, moving time, speed colouring, elevation against
correlated GPS error, battery, and above all whether iOS suspends a stationary
app mid-ride -- the item that decides whether geolocator is sufficient.
Named gap: no screenshot of the iOS trips or detail screens. Verified
functionally, but tapping the iOS simulator is not scriptable and a fresh install
re-prompts for location over the UI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Launcher, notification and launch screen on both platforms, all generated from
the native app's design sources rather than redrawn.
Android uses the adaptive icon files verbatim from the native app: foreground,
monochrome and the #101418 background colour. That matters because the adaptive
foreground draws its ring at r=32, the corrected radius that survives the
launcher's circular mask -- the first native version used r=43 and the mask
cropped the ring clean off.
iOS gets the full-bleed r=43 artwork instead, which is right there: iOS applies a
rounded superellipse rather than a circle crop, so the wider ring fits. All 15
sizes rendered from the SVG, and the 1024 verified to have no alpha channel,
which the App Store rejects.
The notification icon is ic_stat_rippr, a white silhouette on transparency.
Android masks status bar icons to a solid shape, so pointing this at the colour
launcher icon would render a featureless white blob.
Launch screens were the generated white, which flashes before a dark app on every
cold start. Both now use the app's own tarmac colour with the route mark centred.
Verified on the Android emulator rather than assumed: the launcher shows the
route mark with its orange accent intact, resource 0x7f07007c resolves to
drawable/ic_stat_rippr, and the notification reads "Rippr is recording" with a
clean silhouette. The run also recorded 23 points into a real trip -- the first
time this port has captured anything on Android.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PARITY-AUDIT.md walks every row of the v2.0.1 feature table with the evidence
behind each claim, keeping verified, implemented-but-unproven, and gap strictly
apart rather than ticking boxes in bulk.
Carried the durable docs forward: ARCHITECTURE.md with what changed and why,
a README covering status and the parity harness, and the v3 backlog with a
header noting the two items whose status changed. The native repo's README now
opens with a pointer here and records both bugs the port found. All internal
doc links verified.
The cutover step is deliberately left undone: the application id stays
com.rippr.port so both apps can be installed together, because the real-ride
checklist depends on recording the same ride on both at once. That comparison is
worth more than finishing tidily. The native repo is marked superseded rather
than archived -- it is still the only version that has recorded a real ride.
Also corrected an arithmetic slip: 175 tests, not 178.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T22 -- TelemetryUploader on package:http with MockClient standing in for
MockWebServer, tested against a real in-memory Drift database rather than a fake
DAO. Upload runs on its own timer, injected as a callback so the engine has no
opinion about HTTP and tests need no network. Config on shared_preferences with
a hand-rolled UUID v4. Still no UI for the endpoint, exactly as in the native
app. 7 tests.
T24 -- integration_test/app_test.dart, 4 tests passing on the iOS simulator.
These cover what widget tests cannot: Drift opening against real platform
storage, plugin registration, go_router driving a real Navigator, cold start.
T26 -- RELEASE-IOS.md. Usage strings are specific rather than generic, which is
the leading Guideline 5.1.1 rejection cause; privacy-label answers decided; a
pre-submission list covering the bundle-id switch back to com.rippr, the still
default app icon, a release build, and a demo video for review notes.
T25 written up as REAL-RIDE-CHECKLIST.md but outstanding by nature. Two items
decide real things: force-stopping mid-ride on Android confirms the 111 km
crash-gap bug is fixed, and parking 15 minutes mid-recording on iOS decides
whether geolocator is sufficient or the paid engine is needed.
Two prefer_initializing_formals lints suppressed with a reason: Dart forbids a
named parameter beginning with an underscore, so the suggested fix will not
compile.
178 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
flutter_map with the same OSM tiles and no-API-key reasoning that chose osmdroid.
All four load-bearing behaviours ported and, unlike the native app's map, tested:
one polyline per segment so a pause is a real gap, render-only decimation, the
zoom clamp at OSM's max tile zoom with a short-ride fallback, and a real user
agent. Speed colouring is bucketed per run rather than per-vertex, since neither
osmdroid nor flutter_map makes per-vertex paint reasonable.
Export via share_plus, which also handles the iPad popover anchor a naive port
forgets. It passes the raw stored points, never the map's decimated path.
164 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Theme, go_router shell, record screen, trips list with selection, trip detail
with stats and charts, rename/delete/merge. 15 widget tests -- the first this
project has ever had, closing the gap docs/v3/BACKLOG.md names as v2's largest.
The theme sets bodyColor and displayColor explicitly rather than relying on a
wrapping widget, so the black-on-black regression cannot recur by someone
removing a container, and a widget test now asserts the headline names a colour
distinct from the ground.
Those tests immediately found a real layout bug: with a ride active the record
screen grows to six stat rows and overflows a short screen. Compose clipped this
silently, so it may be latent in the native app; Flutter reports it. Fixed by
making the screen scrollable while still centring when there is room.
Two harness lessons. pumpAndSettle never settles against a repeating timer, so
the elapsed clock now runs only while a ride is active -- better behaviour
anyway, since an idle screen has no clock to advance. And flutter_test asserts
no Timer is pending after disposal, which Drift's stream-query keep-alive cache
trips; tests now tear the tree down and pump past that window.
160 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
geolocator_android's ForegroundNotificationConfig raises a service with
foregroundServiceType=location and holds a wake lock for as long as the position
stream is subscribed, and the plugin declares that service in its own manifest.
That covers everything the native TrackingService used a foreground service and
a PARTIAL_WAKE_LOCK for, so flutter_foreground_task is gone -- and with it both
deprecation warnings that were recorded under T01.
Known parity gap for T27: geolocator's notification cannot carry actions, so the
native Pause/Resume buttons in the shade are not reproduced.
iOS: UIBackgroundModes plus allowBackgroundLocationUpdates keep the isolate and
the writer timer alive while updates flow. Two settings matter more than they
look -- pauseLocationUpdatesAutomatically is false because CoreLocation does not
reliably restart, and activityType is otherNavigation rather than
automotiveNavigation, which would snap fixes to the road network and silently
falsify the recorded path. Usage strings are specific, not generic.
Android deliberately does not request ACCESS_BACKGROUND_LOCATION: a foreground
service with a visible notification is exactly the supported case, and asking
would trigger the harder "Allow all the time" flow for no benefit.
T14 process-death resume is implemented and called at startup, covered by four
tests including the 111 km crash-gap guard.
Verified by building both platforms and launching on the iOS simulator: Drift
opened, Riverpod resolved, restore found no active trip, controls correct.
lib/main.dart is a harness so the pipeline can be exercised on hardware before
Phase 4 builds screens; T15 replaces it.
Phase 3 done: 145 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T10 -- LocationSource abstraction plus FakeLocationSource, so the riskiest code
in the app is testable with no device. Also found that geolocator ships its own
Android foreground service with enableWakeLock, which could drop
flutter_foreground_task and both its deprecation paths; deferred to T12 since it
costs notification actions.
T11 -- the pipeline. Kotlin's unbounded Channel plus blocking-receive writer
becomes a List buffer plus a periodic Timer; Dart's event loop makes a blocking
receive unnecessary and the guarantee is unchanged, since the fix callback only
appends and returns. 24 tests.
Found a real bug in the native app while porting. restoreAfterProcessDeath says
it resumes into a new segment because the dead time is a real gap, but it calls
resumeTrip, which adopts the segment a crash left open. After a pause that is
right; after a crash nothing closed it. Points either side of the dead time then
share a segment, and computeSummary -- the authoritative pass that overwrites the
live estimate on completion -- measures straight through the gap. Reverting the
fix and running the guard shows 111,217 m of phantom distance.
Fixed via TripRepository.resumeIntoNewSegment, which closes the stale segment at
its last recorded point rather than at now, so the dead time is not billed as
ride time either. A deliberate, documented departure from parity: the code
contradicted its own comment and produced silently wrong data.
The first version of that guard passed with the bug still present -- the fixture
used pause(), which closes the segment and stops reproducing the crash. Caught
only by reverting the fix and checking the test failed.
145 tests passing, analyze clean.
Co-Authored-By: Claude Opus 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>
Three tables with CASCADE foreign keys, indices, WAL, and a real MigrationStrategy
from schema version 1. No destructive fallback, ever.
Two things needed deliberate handling. SQLite defaults foreign_keys to OFF and
Drift, unlike Room, does not enable it -- without the pragma every CASCADE is
decorative, so there is now a test that reads the pragma back. And Drift both
snake_cases columns and names row classes after tables; build.yaml sets
case_from_dart_to_sql: preserve so the schema stays column-for-column identical
to Room's, and @DataClassName keeps row types from colliding with the domain
models.
SchemaTest was instrumented and needed a device; the Drift version is a plain
unit test that runs in under a second with no emulator. 16 tests.
95 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T05 — ride_accumulator.dart with the cross-batch anchor intact (12 tests).
T06 — ride_export.dart, GPX 1.1 and GeoJSON (15 tests), parsed with real
parsers rather than substring matching.
T07 — parity harness extended to cover export byte output.
GPX and GeoJSON are byte-identical across Kotlin and Dart: same length, same
FNV hash, including the escaped hostile name and all %.7f/%.1f formatting.
Two harness bugs found and fixed while building it. String.hashCode is not
comparable across Java and Dart, so text comparison used FNV-1a instead. And a
missing jar let a failed Kotlin compile pass as a green run -- run.sh now checks
for the artifact and exits non-zero, the same failure mode as the v2 sweep that
hid a compile error behind /dev/null.
Phase 1 done: 79 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T03 — telemetry.dart, format.dart, live_telemetry.dart, plus pure domain models
(Trip/Segment/TrackPoint/RideStats) with no persistence dependency, so Drift can
map to them in T08 rather than the domain depending on the database.
T04 — ride_statistics.dart including ElevationAccumulator, ported structurally
faithfully: moving average, reversal hysteresis, gainIncludingPending, and the
finish() reconciliation against lastRaw.
T07 (early, because T04 forced it) — tool/parity/ drives identical fixtures
through the real Kotlin files and the Dart port, then diffs. Result: every value
byte-identical, including noisy_gain=38.959594555022136 to the last digit. The
sole difference is run_avg_speed, where Kotlin's 32-bit Float widens to double
with artefacts Dart's binary64 does not reproduce. Documented, not papered over.
That harness settled a real question. The ported elevation test failed at 50.9m
against Kotlin's 35m bound, which looked like a porting bug. It was not: Kotlin's
and Dart's Random(42) are different streams. On a shared LCG fixture both produce
39.0m -- which would also fail Kotlin's own bound. The native guard passes on seed
luck rather than on a property of the algorithm. The Dart test now uses the shared
LCG, asserts bit-equality with Kotlin, and sets its bound from measured behaviour
(25 seeds spanned 24.7-46.7m).
52 tests passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compileSdk 37 (a dependency requires it; the build fails on 36), minSdk 26 for
parity with the native app, targetSdk 36. AGP auto-installed platform 37, CMake
and a 2.8GB NDK during the build since licences were already accepted.
Both artifacts confirmed: Runner.app and app-debug.apk.
Recorded two deprecation warnings against flutter_foreground_task, the plugin
chosen for Android background liveness in T12: no Swift Package Manager support
on iOS, and it applies KGP on Android. Both are future hard errors, so the
LocationSource seam in T10 matters more than assumed.
Also recorded the disk episode: the plan braced for SDK installs, but builds
were what filled the disk (22 GiB to 3.9 GiB). Recovered to 11 GiB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T00 — toolchain green on both platforms. flutter doctor reports no issues.
Pinned Flutter to Temurin 21 (AGP rejects the default JDK 25, same constraint
as the native build) and installed Android cmdline-tools with an explicit
--sdk_root, the trap already documented in the native DEVELOPMENT.md.
No caches were deleted: the 16 GiB disk reading that drove the cleanup plan
re-measured at 36 GiB before anything was removed.
T01 — flutter create for android+ios. applicationId is com.rippr.port, not
com.rippr, so the native app stays installable alongside it during the port;
T27 switches it at cutover.
T02 — geo/geo.dart ported from com.rippr.geo.Geo with all 23 tests, tolerances
and comments carried over unchanged. Kotlin's object namespace became top-level
functions; LatLon stays our own type so the pure layer never depends on
flutter_map.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>