Add the Rippr Flutter port: source, history bundle, and installable APK
The port runs on Android and iOS and is feature-complete; the native Android app is superseded but kept, since it is still the only version that has recorded real rides. rippr-flutter-1.0-debug.apk is package com.rippr.port, deliberately different from the native com.rippr so both install side by side. Recording the same ride on both at once is the strongest available check that the port is faithful. Added INSTALL.md covering both platforms. Android is a one-line adb install; iOS has no APK equivalent and must be built and signed through Xcode with a free Apple ID, which gives a 7-day profile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
168
rippr-flutter-src/docs/port/PARITY-AUDIT.md
Normal file
168
rippr-flutter-src/docs/port/PARITY-AUDIT.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# T27 — parity audit
|
||||
|
||||
Every row of the native app's v2.0.1 feature table, with the evidence behind each claim.
|
||||
|
||||
**Deliberately not ticked in bulk.** A v2 task once had every acceptance criterion checked
|
||||
by a blanket regex, including items nobody had verified. So each row below states *how* it
|
||||
is known, and three categories are kept apart:
|
||||
|
||||
| | Meaning |
|
||||
|---|---|
|
||||
| ✅ **Verified** | An automated test asserts it, or it was demonstrated running on a device |
|
||||
| 🟡 **Implemented, unproven** | The code is there and reviewed, but nothing has exercised it in the conditions that matter |
|
||||
| ⛔ **Gap** | Present in the native app, absent here |
|
||||
|
||||
**175 automated tests** — 171 unit and widget, 4 integration on a real iOS simulator.
|
||||
|
||||
---
|
||||
|
||||
## The feature table
|
||||
|
||||
### GPS recording via foreground service — ✅ / 🟡
|
||||
|
||||
**Verified:** the pipeline has 24 tests covering all five actions, batching, the unbounded
|
||||
buffer, accuracy filtering, noise-floor sanitisation and process-death resume. The app
|
||||
launches and runs on an iOS simulator.
|
||||
|
||||
**Unproven:** that it records a real ride. No simulator produces velocity, so speed,
|
||||
moving time and speed colouring are structurally untestable here.
|
||||
|
||||
Android liveness moved from a hand-written `TrackingService` to geolocator's own
|
||||
foreground service (`foregroundServiceType="location"`, `enableWakeLock`), verified
|
||||
present in the merged manifest. iOS uses `UIBackgroundModes: [location]` with
|
||||
`allowBackgroundLocationUpdates`.
|
||||
|
||||
→ `REAL-RIDE-CHECKLIST.md` items 1–11, A1–A3, I1–I5.
|
||||
|
||||
### Trips with pause/resume/stop/discard — ✅
|
||||
|
||||
26 repository tests plus 24 engine tests. Every transition idempotent inside a
|
||||
transaction; `startTrip` adopts rather than duplicating; pausing closes the segment;
|
||||
discard cascades; a ride that captured nothing is dropped rather than saved.
|
||||
|
||||
The pause guarantee has its own test: **a fix buffered before a pause is written with the
|
||||
old segment id**, because ids are stamped at creation.
|
||||
|
||||
### Live speed + elapsed clock — ✅
|
||||
|
||||
The 2.0.1 fix is pinned by two widget tests: the headline reads `SPEED`, not `MAX SPEED`,
|
||||
and a separate test asserts the figure names a colour distinct from the background — the
|
||||
black-on-black regression that only a screenshot caught in v2.
|
||||
|
||||
The clock now ticks **only while a ride is active**, which is both correct and what makes
|
||||
the screen testable at all.
|
||||
|
||||
### Path rendered on OpenStreetMap — ✅ / 🟡
|
||||
|
||||
**Verified** by four map tests, which the native map never had: one polyline per segment
|
||||
with a test asserting none straddles a pause, render-only decimation, the zoom clamp at
|
||||
OSM's max tile zoom 19, and a `shortRideZoom` fallback for degenerate bounds — the v2.0
|
||||
empty-grid bug, now guarded.
|
||||
|
||||
**Unproven:** speed colouring. Every simulated path renders in one colour because speed is
|
||||
always zero.
|
||||
|
||||
### Ride statistics + charts — ✅
|
||||
|
||||
21 statistics tests, and stronger evidence than the native app ever had: the parity
|
||||
harness drives the real Kotlin and the Dart port from one shared fixture and they agree
|
||||
**to the last digit**, including the elevation accumulator at `38.959594555022136`.
|
||||
|
||||
Charts degrade to a message below two points rather than rendering a blank box, with a
|
||||
widget test for it.
|
||||
|
||||
**Carried over deliberately:** the ~30 m elevation drift against synthetic noise. Fixing
|
||||
it during translation would have made every differential failure ambiguous. It stays in
|
||||
the v3 backlog, where the standing instruction is *do not tune this blind*.
|
||||
|
||||
### Rename / delete / merge rides — ✅
|
||||
|
||||
Repository tests cover merge re-parenting, rejection of self/active/missing merges,
|
||||
atomicity, and the two properties that matter: **segments are never joined**, and
|
||||
aggregates are **recomputed rather than summed** because distance is not additive across
|
||||
the gap. Widget tests cover the UI, including Merge enabling at exactly two selections.
|
||||
|
||||
Rename collapses empty and whitespace-only input to null.
|
||||
|
||||
### GPX + GeoJSON export — ✅
|
||||
|
||||
15 tests parsed with a real XML parser and `jsonDecode`, not substring matching. The
|
||||
parity harness additionally confirms both formats are **byte-identical** to the Kotlin
|
||||
output — same length, same FNV hash, including an escaped hostile trip name.
|
||||
|
||||
Exports carry the raw stored points; decimation never reaches them.
|
||||
|
||||
### Upload to a REST endpoint — ✅ (still no UI, as before)
|
||||
|
||||
7 tests: disabled endpoint, success, rejection, network failure, multi-batch backlog,
|
||||
partial failure, and per-point trip/segment identity. Runs on its own timer so a dead
|
||||
endpoint cannot disturb recording.
|
||||
|
||||
Parity is exact, including the absence of UI.
|
||||
|
||||
### Live map during recording — ⛔ by design
|
||||
|
||||
Absent in v2, absent here. It is a **v3 decision**, and the backlog records that Dylan has
|
||||
since asked for it and that handlebar mounting changes the app's founding premise.
|
||||
|
||||
### Group ride view — ⛔ by design
|
||||
|
||||
Deferred to v3 in the native app; unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Gaps and departures
|
||||
|
||||
### ⛔ Notification actions — the one capability lost
|
||||
|
||||
The native notification carried Pause/Resume buttons. geolocator's
|
||||
`ForegroundNotificationConfig` cannot carry actions, so the notification is display-only;
|
||||
tapping it opens the app.
|
||||
|
||||
This was the price of dropping `flutter_foreground_task`, which bought the removal of two
|
||||
deprecations that Flutter says become hard build errors. Worth revisiting if the shade
|
||||
controls turn out to matter on a real ride — a separate notification plugin could add them
|
||||
back without touching the engine.
|
||||
|
||||
### ✅ A departure that fixes a native bug
|
||||
|
||||
`restoreAfterProcessDeath` now resumes into a genuinely new segment. The native version
|
||||
adopts the segment a crash left open, so the authoritative recomputation measures straight
|
||||
through the dead time — **111 km of phantom distance** in the reproduction. Documented at
|
||||
length in `PROGRESS.md` and in `TripRepository.resumeIntoNewSegment`.
|
||||
|
||||
### Improvements that are not parity items
|
||||
|
||||
- **The first UI tests this project has ever had** (15), closing what the v3 backlog names
|
||||
as v2's largest coverage gap.
|
||||
- **Instrumented tests became unit tests.** `SchemaTest`, `TripRepositoryTest` and
|
||||
`MergeTest` were 666 lines needing a booted emulator; they now run in about two seconds
|
||||
with nothing running.
|
||||
- **A cross-language parity harness** that can be re-run at any time.
|
||||
- A **record screen layout bug** fixed that Compose was clipping silently.
|
||||
|
||||
---
|
||||
|
||||
## Not done, and why
|
||||
|
||||
**The bundle id is still `com.rippr.port`.** Switching it to `com.rippr` is the final
|
||||
cutover step — but doing it now would make the two apps unable to coexist, and
|
||||
`REAL-RIDE-CHECKLIST.md` item **A3** depends on recording the same ride on both
|
||||
simultaneously. That comparison is the strongest evidence available that the port is
|
||||
faithful.
|
||||
|
||||
**Switch it after T25, not before.**
|
||||
|
||||
Also outstanding before any release: the app icon is still the Flutter default, and every
|
||||
build so far has been debug.
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
Feature parity is **complete in implementation** and **verified as far as anything can be
|
||||
without riding**. One capability is lost (notification actions), one native bug is fixed,
|
||||
and the test coverage is substantially better than the app being replaced.
|
||||
|
||||
What remains is not code. It is a rider, two phones, and
|
||||
[REAL-RIDE-CHECKLIST.md](REAL-RIDE-CHECKLIST.md).
|
||||
Reference in New Issue
Block a user