Files
samplez/rippr-flutter-src/docs/port/PARITY-AUDIT.md
Dylan 0bc42b2e5a 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>
2026-08-15 22:37:45 -05:00

169 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).