Files
rippr/docs/port/PARITY-AUDIT.md
Dylan fc6d8f9dba T27: parity audit and handover
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>
2026-08-15 22:26:56 -05:00

7.1 KiB
Raw Blame History

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.