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:
141
docs/ARCHITECTURE.md
Normal file
141
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Architecture
|
||||
|
||||
Why Rippr is built this way. Ported from the native app's `docs/ARCHITECTURE.md`, keeping
|
||||
the reasoning that still holds and recording what changed.
|
||||
|
||||
```
|
||||
LocationSource (geolocator) ← the only file that knows the platforms differ
|
||||
│ LocationFix
|
||||
▼
|
||||
RecordingEngine ── unbounded buffer ──► single writer loop (25 fixes / 2 s)
|
||||
│ │
|
||||
│ ┌─────────────┴──────────────┐
|
||||
│ ▼ ▼
|
||||
│ TripRepository Accumulator
|
||||
│ (all transitions) distance / moving time /
|
||||
│ │ elevation, folded per batch
|
||||
│ ▼
|
||||
│ Drift (SQLite)
|
||||
│ Trip → Segment → TrackPoint
|
||||
│ │
|
||||
└──► LiveTelemetry ▼
|
||||
(speedo) Riverpod providers
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
▼ ▼ ▼
|
||||
RecordScreen TripsScreen TripDetailScreen
|
||||
└─ RideMap (flutter_map)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The decisions that carry the most weight
|
||||
|
||||
### The fix path never blocks
|
||||
|
||||
A GPS fix is appended to an in-memory list and nothing else. A single writer loop drains
|
||||
it in batches every two seconds. Slow disk stalls the writer, never the fix stream, and no
|
||||
fix is dropped under back pressure.
|
||||
|
||||
Kotlin used `Channel(UNLIMITED)` plus a coroutine blocking on `receive()`. Dart has no
|
||||
blocking receive and its single-threaded event loop makes one unnecessary — but the
|
||||
guarantee is identical, and it is the reason a 2 Hz stream does not become two database
|
||||
transactions per second.
|
||||
|
||||
### Recording state lives in the database
|
||||
|
||||
A row in `trips` with `endedAt IS NULL` **is** the fact of an in-progress ride. It survives
|
||||
process death, which an in-memory flag cannot: Android can restart the process with no
|
||||
intent, iOS can suspend and resume, and in both cases a flag would come back `false` while
|
||||
a ride was genuinely underway.
|
||||
|
||||
### Points are stamped at creation
|
||||
|
||||
Every point carries its `tripId` and `segmentId` from the moment it is built, never looked
|
||||
up at write time. This is what makes pause correct: a fix still buffered when the rider
|
||||
pauses is written to the segment it was actually recorded during.
|
||||
|
||||
Refactoring this into a write-time lookup would break the guarantee **silently**.
|
||||
|
||||
### Segments make pause correct rather than cosmetic
|
||||
|
||||
Without them, pausing at a gas station and resuming across town draws a straight line
|
||||
through terrain never ridden — and counts it as distance. Segments propagate all the way
|
||||
through: distance accumulation, map polylines, and GPX `<trkseg>`.
|
||||
|
||||
The same reasoning applies to a crash. See `TripRepository.resumeIntoNewSegment`, which
|
||||
fixes a bug the native app has here.
|
||||
|
||||
### Whoever owns the writes owns the database
|
||||
|
||||
The single most important structural decision of the port. `TrackingService` wrote to Room
|
||||
directly. If Dart owned the schema but a native service owned the writes, every fix would
|
||||
cross a platform channel that is *dead while iOS suspends Dart*.
|
||||
|
||||
So Dart owns both, and the platform layer only has to keep the isolate alive. That is also
|
||||
why there is **one isolate**: the foreground service provides liveness, not a second Dart
|
||||
runtime, so two isolates never contend for one SQLite file.
|
||||
|
||||
### Aggregates are accumulated live, then recomputed authoritatively
|
||||
|
||||
Distance and elevation need consecutive-point differences, so they are folded in the
|
||||
writer loop and persisted per flush — letting the recording screen show live numbers
|
||||
without rescanning the point table.
|
||||
|
||||
Those values are an **estimate**. On completion they are replaced by `computeSummary` over
|
||||
the stored points, so a mid-ride process kill cannot leave permanently skewed totals.
|
||||
|
||||
### Decimation is render-only
|
||||
|
||||
Douglas–Peucker exists in the map path and nowhere else. A three-hour ride is ~21,600
|
||||
points and would jank an undecimated polyline, but storage and export must carry every raw
|
||||
point. Tests assert exact counts in exports to catch a leak.
|
||||
|
||||
---
|
||||
|
||||
## What changed from the native app
|
||||
|
||||
| Native | Port | Why |
|
||||
|---|---|---|
|
||||
| Room | Drift | Same shape; runs on the Dart VM, so data-layer tests need no device |
|
||||
| Foreground service (hand-written) | geolocator's own | Removes `flutter_foreground_task` and two deprecations. Costs notification actions. |
|
||||
| `PARTIAL_WAKE_LOCK` | `enableWakeLock` | Same mechanism, configured rather than coded |
|
||||
| ViewModel + StateFlow | Riverpod | Direct mapping |
|
||||
| navigation-compose | go_router | Three destinations, same shape |
|
||||
| osmdroid | flutter_map | Same OSM raster tiles, same no-API-key reasoning |
|
||||
| FileProvider + ACTION_SEND | share_plus | Also handles the iPad popover anchor |
|
||||
| OkHttp | package:http | Direct mapping |
|
||||
| `Float` | `double` | Dart has no float32. See the parity note below. |
|
||||
|
||||
### The Float→double divergence
|
||||
|
||||
Kotlin stores speed and accuracy as 32-bit `Float`. Dart has no float32, so these widen.
|
||||
Every other value in the app is bit-identical across the two implementations; speed-derived
|
||||
values differ in the last digits (`40.030228` vs `40.03022888407912`).
|
||||
|
||||
This is verified rather than assumed — `tool/parity/run.sh` compiles the real Kotlin
|
||||
sources and diffs them against the Dart port across twenty fixtures.
|
||||
|
||||
### iOS specifics that are easy to get wrong
|
||||
|
||||
- `pauseLocationUpdatesAutomatically: false` — CoreLocation otherwise decides the ride has
|
||||
ended, and does not reliably restart.
|
||||
- `activityType: otherNavigation`, **not** `automotiveNavigation` — the latter snaps fixes
|
||||
to the road network, which silently falsifies a recording of where you actually went.
|
||||
|
||||
---
|
||||
|
||||
## Things that must not regress
|
||||
|
||||
1. The unbounded buffer and single batched writer. Never write to the database from the
|
||||
fix callback.
|
||||
2. Points stamped with `tripId`/`segmentId` at creation.
|
||||
3. Recording state derived from the database, never an in-memory flag.
|
||||
4. No destructive migration. Any schema change ships a real `Migration`.
|
||||
5. Decimation is render-only.
|
||||
6. The theme names its text colours explicitly — a missing content colour once rendered a
|
||||
64 sp figure black-on-black, and no test caught it.
|
||||
7. The map zoom clamp. A short ride will render an empty grid without it.
|
||||
8. `PRAGMA foreign_keys = ON`. SQLite defaults it off and Drift does not set it; without
|
||||
it every CASCADE is decorative.
|
||||
9. Tests use in-memory databases. An instrumented test once wiped a real device's rides.
|
||||
Reference in New Issue
Block a user