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>
169 lines
7.1 KiB
Markdown
169 lines
7.1 KiB
Markdown
# 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).
|