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