Complete Phase 1: accumulator, exports, and full parity coverage

T05 — ride_accumulator.dart with the cross-batch anchor intact (12 tests).
T06 — ride_export.dart, GPX 1.1 and GeoJSON (15 tests), parsed with real
parsers rather than substring matching.
T07 — parity harness extended to cover export byte output.

GPX and GeoJSON are byte-identical across Kotlin and Dart: same length, same
FNV hash, including the escaped hostile name and all %.7f/%.1f formatting.

Two harness bugs found and fixed while building it. String.hashCode is not
comparable across Java and Dart, so text comparison used FNV-1a instead. And a
missing jar let a failed Kotlin compile pass as a green run -- run.sh now checks
for the artifact and exits non-zero, the same failure mode as the v2 sweep that
hid a compile error behind /dev/null.

Phase 1 done: 79 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 01:08:30 -05:00
parent 4af4e3411a
commit 398157b1f5
12 changed files with 918 additions and 6 deletions

View File

@@ -157,3 +157,123 @@ Two lessons, both echoing v2's harness troubles:
- **A killed background job still reports exit code 0.** Both jobs "completed
successfully" *because they were killed*. Never read a success code from a process you
terminated — re-run it cleanly.
---
## T03 — Telemetry, formatting, ephemeral state · **complete**
`telemetry.dart` (msToKmh, sanitizeSpeedKmh, isUsableFix, formatDuration, encodeBatch),
`ui/format.dart`, `telemetry/live_telemetry.dart`. **8 tests.**
Also added `domain/models.dart` — `Trip`, `Segment`, `TrackPoint`, `TripState`,
`RideStats` as plain Dart with **no persistence dependency**. Drift will map *to* these
in T08 rather than the domain depending on the database. This is the Dart equivalent of
the discipline that made the Kotlin logic testable on the JVM.
**Kotlin `Float` becomes Dart `double`.** Dart has no float32. Widening is the right
call — a shim would be friction for sub-millimetre precision on GPS-derived values — but
it means speed-derived values cannot be compared bit-for-bit. See T07 for the measured
consequence.
**`Format` builds its `DateFormat` per call**, unlike the Kotlin original which captured
`Locale.getDefault()` once at class-init. Doing the same in Dart would freeze the format
for the process lifetime and ignore a locale change.
---
## T04 — Ride statistics · **complete**
`stats/ride_statistics.dart` including `ElevationAccumulator`. **21 tests.**
Ported structurally faithfully — moving average, reversal hysteresis,
`gainIncludingPending()`, and the `finish()` reconciliation against `lastRaw`. Nothing
was "improved" during translation, per the plan.
### The failure that proved the port correct
The ported elevation test failed: **50.86 m against Kotlin's 35 m bound.** That looks
exactly like a porting bug in the hardest code in the project.
It was not. The two languages' `Random(42)` are different streams. Rather than tune the
bound blind — which `docs/v3/BACKLOG.md` explicitly warns against — the question was
settled by building the T07 harness early and driving **both implementations from one
shared LCG**:
```
noisy_gain = 38.959594555022136 ← Kotlin
noisy_gain = 38.959594555022136 ← Dart
```
Bit-identical. The port is exact.
**This also found something about the native app.** On the shared fixture the algorithm
yields ~39 m, which would **fail Kotlin's own 35 m bound**. The native test passes on
seed luck, not on a property of the algorithm. A sweep of 25 Dart seeds spanned
24.7–46.7 m (median 36). The Dart test now uses the shared LCG, asserts bit-equality
with Kotlin, and sets its bound from measured behaviour with headroom.
> Worth carrying back to the native repo if it is ever revived: that guard is weaker
> than it looks.
---
## T05 — Live accumulator · **complete**
`recording/ride_accumulator.dart`. **12 tests, green on the first run.**
The cross-batch anchor is intact — `distance across many small batches matches one big
batch` is the guard, and its absence under-reports distance by a few percent invisibly.
---
## T06 — Export writers · **complete**
`export/ride_export.dart`. **15 tests, green on the first run.** Parsed with a real XML
parser and `jsonDecode`, not substring matching, exactly as the Kotlin suite did.
---
## T07 — Cross-language parity harness · **complete**
`tool/parity/` — `run.sh`, `main.kt` (Kotlin oracle), `probe.dart`. Run it with
`JAVA_HOME` set to a 17–21 JDK; needs `kotlinc` (`brew install kotlin`, ~95 MB).
It copies `Geo.kt`, `RideStatistics.kt` and `RideExport.kt` **verbatim** from the native
repo and compiles them against minimal stand-ins for the Room-annotated holders and one
`Telemetry` constant. The files under test are never reimplemented.
### Result
Every key byte-identical across 20 fixtures, including:
| | |
|---|---|
| `noisy_gain` | `38.959594555022136` — the elevation accumulator, to the last digit |
| `simplify_count` | 218 of 21,600 points, identical Douglas–Peucker decisions |
| `gpx_len` / `gpx_fnv` | GPX output byte-identical, including the escaped hostile name |
| `geojson_len` / `geojson_fnv` | GeoJSON byte-identical |
**The single accepted difference** is `run_avg_speed`: `40.030228` (Kotlin `Float`) vs
`40.03022888407912` (Dart `double`) — precisely the divergence predicted in T03. The
harness prints this explanation on failure so a future reader is not left guessing.
### Two traps hit while building it
**`String.hashCode` is not comparable across languages.** The first version compared
Java's and Dart's hashes of the GPX output — which would have "failed" forever for no
reason. Replaced with an FNV-1a implemented identically in both.
**A missing jar silently passed as success.** When `RideExport.kt` was not yet copied,
compilation failed, `java -jar` errored, and the pipeline continued. `run.sh` now checks
for the jar and exits non-zero. This is the same class of bug as the v2 sweep that
returned four identical results because a compile error hid behind `/dev/null` — the
comment in `run.sh` says so explicitly.
---
## Phase 1 complete
**79 tests passing, `flutter analyze` clean.** ~890 lines of Kotlin logic ported, with
its ~965 lines of tests, and proven equivalent rather than assumed equivalent.
Next: **T08 (Drift schema)**, the first task that touches persistence.