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>
This commit is contained in:
2026-08-15 22:26:56 -05:00
parent a3bcd014c0
commit fc6d8f9dba
5 changed files with 662 additions and 12 deletions

168
docs/port/PARITY-AUDIT.md Normal file
View 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).

View File

@@ -736,4 +736,64 @@ build, and a demo video for the review notes.
**T22, T23, T24 complete. T25 and T26 need a real device and a real rider.**
`178 tests passing` (171 unit + widget, plus 4 integration on device), analyze clean.
`175 tests passing` — 171 unit and widget, plus 4 integration on device. Analyze clean.
> Corrected: an earlier summary in this file said 178. 171 + 4 is 175.
---
## T27 — Parity audit and handover · **complete, except the cutover step**
[PARITY-AUDIT.md](PARITY-AUDIT.md) walks every row of the v2.0.1 feature table with the
evidence behind each claim, keeping three categories strictly apart: **verified** (a test
asserts it, or it ran on a device), **implemented but unproven**, and **gap**.
Deliberately not ticked in bulk — a v2 task once had every criterion checked by a blanket
regex including unverified items, and that lesson is written into the audit's preamble.
### Documentation carried forward
- `docs/ARCHITECTURE.md` — the durable reasoning, with a table of what changed and why,
the Float→double divergence, and the two iOS settings that are easy to get wrong
- `README.md` — status, the parity harness, and the two native bugs this port found
- `docs/V3-BACKLOG.md` — carried over with a header noting the two items whose status
changed (UI tests are no longer a gap; elevation can now be checked against Kotlin)
- The native repo's `README.md` now opens with a pointer here, and records both bugs
All internal documentation links verified to resolve.
### The cutover step is deliberately NOT done
**The application id stays `com.rippr.port`.**
Switching it to `com.rippr` is the last act of the port, but doing it now would stop the
two apps coexisting — and `REAL-RIDE-CHECKLIST.md` item **A3** depends on recording the
same ride on both simultaneously. That side-by-side comparison is the strongest evidence
available that the port is faithful, and it is worth more than finishing the checklist
tidily.
The native repo is therefore marked **superseded, not archived**: it is still the only
version that has recorded a real ride.
### An arithmetic correction
An earlier entry in this file said 178 tests. The real figure is **175** — 171 unit and
widget, plus 4 integration. Corrected in place.
---
## The port is complete
| | |
|---|---|
| **175 tests** | 171 unit and widget, 4 integration on a real iOS simulator |
| **~4,600 lines** of Dart | plus 3,000 generated by Drift |
| **Both platforms build** | and the app runs on an iOS simulator |
| **Analyze clean** | throughout |
| **Parity proven, not assumed** | `tool/parity/run.sh` diffs against the real Kotlin |
Two bugs found in the native app. One capability lost (notification actions). The first UI
tests the project has ever had.
**What remains is not code.** It is a rider, two phones, and
[REAL-RIDE-CHECKLIST.md](REAL-RIDE-CHECKLIST.md).